import Testing
@testable import MusicModel

private let simpleSong = """
acasong 1
title: Test Song
time: 4/4
key: G major
tempo: 80

part Soprano [treble]
G4 4 A4 4 B4 4 A4 4 | G4 4 B4 4 D5 2
part Bass [bass]
G3 4 A3 4 B3 4 A3 4 | G3 2 G3 2
"""

@Suite struct AcasongParserTests {
    @Test func parsesSimpleSong() throws {
        let score = try Acasong.parse(simpleSong)
        #expect(score.title == "Test Song")
        #expect(score.timeSignature.name == "4/4")
        #expect(score.keySignature.name == "G major")
        #expect(score.tempoMap.baseBPM == 80)
        #expect(score.parts.count == 2)
        #expect(score.parts[0].name == "Soprano")
        #expect(score.parts[0].clef == .treble)
        #expect(score.parts[1].clef == .bass)
        #expect(score.parts[0].notes.count == 7)
        #expect(score.measureCount == 2)
        try score.validate()
    }

    @Test func parsesTies() throws {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        C4 2 D4 4 E4 4~ | E4 1
        """
        let score = try Acasong.parse(text)
        let notes = score.parts[0].notes
        #expect(notes[2].tieToNext)
        #expect(notes[3].pitch == notes[2].pitch)
        try score.validate()
    }

    @Test func rejectsBarlineCrossing() {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        C4 2 D4 4 E4 2 R 4 R 2.
        """
        #expect(throws: AcasongError.noteCrossesBarline(line: 8, part: "S", atTick: 2880)) {
            _ = try Acasong.parse(text)
        }
    }

    @Test func rejectsBadBarCheck() {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        C4 2 D4 4 | E4 4 R 1
        """
        #expect(throws: AcasongError.badBarCheck(line: 8, part: "S", atTick: 2880)) {
            _ = try Acasong.parse(text)
        }
    }

    @Test func parsesTriplets() throws {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        3( C4 8 D4 8 E4 8 ) F4 4 G4 2
        """
        let score = try Acasong.parse(text)
        let notes = score.parts[0].notes
        #expect(notes.count == 5)
        #expect(notes[0].durationTicks == 320)
        #expect(notes[1].durationTicks == 320)
        #expect(notes[2].durationTicks == 320)
        #expect(notes[0].tuplet?.actual == 3)
        #expect(notes[0].tuplet?.normal == 2)
        #expect(notes[3].startTick == 960)  // triplet occupied exactly one beat
        try score.validate()
    }

    @Test func parsesPickup() throws {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100
        pickup: 1/4

        part S
        C4 4 | D4 4 E4 4 F4 4 G4 4
        """
        let score = try Acasong.parse(text)
        #expect(score.pickupTicks == 960)
        #expect(score.measureCount == 2)
        try score.validate()
    }

    @Test func parsesTempoChanges() throws {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100
        tempo@2: 120

        part S
        C4 1 | D4 1 | E4 1
        """
        let score = try Acasong.parse(text)
        #expect(score.tempoMap.changes.count == 2)
        #expect(score.tempoMap.changes[1].tick == 3840)
        #expect(score.tempoMap.bpm(at: 3840) == 120)
    }

    @Test func parsesLyrics() throws {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        C4 4 D4 4 E4 2~ | E4 4 R 2.
        lyrics S: Hel- lo world
        """
        let score = try Acasong.parse(text)
        let notes = score.parts[0].notes
        #expect(notes[0].lyric == "Hel-")
        #expect(notes[1].lyric == "lo")
        #expect(notes[2].lyric == "world")
        #expect(notes[3].lyric == nil)  // tie continuation gets no syllable
    }

    @Test func rejectsTooManyLyrics() {
        let text = """
        acasong 1
        title: T
        time: 4/4
        key: C major
        tempo: 100

        part S
        C4 2 D4 2
        lyrics S: one two three
        """
        #expect(throws: AcasongError.tooManyLyrics(line: 9, part: "S", syllables: 3, notes: 2)) {
            _ = try Acasong.parse(text)
        }
    }

    @Test func commentsAndBlankLines() throws {
        let text = """
        acasong 1
        title: T // this is the title
        time: 4/4

        key: C major
        tempo: 100

        // a whole comment line
        part S
        C4 1 // whole note
        """
        let score = try Acasong.parse(text)
        #expect(score.title == "T")
        #expect(score.parts[0].notes.count == 1)
    }
}

@Suite struct AcasongRoundTripTests {
    @Test func roundTripPreservesScore() throws {
        let score = try Acasong.parse(simpleSong)
        let serialized = Acasong.serialize(score)
        let reparsed = try Acasong.parse(serialized)
        #expect(reparsed == score)
    }

    @Test func canonicalFormIsStable() throws {
        let score = try Acasong.parse(simpleSong)
        let once = Acasong.serialize(score)
        let twice = Acasong.serialize(try Acasong.parse(once))
        #expect(once == twice)
    }

    @Test func roundTripWithAllFeatures() throws {
        let text = """
        acasong 1
        title: Kitchen Sink
        composer: Trad.
        time: 6/8
        key: F# minor
        tempo: 92.5
        tempo@3: 110
        pickup: 1/8

        part Lead Tenor [treble8vb]
        C#4 8 | F#4 8 G#4 8 A4 8 3( B4 16 C#5 16 B4 16 ) A4 4 | F#4 4.~ F#4 4.
        lyrics Lead Tenor: Oh sing- ing all through the night
        part Bass [bass]
        R 8 | F#2 4. C#3 4. | F#2 2.
        """
        let score = try Acasong.parse(text)
        try score.validate()
        let serialized = Acasong.serialize(score)
        let reparsed = try Acasong.parse(serialized)
        #expect(reparsed == score)
        #expect(Acasong.serialize(reparsed) == serialized)
    }
}

@Suite struct V1ConverterTests {
    @Test func convertsV1Format() throws {
        let v1 = """
        Simple Harmony Test
        4/4, G major, 80 bpm
        Soprano G4 4 A4 4 B4 4 A4 4 G4 4 B4 4 D5 2
        Bass G3 4 A3 4 B3 4 A3 4 G3 2 G3 2
        """
        let score = try V1Converter.convert(v1)
        #expect(score.title == "Simple Harmony Test")
        #expect(score.parts.count == 2)
        #expect(score.tempoMap.baseBPM == 80)
        try score.validate()
    }

    @Test func splitsBarlineCrossingsWithTies() throws {
        // Half note starting on beat 4 — v1 allowed this; converter must split+tie.
        let v1 = """
        Crossing
        4/4, C major, 100
        Solo C4 4 C4 4 C4 4 C4 2 C4 4 C4 4 C4 2
        """
        let score = try V1Converter.convert(v1)
        try score.validate()
        let notes = score.parts[0].notes
        // The 2 at beat 4 became 4~ | 4
        #expect(notes[3].durationTicks == Ticks.quarter)
        #expect(notes[3].tieToNext)
        #expect(notes[4].durationTicks == Ticks.quarter)
        #expect(notes[4].pitch == notes[3].pitch)
    }

    @Test func padsShortParts() throws {
        let v1 = """
        Uneven
        4/4, C major, 100
        Long C4 1 C4 1
        Short C4 1
        """
        let score = try V1Converter.convert(v1)
        try score.validate()
        #expect(score.parts[1].endTick == score.parts[0].endTick)
    }
}
