import XCTest
@testable import MixBoard

@MainActor
final class AudioEngineTests: XCTestCase {

    // MARK: - Error Descriptions

    func testFileNotFoundErrorDescription() {
        let error = AudioEngineError.fileNotFound("missing.mp3")
        XCTAssertEqual(error.errorDescription, "Audio file not found: missing.mp3")
    }

    func testFileNotFoundErrorWithEmptyName() {
        let error = AudioEngineError.fileNotFound("")
        XCTAssertNotNil(error.errorDescription)
    }

    // MARK: - Initial State

    func testInitialIsPlayingFalse() {
        let engine = AudioEngine()
        XCTAssertFalse(engine.isPlaying)
    }

    func testInitialCurrentTimeZero() {
        let engine = AudioEngine()
        XCTAssertEqual(engine.currentTime, 0)
    }

    func testInitialDurationZero() {
        let engine = AudioEngine()
        XCTAssertEqual(engine.duration, 0)
    }

    func testDefaultVolume() {
        let engine = AudioEngine()
        XCTAssertEqual(engine.volume, 0.8, accuracy: 0.001)
    }

    func testCurrentTrackNil() {
        let engine = AudioEngine()
        XCTAssertNil(engine.currentTrack)
    }

    // MARK: - Volume

    func testSetVolume() {
        let engine = AudioEngine()
        engine.volume = 0.5
        XCTAssertEqual(engine.volume, 0.5, accuracy: 0.001)
    }

    func testSetVolumeZero() {
        let engine = AudioEngine()
        engine.volume = 0
        XCTAssertEqual(engine.volume, 0, accuracy: 0.001)
    }

    func testSetVolumeMax() {
        let engine = AudioEngine()
        engine.volume = 1.0
        XCTAssertEqual(engine.volume, 1.0, accuracy: 0.001)
    }

    // MARK: - Load Track Error Paths

    func testLoadTrackFileNotFound() {
        let engine = AudioEngine()
        let track = Track(title: "Missing", filePath: "nonexistent/path/missing.mp3")

        XCTAssertThrowsError(try engine.loadTrack(track)) { error in
            guard case AudioEngineError.fileNotFound = error else {
                XCTFail("Expected fileNotFound error, got \(error)")
                return
            }
        }
    }

    func testLoadTrackResetsPlayingState() {
        let engine = AudioEngine()
        // Even a failed load should leave isPlaying = false
        let track = Track(title: "Missing", filePath: "does/not/exist.mp3")
        try? engine.loadTrack(track)
        XCTAssertFalse(engine.isPlaying)
    }

    // MARK: - Stop

    func testStopResetsState() {
        let engine = AudioEngine()
        engine.stop()
        XCTAssertFalse(engine.isPlaying)
        XCTAssertEqual(engine.currentTime, 0)
    }

    // MARK: - Pause Without File

    func testPauseWithoutFile() {
        let engine = AudioEngine()
        engine.pause()
        XCTAssertFalse(engine.isPlaying)
    }

    // MARK: - Toggle Play/Pause Without File

    func testTogglePlayPauseWithNoFile() {
        let engine = AudioEngine()
        // Should not crash
        engine.togglePlayPause()
        XCTAssertFalse(engine.isPlaying)
    }

    // MARK: - EQ

    func testSetEQValidBand() {
        let engine = AudioEngine()
        // Should not crash
        engine.setEQ(band: 0, gain: 5.0)
        engine.setEQ(band: 1, gain: -3.0)
        engine.setEQ(band: 2, gain: 0.0)
    }

    func testSetEQOutOfRangeBand() {
        let engine = AudioEngine()
        // Should not crash — guard protects against out-of-range indices
        engine.setEQ(band: 99, gain: 10.0)
        engine.setEQ(band: -1, gain: 10.0)
        engine.setEQ(band: 3, gain: 10.0)
        engine.setEQ(band: 1000, gain: 10.0)
    }

    // MARK: - Seek Without File

    func testSeekToWithoutFile() {
        let engine = AudioEngine()
        engine.seek(to: 30.0)
        // Should not crash; currentTime should be clamped
        XCTAssertEqual(engine.currentTime, 0, accuracy: 0.01)
    }

    func testSeekByWithoutFile() {
        let engine = AudioEngine()
        engine.seek(by: 10.0)
        XCTAssertEqual(engine.currentTime, 0, accuracy: 0.01)
    }

    // MARK: - Update Current Time

    func testUpdateCurrentTimeNotPlaying() {
        let engine = AudioEngine()
        engine.updateCurrentTime()
        // Should not crash or change time
        XCTAssertEqual(engine.currentTime, 0)
    }

    // MARK: - Playback Callback

    func testOnPlaybackFinishedCallback() {
        let engine = AudioEngine()
        var callbackCalled = false
        engine.onPlaybackFinished = { callbackCalled = true }
        XCTAssertFalse(callbackCalled)
        // The callback should be set but not called until playback actually finishes
        XCTAssertNotNil(engine.onPlaybackFinished)
    }
}
