import XCTest
import AVFoundation
@testable import ExpoAudioStream

/// Tests for the new native AAC direct-to-file encoder (`AACFileWriter`).
///
/// The encoder writes ADTS-framed AAC-LC to disk, entirely native-side. The
/// tests below verify, in order of increasing scope:
///
///   1. The pure ADTS header helper produces the expected sync word and
///      bit layout.
///   2. The writer actually produces a file that begins with the ADTS sync
///      word and whose frames are decodable with `AVAudioFile`.
///   3. `RecordingSettings.fromDictionary` rejects invalid compression
///      configs (bad sample rate / bitrate / container / format).
///   4. The legacy `output.compressed` path is disabled with a warning when
///      the new top-level `compression` is also set.
class AACFileWriterTests: XCTestCase {

    // MARK: - ADTS header helper

    func testADTSHeaderStartsWithSyncWord() {
        let header = AACFileWriter.makeADTSHeader(
            packetLength: 256,
            sampleRateIndex: 8,      // 16 kHz
            channelConfig: 1
        )
        XCTAssertEqual(header.count, 7)
        XCTAssertEqual(header[0], 0xFF)
        XCTAssertEqual(header[1], 0xF9) // MPEG-2, no CRC
    }

    func testADTSHeaderEncodesFrameLengthCorrectly() {
        // frame_length = packet_length + 7 = 263; occupies 13 bits across
        // byte[3] low-2-bits, byte[4] full, byte[5] high-3-bits.
        let header = AACFileWriter.makeADTSHeader(
            packetLength: 256,
            sampleRateIndex: 8,
            channelConfig: 1
        )
        let frameLength = (Int(header[3] & 0x03) << 11)
                        | (Int(header[4]) << 3)
                        | (Int(header[5] & 0xE0) >> 5)
        XCTAssertEqual(frameLength, 263)
    }

    func testAdtsSampleRateTableContainsCommonRates() {
        XCTAssertEqual(AACFileWriter.adtsSampleRateIndex(for: 16_000), 8)
        XCTAssertEqual(AACFileWriter.adtsSampleRateIndex(for: 44_100), 4)
        XCTAssertEqual(AACFileWriter.adtsSampleRateIndex(for: 48_000), 3)
        XCTAssertNil(AACFileWriter.adtsSampleRateIndex(for: 22_000))
        XCTAssertNil(AACFileWriter.adtsSampleRateIndex(for: 100))
    }

    // MARK: - End-to-end encode + decode

    /// Feeds ~5 s of 440 Hz sine @ 16 kHz mono to the writer and verifies:
    ///   - a non-empty file is produced,
    ///   - the first two bytes are the ADTS sync word (0xFF 0xF9),
    ///   - the file is decodable by AVAudioFile and yields ~80 000 samples
    ///     (5 s * 16 kHz) within a tolerance of two AAC frames (2 * 1024).
    func testEncodesSineToDecodableAdtsFile() throws {
        let writer = AACFileWriter()
        let url = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("aac_writer_test_\(UUID().uuidString).aac")

        try writer.start(sampleRate: 16_000, channels: 1, bitrate: 32_000, outputURL: url)

        // 5 seconds of tone, split into 100 ms buffers so we exercise the
        // streaming path rather than a single one-shot encode.
        let chunkDuration = 0.1
        let totalDuration = 5.0
        let chunkCount = Int(totalDuration / chunkDuration)
        for _ in 0..<chunkCount {
            guard let buffer = TestAudioGenerator.generateTone(
                frequency: 440,
                duration: chunkDuration,
                sampleRate: 16_000
            ) else {
                XCTFail("Failed to synthesise test tone")
                return
            }
            writer.encode(buffer)
        }

        let finalURL = writer.stop()
        XCTAssertEqual(finalURL, url)

        let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
        let size = (attributes[.size] as? Int64) ?? 0
        XCTAssertGreaterThan(size, 0, "ADTS file should not be empty")

        // First two bytes = ADTS sync word
        let handle = try FileHandle(forReadingFrom: url)
        defer { try? handle.close() }
        let prefix = try handle.read(upToCount: 2) ?? Data()
        XCTAssertEqual(prefix.count, 2)
        XCTAssertEqual(prefix[0], 0xFF)
        XCTAssertEqual(prefix[1], 0xF9)

        // Decode via AVAudioFile — if AAC framing is valid, this succeeds
        // and reports a frame count close to what we fed in.
        let decoded = try AVAudioFile(forReading: url)
        let expectedFrames: AVAudioFramePosition = 16_000 * 5
        let tolerance: AVAudioFramePosition = 2 * 1024 // two AAC packets
        XCTAssertGreaterThan(decoded.length, expectedFrames - tolerance,
                             "Decoded length \(decoded.length) too short vs expected \(expectedFrames)")
        XCTAssertLessThan(decoded.length, expectedFrames + tolerance,
                          "Decoded length \(decoded.length) too long vs expected \(expectedFrames)")

        // Clean up
        try? FileManager.default.removeItem(at: url)
    }

    func testSynchronizeAllowsEncodingToResumeInSameAdtsFile() throws {
        let writer = AACFileWriter()
        let url = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("aac_writer_resume_test_\(UUID().uuidString).aac")

        try writer.start(sampleRate: 16_000, channels: 1, bitrate: 32_000, outputURL: url)

        let chunkDuration = 0.1
        let totalDuration = 4.0
        let chunksPerHalf = Int((totalDuration / 2) / chunkDuration)

        for _ in 0..<chunksPerHalf {
            guard let buffer = TestAudioGenerator.generateTone(
                frequency: 440,
                duration: chunkDuration,
                sampleRate: 16_000
            ) else {
                XCTFail("Failed to synthesise first-half test tone")
                return
            }
            writer.encode(buffer)
        }

        writer.synchronize()

        for _ in 0..<chunksPerHalf {
            guard let buffer = TestAudioGenerator.generateTone(
                frequency: 660,
                duration: chunkDuration,
                sampleRate: 16_000
            ) else {
                XCTFail("Failed to synthesise second-half test tone")
                return
            }
            writer.encode(buffer)
        }

        let finalURL = writer.stop()
        XCTAssertEqual(finalURL, url)

        let decoded = try AVAudioFile(forReading: url)
        let expectedFrames: AVAudioFramePosition = 16_000 * 4
        let tolerance: AVAudioFramePosition = 2 * 1024
        XCTAssertGreaterThan(decoded.length, expectedFrames - tolerance,
                             "Decoded length \(decoded.length) too short vs expected \(expectedFrames)")
        XCTAssertLessThan(decoded.length, expectedFrames + tolerance,
                          "Decoded length \(decoded.length) too long vs expected \(expectedFrames)")

        try? FileManager.default.removeItem(at: url)
    }

    func testStartFailsOnUnsupportedSampleRate() {
        let writer = AACFileWriter()
        let url = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("aac_writer_bad_rate_\(UUID().uuidString).aac")
        XCTAssertThrowsError(
            try writer.start(sampleRate: 22_000, channels: 1, bitrate: 32_000, outputURL: url)
        ) { error in
            if case AACFileWriter.WriterError.invalidSampleRate(let rate) = error {
                XCTAssertEqual(rate, 22_000)
            } else {
                XCTFail("Expected invalidSampleRate, got \(error)")
            }
        }
    }

    // MARK: - RecordingSettings parsing

    func testFromDictionaryAcceptsValidCompression() {
        let dict: [String: Any] = [
            "sampleRate": 16_000.0,
            "autoResumeAfterInterruption": false,
            "channels": 1,
            "compression": [
                "format": "aac",
                "bitrate": 32_000,
                "container": "adts"
            ]
        ]
        switch RecordingSettings.fromDictionary(dict) {
        case .success(let s):
            XCTAssertNotNil(s.compression)
            XCTAssertEqual(s.compression?.format, "aac")
            XCTAssertEqual(s.compression?.bitrate, 32_000)
            XCTAssertEqual(s.compression?.container, "adts")
        case .failure(let err):
            XCTFail("Expected success, got \(err)")
        }
    }

    func testFromDictionaryRejectsUnsupportedContainer() {
        let dict: [String: Any] = [
            "sampleRate": 16_000.0,
            "autoResumeAfterInterruption": false,
            "compression": [
                "format": "aac",
                "bitrate": 32_000,
                "container": "m4a"
            ]
        ]
        switch RecordingSettings.fromDictionary(dict) {
        case .success:
            XCTFail("Expected failure for unsupported container")
        case .failure(let err):
            XCTAssertTrue(err is RecordingError)
        }
    }

    func testFromDictionaryRejectsInvalidBitrate() {
        let dict: [String: Any] = [
            "sampleRate": 16_000.0,
            "autoResumeAfterInterruption": false,
            "compression": [
                "format": "aac",
                "bitrate": 8_000,      // below min
                "container": "adts"
            ]
        ]
        switch RecordingSettings.fromDictionary(dict) {
        case .success:
            XCTFail("Expected failure for invalid bitrate")
        case .failure(let err):
            XCTAssertTrue(err is RecordingError)
        }
    }

    func testFromDictionaryRejectsNonAdtsSampleRate() {
        let dict: [String: Any] = [
            "sampleRate": 22_000.0, // not in ADTS table
            "autoResumeAfterInterruption": false,
            "compression": [
                "format": "aac",
                "bitrate": 32_000,
                "container": "adts"
            ]
        ]
        switch RecordingSettings.fromDictionary(dict) {
        case .success:
            XCTFail("Expected failure for non-ADTS sample rate")
        case .failure(let err):
            XCTAssertTrue(err is RecordingError)
        }
    }

    /// When both the new `compression` and legacy `output.compressed` are
    /// set, the new path wins and the legacy is disabled with a warning.
    func testFromDictionaryDisablesLegacyCompressedWhenNewCompressionSet() {
        let dict: [String: Any] = [
            "sampleRate": 16_000.0,
            "autoResumeAfterInterruption": false,
            "output": [
                "compressed": [
                    "enabled": true,
                    "format": "aac",
                    "bitrate": 128_000
                ]
            ],
            "compression": [
                "format": "aac",
                "bitrate": 32_000,
                "container": "adts"
            ]
        ]
        switch RecordingSettings.fromDictionary(dict) {
        case .success(let s):
            XCTAssertNotNil(s.compression)
            XCTAssertFalse(s.output.compressed.enabled,
                           "Legacy output.compressed must be disabled when new compression is set")
        case .failure(let err):
            XCTFail("Expected success, got \(err)")
        }
    }
}
