import Foundation
import AVFoundation
import Testing
import MusicModel
@testable import AudioCore

private func testScore() throws -> Score {
    try Acasong.parse("""
    acasong 1
    title: Mix Test
    time: 4/4
    key: C major
    tempo: 120

    part S
    C5 2 E5 2
    part B
    C3 1
    """)
}

@Suite struct MixdownTests {
    let sr = 48000.0

    @Test func offlineMixEqualsLiveRenderPreMastering() throws {
        // The mixdown path and the "live" path must produce identical audio.
        let score = try testScore()
        var take = [Float](repeating: 0, count: 96_000)
        for i in 0..<take.count { take[i] = sinf(Float(i) * 0.05) * 0.2 }
        let loaded = LoadedTake(
            partName: "S",
            buffer: TakeBuffer(samples: take, sampleRate: sr),
            offsetAtEngineRate: 0
        )

        let mix = try Mixdown.renderRaw(
            score: score, takes: [loaded],
            options: .init(sampleRate: sr)
        )

        // "Live": same program, same takes, rendered directly.
        let program = try ProgramCompiler.compile(score: score, options: .init(
            sampleRate: sr, tonePartNames: ["B"]   // S is recorded → tone for B only
        ))
        let live = OfflineRenderer.render(program: program, takes: [loaded], tailSeconds: 0.5)

        #expect(mix.count == live.samples.count)
        for i in stride(from: 0, to: min(mix.count, live.samples.count), by: 483) {
            if mix[i] != live.samples[i] {
                Issue.record("mix differs from live at \(i)")
                break
            }
        }
    }

    @Test func masteringRespectsCeiling() {
        // Loud input gets limited below -1 dBFS.
        let loud = (0..<48_000).map { i in sinf(Float(i) * 0.1) * 1.8 }
        let mastered = Mixdown.master(loud)
        let ceiling: Float = pow(10, -1.0 / 20.0) + 0.001
        #expect(mastered.allSatisfy { abs($0) <= ceiling })

        // Quiet input gets boosted.
        let quiet = (0..<48_000).map { i in sinf(Float(i) * 0.1) * 0.02 }
        let masteredQuiet = Mixdown.master(quiet)
        let peakBefore = quiet.map(abs).max()!
        let peakAfter = masteredQuiet.map(abs).max()!
        #expect(peakAfter > peakBefore * 2)
    }

    @Test func exportWritesReadableWav() throws {
        let score = try testScore()
        let url = try Mixdown.export(
            score: score, takes: [],
            options: .init(format: .wav, sampleRate: sr),
            suggestedName: "test-mix"
        )
        defer { try? FileManager.default.removeItem(at: url) }

        let file = try AVAudioFile(forReading: url)
        // 1 measure at 120 bpm = 2 s, + 0.5 s tail.
        let expectedFrames = Int(2.5 * sr)
        #expect(abs(Int(file.length) - expectedFrames) < 100)
        #expect(file.fileFormat.sampleRate == sr)
    }

    @Test func exportWritesReadableM4a() throws {
        let score = try testScore()
        let url = try Mixdown.export(
            score: score, takes: [],
            options: .init(format: .m4a, sampleRate: sr),
            suggestedName: "test-mix"
        )
        defer { try? FileManager.default.removeItem(at: url) }
        let file = try AVAudioFile(forReading: url)
        // AAC adds priming/padding; just confirm it decodes to ≥ 2 s.
        #expect(Double(file.length) / file.fileFormat.sampleRate > 2.0)
    }

    @Test func refusesEmptyExport() throws {
        let score = try testScore()
        #expect(throws: (any Error).self) {
            _ = try Mixdown.renderRaw(
                score: score, takes: [],
                options: .init(includeTonesForUnrecordedParts: false, sampleRate: sr)
            )
        }
    }
}
