import MusicAssistantCore import XCTest final class SongProjectModelTests: XCTestCase { func testSongProjectStoresCoreDomainCollections() { let section = SongSection( id: "section-verse-1", type: .verse, title: "Verse 1", startTime: 12, endTime: 42, lyrics: "Draft verse", emotion: "hopeful", energy: 6, vocalDirection: "soft lead", productionDirection: "light percussion" ) let placement = InstrumentPlacement( sectionId: section.id, startTime: section.startTime, endTime: section.endTime, direction: "enter after the first line" ) let instrument = InstrumentTrack( instrumentId: "oud", selected: true, variant: "Arabic oud", playingStyle: "picked", role: "main melodic hook", autoArrangementEnabled: false, placements: [placement] ) let vocalist = Vocalist( id: "lead", label: "Lead vocalist", voiceType: "warm tenor", genderSelection: "male", performanceStyle: "sung", assignedSections: [section.id] ) let project = SongProject( id: "project-1", title: "Desert Lights", idea: "A hopeful Arabic pop song", duration: SongDuration(seconds: 180), conversationMode: .discuss, languages: [LanguageSelection(id: "arabic", name: "Arabic")], dialects: [DialectSelection(id: "levantine", name: "Levantine", languageID: "arabic")], arabicPronunciationSettings: ArabicPronunciationSettings(isEnabled: true), genres: [GenreStyle(id: "arabic-pop", name: "Arabic Pop")], moods: [MoodTag(id: "hopeful", name: "Hopeful")], emotionalArc: [EmotionalArcPoint(id: "arc-1", sectionID: section.id, emotion: "hopeful")], bpm: ManualAutoValue(mode: .manual, value: 96), key: ManualAutoValue(mode: .auto), scale: ManualAutoValue(mode: .auto), maqam: ManualAutoValue(mode: .manual, value: "Hijaz"), sections: [section], instruments: [instrument], vocalists: [vocalist], lyrics: Lyrics(text: "Draft lyrics", sectionTexts: [section.id: "Draft verse"]), productionDirections: [ProductionDirection(id: "prod-1", sectionID: section.id, text: "add lift")], sunoOutput: SunoOutput(lyricsText: "Draft lyrics", stylePrompt: "Arabic pop with oud") ) XCTAssertEqual(project.sections.first?.type, .verse) XCTAssertEqual(project.instruments.first?.instrumentId, "oud") XCTAssertEqual(project.instruments.first?.placements.first?.sectionId, section.id) XCTAssertEqual(project.vocalists.first?.assignedSections, [section.id]) XCTAssertEqual(project.bpm?.mode, .manual) XCTAssertEqual(project.maqam?.value, "Hijaz") } func testSongProjectSnapshotCopiesVersionableState() { let project = SongProject( id: "project-2", title: "Night Drive", idea: "Electronic track", genres: [GenreStyle(id: "synthwave", name: "Synthwave")], instruments: [InstrumentTrack(instrumentId: "synthesizer")] ) let snapshot = SongProjectSnapshot(project: project) let version = SongVersion(label: "Initial direction", snapshot: snapshot) XCTAssertEqual(version.snapshot.title, project.title) XCTAssertEqual(version.snapshot.idea, project.idea) XCTAssertEqual(version.snapshot.genres, project.genres) XCTAssertEqual(version.snapshot.instruments, project.instruments) } func testGenreStylesAreModeledAsMultiSelect() { let arabicPop = GenreStyle(id: "arabic-pop", name: "Arabic Pop") let synthwave = GenreStyle(id: "synthwave", name: "Synthwave") var project = SongProject( id: "project-3", title: "Hybrid", idea: "Blend traditional melody with electronic production" ) project.selectGenreStyle(arabicPop) project.selectGenreStyle(synthwave) project.selectGenreStyle(arabicPop) XCTAssertEqual(project.genres, [arabicPop, synthwave]) XCTAssertTrue(project.isGenreStyleSelected(arabicPop)) XCTAssertEqual(project.selectedGenreStyleIDs, ["arabic-pop", "synthwave"]) project.deselectGenreStyle(id: arabicPop.id) XCTAssertEqual(project.genres, [synthwave]) XCTAssertFalse(project.isGenreStyleSelected(arabicPop)) } func testSongSectionsKeepExplicitOrderAndOptionalTimestamps() { var project = SongProject( id: "project-4", title: "Section Flow", idea: "Build a classic song structure" ) let intro = SongSection(id: "intro", type: .intro, title: "Intro") let verse = SongSection( id: "verse", type: .verse, title: "Verse", startTime: 15, endTime: 45 ) let chorus = SongSection(id: "chorus", type: .chorus, title: "Chorus") project.appendSection(verse) project.appendSection(chorus) project.appendSection(intro) project.moveSection(id: intro.id, toOrder: 0) XCTAssertEqual(project.orderedSections.map(\.id), ["intro", "verse", "chorus"]) XCTAssertNil(project.orderedSections.first?.startTime) XCTAssertNil(project.orderedSections.first?.endTime) XCTAssertEqual(project.orderedSections[1].startTime, 15) XCTAssertEqual(project.orderedSections[1].endTime, 45) XCTAssertEqual(project.orderedSections.map(\.order), [0, 1, 2]) } func testInstrumentsTrackVariantsRolesSelectionAndPlacements() { var project = SongProject( id: "project-5", title: "Instrument Plan", idea: "Feature oud and violin with clear entries" ) let oud = InstrumentTrack( instrumentId: "oud", selected: true, variant: "Arabic oud", playingStyle: "tremolo", role: "main hook", autoArrangementEnabled: false ) let introPlacement = InstrumentPlacement( sectionId: "intro", startTime: 0, endTime: 12, direction: "solo intro" ) let chorusPlacement = InstrumentPlacement( sectionId: "chorus", direction: "double the vocal melody" ) project.selectInstrument(oud) project.addInstrumentPlacement(introPlacement, to: oud.instrumentId) project.addInstrumentPlacement(chorusPlacement, to: "violin") project.configureInstrument( id: "violin", variant: "solo violin", playingStyle: "legato", role: "countermelody", autoArrangementEnabled: true ) XCTAssertEqual(project.selectedInstrumentIDs, ["oud", "violin"]) XCTAssertEqual(project.instrumentTrack(for: "oud")?.variant, "Arabic oud") XCTAssertEqual(project.instrumentTrack(for: "oud")?.role, "main hook") XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement]) XCTAssertEqual(project.instrumentTrack(for: "violin")?.variant, "solo violin") XCTAssertEqual(project.instrumentTrack(for: "violin")?.playingStyle, "legato") XCTAssertEqual(project.instrumentTrack(for: "violin")?.role, "countermelody") XCTAssertEqual(project.instrumentTrack(for: "violin")?.placements, [chorusPlacement]) project.deselectInstrument(id: "oud") XCTAssertFalse(project.isInstrumentSelected(id: "oud")) XCTAssertEqual(project.selectedInstrumentIDs, ["violin"]) XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement]) } }