import Foundation
import Testing
@testable import AudioCore

private func sine(_ frequency: Double, sampleRate: Double = 48000, count: Int = 4096,
                  amplitude: Float = 0.3, harmonics: [Double] = []) -> [Float] {
    (0..<count).map { i in
        let t = Double(i) / sampleRate
        var v = sin(2 * .pi * frequency * t)
        for (k, level) in harmonics.enumerated() {
            v += level * sin(2 * .pi * frequency * Double(k + 2) * t)
        }
        return Float(v) * amplitude
    }
}

@Suite struct PitchDetectorTests {
    @Test func detectsPureTones() {
        for target in [110.0, 220.0, 440.0, 523.25, 880.0] {
            let detected = PitchDetector.detect(samples: sine(target), sampleRate: 48000)
            #expect(detected != nil, "\(target) Hz")
            if let detected {
                let cents = abs(1200 * log2(detected / target))
                #expect(cents < 5, "\(target) Hz off by \(cents) cents")
            }
        }
    }

    @Test func detectsVoiceLikeTone() {
        // Rich harmonic content like a voice — fundamental must win.
        let target = 196.0   // G3
        let samples = sine(target, harmonics: [0.5, 0.33, 0.2, 0.1])
        let detected = PitchDetector.detect(samples: samples, sampleRate: 48000)
        #expect(detected != nil)
        if let detected {
            let cents = abs(1200 * log2(detected / target))
            #expect(cents < 10, "off by \(cents) cents")
        }
    }

    @Test func toleratesNoise() {
        var samples = sine(330)
        var rng = SystemRandomNumberGenerator()
        for i in samples.indices {
            samples[i] += Float.random(in: -0.03...0.03, using: &rng)
        }
        let detected = PitchDetector.detect(samples: samples, sampleRate: 48000)
        #expect(detected != nil)
        if let detected {
            #expect(abs(1200 * log2(detected / 330)) < 15)
        }
    }

    @Test func rejectsSilence() {
        let silence = [Float](repeating: 0, count: 4096)
        #expect(PitchDetector.detect(samples: silence, sampleRate: 48000) == nil)

        let veryQuiet = sine(440, amplitude: 0.001)
        #expect(PitchDetector.detect(samples: veryQuiet, sampleRate: 48000) == nil)
    }

    @Test func centsFoldingIsOctaveAgnostic() {
        // Singing an octave below the target is "in tune".
        #expect(abs(PitchMonitor.centsOff(frequency: 220, target: 440)) < 0.01)
        #expect(abs(PitchMonitor.centsOff(frequency: 880, target: 440)) < 0.01)
        // 20 cents sharp an octave down reads +20.
        let sharp = 220.0 * pow(2.0, 20.0 / 1200.0)
        #expect(abs(PitchMonitor.centsOff(frequency: sharp, target: 440) - 20) < 0.1)
        // A tritone away folds to ±600.
        let tritone = 440.0 * pow(2.0, 0.5)
        #expect(abs(abs(PitchMonitor.centsOff(frequency: tritone, target: 440)) - 600) < 1)
    }

    @Test func monitorStreamsEstimates() async throws {
        let monitor = PitchMonitor()
        monitor.start()
        // Continuous phase across chunks (a repeated chunk would put a
        // discontinuity at every boundary and smear the estimate).
        let signal = sine(440, count: 1024 * 6)
        for chunk in stride(from: 0, to: signal.count, by: 1024) {
            monitor.feed(Array(signal[chunk..<(chunk + 1024)]), sampleRate: 48000)
        }
        // Detection is async on a utility queue; poll briefly.
        var freq: Double?
        for _ in 0..<50 {
            try await Task.sleep(for: .milliseconds(10))
            if let f = monitor.currentFrequency { freq = f; break }
        }
        #expect(freq != nil)
        if let freq {
            #expect(abs(1200 * log2(freq / 440)) < 10)
        }
        monitor.stop()
        #expect(monitor.currentFrequency == nil)
    }
}
