import Testing
import MusicModel
@testable import AudioCore

/// 120 bpm, 4/4, 48 kHz: one quarter note = exactly 24 000 samples.
private func makeScore(bpm: Double = 120, notes: String, moreParts: [String: String] = [:]) throws -> Score {
    var text = """
    acasong 1
    title: T
    time: 4/4
    key: C major
    tempo: \(bpm)

    part S
    \(notes)
    """
    for (name, line) in moreParts.sorted(by: { $0.key < $1.key }) {
        text += "\npart \(name)\n\(line)"
    }
    return try Acasong.parse(text)
}

@Suite struct ProgramCompilerTests {
    let sr = 48000.0

    @Test func exactSamplePositions() throws {
        let score = try makeScore(notes: "C4 4 D4 4 R 4 E4 4")
        let program = try ProgramCompiler.compile(
            score: score, options: .init(sampleRate: sr)
        )
        let events = program.tracks[0].events
        #expect(events.count == 3)  // rest is a gap, not an event
        #expect(events[0].startSample == 0)
        #expect(events[0].endSample == 24000)
        #expect(events[1].startSample == 24000)
        #expect(events[1].endSample == 48000)
        #expect(events[2].startSample == 72000)   // after the rest
        #expect(events[2].endSample == 96000)
        #expect(program.endSample == 96000)
        #expect(program.firstSample == 0)
    }

    @Test func tiedNotesMergeIntoOneTone() throws {
        let score = try makeScore(notes: "C4 2 D4 2~ | D4 1")
        let program = try ProgramCompiler.compile(score: score, options: .init(sampleRate: sr))
        let events = program.tracks[0].events
        #expect(events.count == 2)
        #expect(events[1].startSample == 48000)
        #expect(events[1].endSample == 48000 + 24000 * 6)  // half + whole tied
    }

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

        part S
        C4 1 | D4 1
        """
        let score = try Acasong.parse(text)
        let program = try ProgramCompiler.compile(score: score, options: .init(sampleRate: sr))
        let events = program.tracks[0].events
        // Measure 1 at 120 bpm = 2 s = 96 000 samples.
        #expect(events[1].startSample == 96000)
        // Measure 2 at 60 bpm = 4 s = 192 000 samples long.
        #expect(events[1].endSample == 96000 + 192000)
        #expect(program.endSample == 96000 + 192000)
    }

    @Test func tempoMultiplierScales() throws {
        let score = try makeScore(notes: "C4 1")
        let program = try ProgramCompiler.compile(
            score: score, options: .init(sampleRate: sr, tempoMultiplier: 2.0)
        )
        // 240 bpm → whole note = 48 000 samples.
        #expect(program.tracks[0].events[0].endSample == 48000)
    }

    @Test func countInOccupiesNegativeSamples() throws {
        let score = try makeScore(notes: "C4 1")
        let program = try ProgramCompiler.compile(
            score: score, options: .init(sampleRate: sr, countIn: true)
        )
        // One 4/4 measure at 120 bpm = 2 s = 96 000 samples of count-in.
        #expect(program.firstSample == -96000)
        #expect(program.clicks.count == 4)
        #expect(program.clicks.map(\.sample) == [-96000, -72000, -48000, -24000])
        #expect(program.clicks[0].isStrong)
        #expect(!program.clicks[1].isStrong)
        let allCountIn = program.clicks.allSatisfy { $0.isCountIn }
        #expect(allCountIn)
        // Content still starts at 0.
        #expect(program.tracks[0].events[0].startSample == 0)
    }

    @Test func measureRangeStartsAtZero() throws {
        let score = try makeScore(notes: "C4 1 | D4 1 | E4 1")
        let program = try ProgramCompiler.compile(
            score: score, options: .init(sampleRate: sr, measureRange: 1...2)
        )
        let events = program.tracks[0].events
        // D4 starts the range → sample 0.
        #expect(events.count == 2)
        #expect(events[0].startSample == 0)
        #expect(abs(events[0].frequency - Pitch.parse("D4")!.frequency) < 1e-9)
        #expect(program.endSample == 96000 * 2)
    }

    @Test func metronomeClicksDuringPlayback() throws {
        let score = try makeScore(notes: "C4 1 | D4 1")
        let program = try ProgramCompiler.compile(
            score: score,
            options: .init(sampleRate: sr, metronomeDuringPlayback: true)
        )
        #expect(program.clicks.count == 8)
        #expect(program.clicks.map(\.sample) == (0..<8).map { Int64($0) * 24000 })
        #expect(program.clicks[0].isStrong)
        #expect(program.clicks[4].isStrong)
        #expect(!program.clicks[5].isStrong)
    }

    @Test func tonePartFilter() throws {
        let score = try makeScore(notes: "C4 1", moreParts: ["B": "C3 1"])
        let program = try ProgramCompiler.compile(
            score: score,
            options: .init(sampleRate: sr, tonePartNames: ["B"])
        )
        #expect(program.tracks.count == 1)
        #expect(program.tracks[0].partName == "B")
    }

    @Test func fractionalTickMapping() throws {
        let score = try makeScore(notes: "C4 1 | D4 1")
        let program = try ProgramCompiler.compile(score: score, options: .init(sampleRate: sr))
        #expect(abs(program.fractionalTick(atSample: 0) - 0) < 1e-9)
        #expect(abs(program.fractionalTick(atSample: 24000) - 960) < 1e-6)
        #expect(abs(program.fractionalTick(atSample: 96000) - 3840) < 1e-6)
    }
}
