import Testing
import MusicModel
@testable import ScoreLayout

private func score(_ body: String, key: String = "C major", time: String = "4/4") throws -> Score {
    try Acasong.parse("""
    acasong 1
    title: T
    time: \(time)
    key: \(key)
    tempo: 100

    \(body)
    """)
}

@Suite struct StaffMathTests {
    @Test func clefCorrectPositions() {
        // Middle C (C4) on each clef.
        let c4 = Pitch.parse("C4")!
        #expect(StaffMath.position(of: c4, clef: .treble) == -6)     // 1st ledger below
        #expect(StaffMath.position(of: c4, clef: .alto) == 0)        // middle line
        #expect(StaffMath.position(of: c4, clef: .bass) == 6)        // 1st ledger above
        #expect(StaffMath.position(of: Pitch.parse("C3")!, clef: .treble8vb) == -6)

        // The v1 bug: bass-clef placement. G2 sits on the bottom line of bass.
        #expect(StaffMath.position(of: Pitch.parse("G2")!, clef: .bass) == -4)
        // F3 sits on the F line (2nd from top).
        #expect(StaffMath.position(of: Pitch.parse("F3")!, clef: .bass) == 2)
        // B4 = treble middle line.
        #expect(StaffMath.position(of: Pitch.parse("B4")!, clef: .treble) == 0)
    }

    @Test func ledgerLines() {
        #expect(StaffMath.ledgerLinePositions(for: -6) == [-6])          // C4 treble
        #expect(StaffMath.ledgerLinePositions(for: -7) == [-6])          // B3 treble
        #expect(StaffMath.ledgerLinePositions(for: -8) == [-6, -8])      // A3 treble
        #expect(StaffMath.ledgerLinePositions(for: 6) == [6])            // A5 treble
        #expect(StaffMath.ledgerLinePositions(for: 0).isEmpty)
        #expect(StaffMath.ledgerLinePositions(for: 4).isEmpty)           // top line
    }

    @Test func keySignaturePositionsShiftByClef() {
        // D major (2 sharps): F# C#.
        let treble = StaffMath.keySignaturePositions(fifths: 2, clef: .treble)
        let bass = StaffMath.keySignaturePositions(fifths: 2, clef: .bass)
        #expect(treble == [4, 1])
        #expect(bass == [2, -1])
        // Bb major (2 flats) on alto.
        #expect(StaffMath.keySignaturePositions(fifths: -2, clef: .alto) == [-1, 2])
    }

    @Test func stemDirection() {
        #expect(StaffMath.stemUp(position: -1))    // below middle → up
        #expect(!StaffMath.stemUp(position: 0))    // middle line → down (convention)
        #expect(!StaffMath.stemUp(position: 3))
    }

    @Test func beamGroups() {
        let fourFour = TimeSignature(numerator: 4, denominator: 4)!
        #expect(StaffMath.beamGroupBoundaries(fourFour) == [0, 960, 1920, 2880, 3840])
        let sixEight = TimeSignature(numerator: 6, denominator: 8)!
        #expect(StaffMath.beamGroupBoundaries(sixEight) == [0, 1440, 2880])
    }
}

@Suite struct LayoutEngineTests {
    @Test func columnsAlignAcrossParts() throws {
        // Soprano moves in 8ths, bass in halves: bass onsets must land at
        // the same x as the soprano onsets sharing their tick.
        let s = try score("""
        part S
        C5 8 D5 8 E5 8 F5 8 G5 8 A5 8 B5 8 C6 8
        part B [bass]
        C3 2 G3 2
        """)
        let engraved = LayoutEngine.layout(score: s)
        let system = engraved.systems[0]

        let sopranoOnsets = system.events.filter { $0.partIndex == 0 }
        let bassOnsets = system.events.filter { $0.partIndex == 1 }
        #expect(bassOnsets.count == 2)

        // Bass note 2 starts at tick 1920 == soprano note index 4.
        let soprano1920 = sopranoOnsets.first { $0.startTick == 1920 }!
        let bass1920 = bassOnsets.first { $0.startTick == 1920 }!
        #expect(abs(soprano1920.x - bass1920.x) < 1e-9)
    }

    @Test func spacingIsMonotonicInTime() throws {
        let s = try score("""
        part S
        C4 4 D4 4 E4 4 F4 4 | G4 4 A4 4 B4 4 C5 4
        """)
        let engraved = LayoutEngine.layout(score: s)
        for system in engraved.systems {
            let xs = system.events.sorted { $0.startTick < $1.startTick }.map(\.x)
            for (a, b) in zip(xs, xs.dropFirst()) {
                #expect(a < b)
            }
        }
    }

    @Test func longNotesGetMoreRoom() throws {
        let s = try score("""
        part S
        C4 2 D4 8 E4 8 F4 4
        """)
        let engraved = LayoutEngine.layout(score: s)
        let events = engraved.systems[0].events.sorted { $0.startTick < $1.startTick }
        let halfGap = events[1].x - events[0].x
        let eighthGap = events[2].x - events[1].x
        #expect(halfGap > eighthGap)
    }

    @Test func measuresWrapIntoSystems() throws {
        let line = "C4 4 D4 4 E4 4 F4 4"
        let manyMeasures = Array(repeating: line, count: 12).joined(separator: " | ")
        let s = try score("part S\n\(manyMeasures)")
        let engraved = LayoutEngine.layout(score: s, options: .init(systemWidth: 60))
        #expect(engraved.systems.count > 1)
        // All measures accounted for exactly once, in order.
        let indices = engraved.systems.flatMap { $0.measures.map(\.index) }
        #expect(indices == Array(0..<12))
        // No system exceeds its width budget.
        for system in engraved.systems {
            #expect(system.width <= 60 * 1.05, "system \(system.index) width \(system.width)")
        }
    }

    @Test func accidentalsFollowMeasureRules() throws {
        // F#4 twice in one measure: accidental only on the first.
        // After the barline it must appear again.
        let s = try score("""
        part S
        F#4 4 F#4 4 F4 4 R 4 | F#4 1
        """)
        let engraved = LayoutEngine.layout(score: s)
        let sharps = engraved.systems[0].glyphs.filter { $0.glyph == .accidentalSharp }
        let naturals = engraved.systems[0].glyphs.filter { $0.glyph == .accidentalNatural }
        #expect(sharps.count == 2)     // first F#4, then F#4 after barline
        #expect(naturals.count == 1)   // the F-natural cancels
    }

    @Test func keySignatureSuppressesAccidentals() throws {
        // In D major, F# needs no accidental.
        let s = try score("""
        part S
        F#4 1
        """, key: "D major")
        let engraved = LayoutEngine.layout(score: s)
        // Two sharps in the key signature, none attached to the note.
        let sharps = engraved.systems[0].glyphs.filter { $0.glyph == .accidentalSharp }
        #expect(sharps.count == 2)
    }

    @Test func beamingGroupsEighthsByBeat() throws {
        let s = try score("""
        part S
        C4 8 D4 8 E4 8 F4 8 G4 2
        """)
        let engraved = LayoutEngine.layout(score: s)
        let system = engraved.systems[0]
        // Two beat groups of two 8ths → two primary beams; no flags.
        #expect(system.beams.count == 2)
        let flags = system.glyphs.filter { $0.glyph.rawValue.hasPrefix("flag") }
        #expect(flags.isEmpty)
    }

    @Test func loneEighthGetsFlag() throws {
        let s = try score("""
        part S
        C4 8 R 8 D4 4 E4 2
        """)
        let engraved = LayoutEngine.layout(score: s)
        let flags = engraved.systems[0].glyphs.filter { $0.glyph.rawValue.hasPrefix("flag") }
        #expect(flags.count == 1)
        #expect(engraved.systems[0].beams.isEmpty)
    }

    @Test func sixteenthsGetTwoBeamLevels() throws {
        let s = try score("""
        part S
        C4 16 D4 16 E4 16 F4 16 G4 4 A4 2
        """)
        let engraved = LayoutEngine.layout(score: s)
        // One group of four 16ths: primary beam + secondary beam(s).
        let system = engraved.systems[0]
        #expect(system.beams.count >= 2)
    }

    @Test func tiesRendered() throws {
        let s = try score("""
        part S
        C4 2 D4 2~ | D4 1
        """)
        let engraved = LayoutEngine.layout(score: s)
        #expect(engraved.systems[0].ties.count == 1)
    }

    @Test func tupletsMarked() throws {
        let s = try score("""
        part S
        3( C4 8 D4 8 E4 8 ) F4 4 G4 2
        """)
        let engraved = LayoutEngine.layout(score: s)
        let marks = engraved.systems[0].tuplets
        #expect(marks.count == 1)
        #expect(marks[0].number == 3)
    }

    @Test func cursorInterpolatesAcrossColumns() throws {
        let s = try score("""
        part S
        C4 4 D4 4 E4 4 F4 4
        """)
        let engraved = LayoutEngine.layout(score: s)
        let system = engraved.systems[0]
        let x0 = system.cursorX(forTick: 0)!
        let x1 = system.cursorX(forTick: 960)!
        let xMid = system.cursorX(forTick: 480)!
        #expect(x0 < xMid && xMid < x1)
        #expect(abs(xMid - (x0 + x1) / 2) < 1e-9)
        // Cursor works to the very end of the system.
        #expect(system.cursorX(forTick: 3840) != nil)
        #expect(system.cursorX(forTick: 99999) == nil)
    }

    @Test func measureHitTesting() throws {
        let s = try score("""
        part S
        C4 1 | D4 1 | E4 1
        """)
        let engraved = LayoutEngine.layout(score: s)
        let system = engraved.systems[0]
        let m1 = system.measures[1]
        let hit = engraved.measure(at: (x: (m1.xStart + m1.xEnd) / 2, y: system.yOrigin + 5))
        #expect(hit?.measure == 1)
    }

    @Test func bassPartUsesLowPositions() throws {
        // Regression for the v1 bass-clef bug: a G2 in a bass-clef part must
        // be engraved near the bottom line, not floating above the staff.
        let s = try score("""
        part B [bass]
        G2 1
        """)
        let engraved = LayoutEngine.layout(score: s)
        let system = engraved.systems[0]
        let staff = system.staves[0]
        let g2 = system.events[0]
        // Bottom line y = yTop + 4 spaces.
        #expect(abs(g2.y - (staff.yTop + 4.0)) < 1e-9)
    }

    @Test func metadataLoads() {
        let m = SMuFLMetadata.bravura
        #expect(m.fontName == "Bravura")
        #expect(m.bbox(.noteheadBlack).width > 1.0)
        #expect(m.defaults.beamThickness == 0.5)
    }
}
