import Testing
@testable import MusicModel

@Suite struct TempoMapTests {
    @Test func constantTempo() {
        let map = TempoMap(bpm: 120)
        // 120 bpm → quarter = 0.5 s
        #expect(abs(map.seconds(atTick: 960) - 0.5) < 1e-12)
        #expect(abs(map.seconds(atTick: 3840) - 2.0) < 1e-12)
        #expect(map.seconds(atTick: 0) == 0)
    }

    @Test func negativeTicksForCountIn() {
        let map = TempoMap(bpm: 120)
        #expect(abs(map.seconds(atTick: -3840) - (-2.0)) < 1e-12)
    }

    @Test func midSongChange() {
        // 4 quarters at 60 bpm (1 s each), then 120 bpm (0.5 s each)
        let map = TempoMap(changes: [
            .init(tick: 0, bpm: 60),
            .init(tick: 3840, bpm: 120),
        ])!
        #expect(abs(map.seconds(atTick: 3840) - 4.0) < 1e-12)
        #expect(abs(map.seconds(atTick: 3840 + 960) - 4.5) < 1e-12)
        #expect(map.bpm(at: 0) == 60)
        #expect(map.bpm(at: 3839) == 60)
        #expect(map.bpm(at: 3840) == 120)
    }

    @Test func inverseIsConsistent() {
        let map = TempoMap(changes: [
            .init(tick: 0, bpm: 72),
            .init(tick: 7680, bpm: 100),
            .init(tick: 15360, bpm: 60),
        ])!
        for tick in stride(from: -3840, through: 30000, by: 977) {
            let seconds = map.seconds(atTick: tick)
            let roundTrip = map.fractionalTick(atSeconds: seconds)
            #expect(abs(roundTrip - Double(tick)) < 1e-6, "tick \(tick)")
        }
    }

    @Test func samplesRounding() {
        let map = TempoMap(bpm: 120)
        #expect(map.samples(atTick: 960, sampleRate: 48000) == 24000)
        #expect(map.samples(atTick: 0, sampleRate: 48000) == 0)
    }

    @Test func noDriftOverTenThousandMeasures() {
        // Seconds at tick N must equal N * secondsPerTick exactly-ish even far out.
        let map = TempoMap(bpm: 97) // awkward bpm
        let ticksPerMeasure = 3840
        let farTick = 10_000 * ticksPerMeasure
        let expected = Double(farTick) * 60.0 / (97.0 * 960.0)
        #expect(abs(map.seconds(atTick: farTick) - expected) < 1e-6)
    }

    @Test func validation() {
        #expect(TempoMap(changes: []) == nil)
        #expect(TempoMap(changes: [.init(tick: 100, bpm: 120)]) == nil)   // must start at 0
        #expect(TempoMap(changes: [.init(tick: 0, bpm: -5)]) == nil)      // positive bpm
        #expect(TempoMap(changes: [
            .init(tick: 0, bpm: 100), .init(tick: 0, bpm: 110),
        ]) == nil)                                                        // strictly increasing
    }

    @Test func scaling() {
        let map = TempoMap(bpm: 100).scaled(by: 1.5)
        #expect(map.baseBPM == 150)
    }
}
