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]) } func testMultipleVocalistsCanBeAssignedToSections() { var project = SongProject( id: "project-6", title: "Vocal Arrangement", idea: "Lead and harmony share selected sections" ) let lead = Vocalist( id: "lead", label: "Lead", voiceType: "tenor", genderSelection: "male", performanceStyle: "sung", assignedSections: ["verse", "verse"] ) let harmony = Vocalist( id: "harmony", label: "Harmony", voiceType: "alto", genderSelection: "female", performanceStyle: "harmony" ) project.upsertVocalist(lead) project.upsertVocalist(harmony) project.assignSection("chorus", toVocalistID: "lead") project.assignSection("chorus", toVocalistID: "lead") project.assignSection("chorus", toVocalistID: "harmony") project.assignSection("bridge", toVocalistID: "harmony") XCTAssertEqual(project.vocalistIDs, ["lead", "harmony"]) XCTAssertEqual(project.vocalist(for: "lead")?.assignedSections, ["verse", "chorus"]) XCTAssertEqual(project.vocalist(for: "harmony")?.assignedSections, ["chorus", "bridge"]) XCTAssertEqual(project.vocalistsAssigned(toSectionID: "chorus").map(\.id), ["lead", "harmony"]) project.unassignSection("chorus", fromVocalistID: "lead") XCTAssertEqual(project.vocalist(for: "lead")?.assignedSections, ["verse"]) XCTAssertEqual(project.vocalistsAssigned(toSectionID: "chorus").map(\.id), ["harmony"]) project.removeVocalist(id: "harmony") XCTAssertNil(project.vocalist(for: "harmony")) } func testLanguagesDialectsAndArabicPronunciationSettingsAreModeled() { var project = SongProject( id: "project-7", title: "Multilingual Song", idea: "Arabic and English hook" ) let arabic = LanguageSelection(id: "arabic", name: "Arabic") let english = LanguageSelection(id: "english", name: "English") let levantine = DialectSelection(id: "levantine", name: "Levantine", languageID: arabic.id) let gulf = DialectSelection(id: "gulf", name: "Gulf", languageID: arabic.id) project.upsertLanguage(arabic) project.upsertLanguage(english) project.upsertLanguage(LanguageSelection(id: "english", name: "English")) project.upsertDialect(levantine) project.upsertDialect(gulf) project.arabicPronunciationSettings = ArabicPronunciationSettings( isEnabled: true, diacritizationPolicy: .pronunciationTargeted, tanweenPolicy: .preserveUserInput, userNotes: "Preserve dialect phrasing", preservedSpellings: ["ya habibi"] ) XCTAssertEqual(project.languageIDs, ["arabic", "english"]) XCTAssertEqual(project.dialectIDs, ["levantine", "gulf"]) XCTAssertEqual(project.language(for: arabic.id), arabic) XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine, gulf]) XCTAssertTrue(project.arabicPronunciationSettings.isEnabled) XCTAssertEqual(project.arabicPronunciationSettings.diacritizationPolicy, .pronunciationTargeted) XCTAssertEqual(project.arabicPronunciationSettings.tanweenPolicy, .preserveUserInput) project.removeDialect(id: gulf.id) XCTAssertNil(project.dialect(for: gulf.id)) XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine]) project.removeLanguage(id: arabic.id) XCTAssertNil(project.language(for: arabic.id)) XCTAssertEqual(project.dialects(forLanguageID: arabic.id), []) XCTAssertEqual(project.languageIDs, ["english"]) } }