import Testing
@testable import MusicModel

private func makeScore(pickupTicks: Tick = 0, parts: [Part]) -> Score {
    Score(
        title: "Test",
        timeSignature: TimeSignature(numerator: 4, denominator: 4)!,
        keySignature: .cMajor,
        tempoMap: TempoMap(bpm: 100),
        pickupTicks: pickupTicks,
        parts: parts
    )
}

private func quarterNotes(_ names: [String], from tick: Tick = 0) -> [NoteEvent] {
    var result: [NoteEvent] = []
    var t = tick
    for name in names {
        let pitch = name == "R" ? nil : Pitch.parse(name)!
        result.append(NoteEvent(startTick: t, durationTicks: Ticks.quarter, pitch: pitch))
        t += Ticks.quarter
    }
    return result
}

@Suite struct MeasureGeometryTests {
    @Test func simpleMeasures() {
        let score = makeScore(parts: [
            Part(name: "S", colorIndex: 0, notes: quarterNotes(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]))
        ])
        #expect(score.measureCount == 2)
        #expect(score.tickRange(ofMeasure: 0) == 0..<3840)
        #expect(score.tickRange(ofMeasure: 1) == 3840..<7680)
        #expect(score.measureIndex(forTick: 0) == 0)
        #expect(score.measureIndex(forTick: 3839) == 0)
        #expect(score.measureIndex(forTick: 3840) == 1)
    }

    @Test func pickupMeasure() throws {
        // Pickup of one quarter, then one full measure.
        var notes = quarterNotes(["C4"])                       // pickup
        notes += quarterNotes(["D4", "E4", "F4", "G4"], from: 960)
        let score = makeScore(pickupTicks: 960, parts: [Part(name: "S", colorIndex: 0, notes: notes)])

        #expect(score.measureCount == 2)
        #expect(score.tickRange(ofMeasure: 0) == 0..<960)      // pickup measure
        #expect(score.tickRange(ofMeasure: 1) == 960..<4800)
        #expect(score.measureIndex(forTick: 959) == 0)
        #expect(score.measureIndex(forTick: 960) == 1)
        try score.validate()
    }
}

@Suite struct ValidationTests {
    @Test func validScorePasses() throws {
        let score = makeScore(parts: [
            Part(name: "S", colorIndex: 0, notes: quarterNotes(["C4", "D4", "E4", "R"]))
        ])
        try score.validate()
    }

    @Test func rejectsBarlineCrossing() {
        // A half note starting on beat 4 crosses into measure 2.
        var notes = quarterNotes(["C4", "D4", "E4"])
        notes.append(NoteEvent(startTick: 2880, durationTicks: Ticks.half, pitch: Pitch.parse("F4")))
        notes.append(NoteEvent(startTick: 4800, durationTicks: 2880, pitch: nil))
        let score = makeScore(parts: [Part(name: "S", colorIndex: 0, notes: notes)])

        #expect(throws: Score.ValidationError.noteCrossesBarline(part: "S", measure: 0, atTick: 2880)) {
            try score.validate()
        }
    }

    @Test func rejectsOverlap() {
        var notes = quarterNotes(["C4", "D4", "E4", "F4"])
        notes[1].startTick = 500  // overlaps note 0
        notes[1] = NoteEvent(startTick: 500, durationTicks: Ticks.quarter, pitch: Pitch.parse("D4"))
        let score = makeScore(parts: [Part(name: "S", colorIndex: 0, notes: notes)])
        #expect(throws: Score.ValidationError.overlappingNotes(part: "S", atTick: 500)) {
            try score.validate()
        }
    }

    @Test func rejectsUnequalParts() {
        let score = makeScore(parts: [
            Part(name: "S", colorIndex: 0, notes: quarterNotes(["C4", "D4", "E4", "F4"])),
            Part(name: "A", colorIndex: 1, notes: quarterNotes(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"])),
        ])
        #expect(throws: Score.ValidationError.partsUnequalLength(part: "S", expected: 7680, actual: 3840)) {
            try score.validate()
        }
    }

    @Test func validTie() throws {
        var notes = quarterNotes(["C4", "D4", "E4"])
        notes.append(NoteEvent(startTick: 2880, durationTicks: Ticks.quarter, pitch: Pitch.parse("F4"), tieToNext: true))
        notes.append(NoteEvent(startTick: 3840, durationTicks: 3840, pitch: Pitch.parse("F4")))
        let score = makeScore(parts: [Part(name: "S", colorIndex: 0, notes: notes)])
        try score.validate()
    }

    @Test func rejectsTieToDifferentPitch() {
        var notes = quarterNotes(["C4", "D4", "E4"])
        notes.append(NoteEvent(startTick: 2880, durationTicks: Ticks.quarter, pitch: Pitch.parse("F4"), tieToNext: true))
        notes.append(NoteEvent(startTick: 3840, durationTicks: 3840, pitch: Pitch.parse("G4")))
        let score = makeScore(parts: [Part(name: "S", colorIndex: 0, notes: notes)])
        #expect(throws: Score.ValidationError.invalidTie(part: "S", atTick: 2880, reason: "tied notes must have the same pitch")) {
            try score.validate()
        }
    }

    @Test func rejectsIncompleteFinalMeasure() {
        let score = makeScore(parts: [
            Part(name: "S", colorIndex: 0, notes: quarterNotes(["C4", "D4", "E4"]))
        ])
        #expect(throws: (any Error).self) { try score.validate() }
    }
}
