import Foundation
import Testing
@testable import MusicModel

@Suite struct MeterChangeGeometryTests {
    /// 3/4, one 4/4 measure at index 2, back to 3/4 — the First Noel shape.
    private func makeScore() throws -> Score {
        try Acasong.parse("""
        acasong 1
        title: Meters
        time: 3/4
        time@3: 4/4
        time@4: 3/4
        key: C major
        tempo: 100

        part S
        C4 2. | D4 2. | E4 1 | F4 2.
        """)
    }

    @Test func perMeasureGeometry() throws {
        let score = try makeScore()
        try score.validate()
        #expect(score.measureCount == 4)
        #expect(score.timeSignature(ofMeasure: 0).name == "3/4")
        #expect(score.timeSignature(ofMeasure: 2).name == "4/4")
        #expect(score.timeSignature(ofMeasure: 3).name == "3/4")
        #expect(score.tickRange(ofMeasure: 0) == 0..<2880)
        #expect(score.tickRange(ofMeasure: 2) == 5760..<9600)     // 4/4 measure
        #expect(score.tickRange(ofMeasure: 3) == 9600..<12480)
        #expect(score.measureIndex(forTick: 5760) == 2)
        #expect(score.measureIndex(forTick: 9599) == 2)
        #expect(score.measureIndex(forTick: 9600) == 3)
        #expect(score.barlineTicks == [0, 2880, 5760, 9600, 12480])
    }

    @Test func rejectsNotesCrossingChangedBarline() {
        // A whole note in a measure that is still 3/4 must fail.
        let text = """
        acasong 1
        title: Bad
        time: 3/4
        key: C major
        tempo: 100

        part S
        C4 1
        """
        #expect(throws: AcasongError.noteCrossesBarline(line: 8, part: "S", atTick: 0)) {
            _ = try Acasong.parse(text)
        }
    }

    @Test func roundTripsThroughAcasong() throws {
        let score = try makeScore()
        let text = Acasong.serialize(score)
        #expect(text.contains("time@3: 4/4"))
        #expect(text.contains("time@4: 3/4"))
        let reparsed = try Acasong.parse(text)
        #expect(reparsed == score)
        #expect(Acasong.serialize(reparsed) == text)
    }

    @Test func meterChangeWithPickup() throws {
        let score = try Acasong.parse("""
        acasong 1
        title: P
        time: 3/4
        time@2: 4/4
        key: C major
        tempo: 100
        pickup: 1/4

        part S
        C4 4 | D4 2. | E4 1
        """)
        try score.validate()
        // Measure 0 = pickup (960), measure 1 = 3/4, measure 2 (full #2) = 4/4.
        #expect(score.tickRange(ofMeasure: 0) == 0..<960)
        #expect(score.tickRange(ofMeasure: 1) == 960..<3840)
        #expect(score.tickRange(ofMeasure: 2) == 3840..<7680)
    }

    @Test func tempoChangeAfterMeterChange() throws {
        // tempo@3 must land at the grid position that accounts for the
        // 4/4 measure at full-measure 2.
        let score = try Acasong.parse("""
        acasong 1
        title: T
        time: 3/4
        time@2: 4/4
        time@3: 3/4
        key: C major
        tempo: 100
        tempo@3: 80

        part S
        C4 2. | D4 1 | E4 2.
        """)
        #expect(score.tempoMap.changes.count == 2)
        #expect(score.tempoMap.changes[1].tick == 2880 + 3840)
    }
}

@Suite struct MusicXMLMeterChangeTests {
    @Test func importsMidSongMeterChange() throws {
        // 3/4 base, one 4/4 measure in the middle (First Noel pattern).
        let xml = """
        <?xml version="1.0"?>
        <score-partwise>
        <part-list><score-part id="P1"><part-name>S</part-name></score-part></part-list>
        <part id="P1">
        <measure number="1">
          <attributes><divisions>4</divisions>
            <time><beats>3</beats><beat-type>4</beat-type></time>
            <clef><sign>G</sign><line>2</line></clef>
          </attributes>
          <note><pitch><step>C</step><octave>4</octave></pitch><duration>12</duration></note>
        </measure>
        <measure number="2">
          <attributes><time><beats>4</beats><beat-type>4</beat-type></time></attributes>
          <note><pitch><step>D</step><octave>4</octave></pitch><duration>16</duration></note>
        </measure>
        <measure number="3">
          <attributes><time><beats>3</beats><beat-type>4</beat-type></time></attributes>
          <note><pitch><step>E</step><octave>4</octave></pitch><duration>12</duration></note>
        </measure>
        </part>
        </score-partwise>
        """
        let result = try MusicXMLImporter.importScore(data: Data(xml.utf8))
        let score = result.score
        try score.validate()
        #expect(score.timeSignature.name == "3/4")
        #expect(score.meterChanges.count == 2)
        #expect(score.timeSignature(ofMeasure: 1).name == "4/4")
        #expect(score.timeSignature(ofMeasure: 2).name == "3/4")
        #expect(score.parts[0].notes.map(\.durationTicks) == [2880, 3840, 2880])
        // No "ignored" warnings anymore.
        #expect(!result.warnings.contains { $0.contains("ignored") })
    }
}
