import Foundation import MusicAssistantCore import XCTest final class LocalSongProjectStoreTests: XCTestCase { private var temporaryDirectoryURL: URL! override func setUpWithError() throws { temporaryDirectoryURL = FileManager.default.temporaryDirectory .appendingPathComponent("music-assistant-store-tests") .appendingPathComponent(UUID().uuidString) } override func tearDownWithError() throws { if let temporaryDirectoryURL { try? FileManager.default.removeItem(at: temporaryDirectoryURL) } temporaryDirectoryURL = nil } func testCreatesOpensSavesAndListsSongProjectsLocally() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let createdAt = Date(timeIntervalSince1970: 100) let updatedAt = Date(timeIntervalSince1970: 200) var project = SongProject( id: "local-project", title: "Local Project", idea: "Persist this song locally", genres: [GenreStyle(id: "pop", name: "Pop")], createdAt: createdAt, updatedAt: updatedAt ) try await store.create(project) let createdProjectIDs = try await store.loadProjectIDs() let openedProject = try await store.open(id: project.id) XCTAssertEqual(createdProjectIDs, ["local-project"]) XCTAssertEqual(openedProject, project) project.title = "Updated Local Project" project.setBPMManually(100) try await store.save(project) let updatedProject = try await store.open(id: project.id) XCTAssertEqual(updatedProject, project) } func testPersistsSelectedInstrumentsOnSongProject() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) var project = SongProject( id: "instrument-selection-project", title: "Instrument Selection", idea: "Persist selected catalog instruments" ) project.setInstrumentSelected(id: "oud", isSelected: true, variant: "Oud") project.setInstrumentSelected(id: "violin", isSelected: true, variant: "Violin") project.setInstrumentSelected(id: "oud", isSelected: false) try await store.create(project) var openedProject = try await store.open(id: project.id) XCTAssertFalse(openedProject.isInstrumentSelected(id: "oud")) XCTAssertTrue(openedProject.isInstrumentSelected(id: "violin")) XCTAssertEqual(openedProject.instrumentTrack(for: "oud")?.variant, "Oud") XCTAssertEqual(openedProject.selectedInstrumentIDs, ["violin"]) openedProject.setInstrumentSelected(id: "oud", isSelected: true, variant: "Oud") try await store.save(openedProject) let savedProject = try await store.open(id: project.id) XCTAssertTrue(savedProject.isInstrumentSelected(id: "oud")) XCTAssertTrue(savedProject.isInstrumentSelected(id: "violin")) XCTAssertEqual(savedProject.selectedInstrumentIDs, ["oud", "violin"]) } func testRestoresPersistedProjectVersionAndKeepsVersionHistory() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let restoredAt = Date(timeIntervalSince1970: 900) var project = SongProject( id: "version-recovery-project", title: "Original Direction", idea: "Start with acoustic instruments", genres: [GenreStyle(id: "folk", name: "Folk")], lyrics: Lyrics(text: "Original lyrics"), createdAt: Date(timeIntervalSince1970: 100), updatedAt: Date(timeIntervalSince1970: 200) ) _ = project.createVersion( id: "original-version", label: "Original direction", createdAt: Date(timeIntervalSince1970: 300) ) project.title = "Later Direction" project.idea = "Add electronic elements" project.selectInstrument(InstrumentTrack(instrumentId: "oud")) project.lyrics = Lyrics(text: "Later lyrics") _ = project.createVersion( id: "later-version", label: "Later direction", createdAt: Date(timeIntervalSince1970: 400) ) project.title = "Unsaved direction" try await store.create(project) var recoveredProject = try await store.open(id: project.id) XCTAssertTrue(recoveredProject.restoreVersion(id: "original-version", restoredAt: restoredAt)) try await store.save(recoveredProject) let restoredProject = try await store.open(id: project.id) XCTAssertEqual(restoredProject.title, "Original Direction") XCTAssertEqual(restoredProject.idea, "Start with acoustic instruments") XCTAssertEqual(restoredProject.genres, [GenreStyle(id: "folk", name: "Folk")]) XCTAssertTrue(restoredProject.instruments.isEmpty) XCTAssertEqual(restoredProject.lyrics, Lyrics(text: "Original lyrics")) XCTAssertEqual(restoredProject.updatedAt, restoredAt) XCTAssertEqual(restoredProject.versions.map(\.id), ["original-version", "later-version"]) XCTAssertEqual(restoredProject.version(id: "later-version")?.snapshot.title, "Later Direction") } func testLoadsProjectListSortedByMostRecentUpdate() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let older = SongProject( id: "older-project", title: "Older", idea: "Created first", createdAt: Date(timeIntervalSince1970: 100), updatedAt: Date(timeIntervalSince1970: 150) ) let newer = SongProject( id: "newer-project", title: "Newer", idea: "Updated later", createdAt: Date(timeIntervalSince1970: 200), updatedAt: Date(timeIntervalSince1970: 300) ) try await store.create(older) try await store.create(newer) let projectList = try await store.loadProjectList() let projectIDs = try await store.loadProjectIDs() XCTAssertEqual( projectList, [ SongProjectListItem(project: newer), SongProjectListItem(project: older) ] ) XCTAssertEqual(projectIDs, ["newer-project", "older-project"]) } func testCreateDoesNotOverwriteExistingProject() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let project = SongProject( id: "duplicate-project", title: "Duplicate", idea: "Do not overwrite" ) try await store.create(project) do { try await store.create(project) XCTFail("Expected duplicate project creation to fail.") } catch let error as LocalSongProjectStoreError { XCTAssertEqual(error, .projectAlreadyExists(project.id)) } } func testDeletesSongProjectAndRemovesItFromTheProjectList() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let project = SongProject( id: "deletable-project", title: "Delete Me", idea: "Remove this local project" ) try await store.create(project) try await store.delete(id: project.id) let remainingProjects = try await store.loadProjectList() XCTAssertTrue(remainingProjects.isEmpty) do { _ = try await store.open(id: project.id) XCTFail("Expected deleted project open to fail.") } catch let error as LocalSongProjectStoreError { XCTAssertEqual(error, .projectNotFound(project.id)) } } func testOpenMissingProjectThrowsNotFound() async throws { let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) do { _ = try await store.open(id: "missing") XCTFail("Expected missing project open to fail.") } catch let error as LocalSongProjectStoreError { XCTAssertEqual(error, .projectNotFound("missing")) } } }