"""Tests for minihost.audio_io module."""

import tempfile
from pathlib import Path

import numpy as np
import pytest

from minihost.audio_io import get_audio_info, read_audio, resample, write_audio


class TestWriteAndReadRoundTrip:
    """Test write -> read round-trip for various bit depths."""

    def _make_test_signal(self, channels=2, samples=4800, sample_rate=48000):
        """Generate a deterministic test signal."""
        t = np.linspace(0, 1, samples, dtype=np.float32)
        data = np.zeros((channels, samples), dtype=np.float32)
        for ch in range(channels):
            freq = 440.0 * (ch + 1)
            data[ch] = 0.5 * np.sin(2 * np.pi * freq * t)
        return data, sample_rate

    def test_wav_16bit_round_trip(self, tmp_path):
        data, sr = self._make_test_signal()
        path = tmp_path / "test.wav"

        write_audio(path, data, sr, bit_depth=16)
        result, result_sr = read_audio(path)

        assert result_sr == sr
        assert result.shape == data.shape
        # 16-bit has ~1/32768 quantization error; triangle dither adds up to 2 LSBs
        np.testing.assert_allclose(result, data, atol=3.0 / 32768)

    def test_wav_24bit_round_trip(self, tmp_path):
        data, sr = self._make_test_signal()
        path = tmp_path / "test.wav"

        write_audio(path, data, sr, bit_depth=24)
        result, result_sr = read_audio(path)

        assert result_sr == sr
        assert result.shape == data.shape
        # 24-bit has ~1/8388608 quantization error
        np.testing.assert_allclose(result, data, atol=1.0 / 8388608 + 1e-6)

    def test_wav_32bit_float_round_trip(self, tmp_path):
        data, sr = self._make_test_signal()
        path = tmp_path / "test.wav"

        write_audio(path, data, sr, bit_depth=32)
        result, result_sr = read_audio(path)

        assert result_sr == sr
        assert result.shape == data.shape
        np.testing.assert_array_almost_equal(result, data, decimal=6)

    def test_flac_16bit_round_trip(self, tmp_path):
        data, sr = self._make_test_signal()
        path = tmp_path / "test.flac"

        write_audio(path, data, sr, bit_depth=16)
        result, result_sr = read_audio(path)

        assert result_sr == sr
        assert result.shape == data.shape
        # 16-bit has ~1/32768 quantization error; triangle dither adds up to 2 LSBs
        np.testing.assert_allclose(result, data, atol=3.0 / 32768)

    def test_flac_24bit_round_trip(self, tmp_path):
        data, sr = self._make_test_signal()
        path = tmp_path / "test.flac"

        write_audio(path, data, sr, bit_depth=24)
        result, result_sr = read_audio(path)

        assert result_sr == sr
        assert result.shape == data.shape
        # 24-bit has ~1/8388608 quantization error
        np.testing.assert_allclose(result, data, atol=1.0 / 8388608 + 1e-6)


class TestMultiChannel:
    """Test multi-channel audio I/O."""

    def test_mono(self, tmp_path):
        data = np.random.randn(1, 1000).astype(np.float32) * 0.5
        path = tmp_path / "mono.wav"

        write_audio(path, data, 44100, bit_depth=32)
        result, sr = read_audio(path)

        assert sr == 44100
        assert result.shape == (1, 1000)
        np.testing.assert_array_almost_equal(result, data, decimal=6)

    def test_stereo(self, tmp_path):
        data = np.random.randn(2, 1000).astype(np.float32) * 0.5
        path = tmp_path / "stereo.wav"

        write_audio(path, data, 48000, bit_depth=32)
        result, sr = read_audio(path)

        assert result.shape == (2, 1000)

    def test_six_channel(self, tmp_path):
        data = np.random.randn(6, 500).astype(np.float32) * 0.5
        path = tmp_path / "surround.wav"

        write_audio(path, data, 48000, bit_depth=32)
        result, sr = read_audio(path)

        assert result.shape == (6, 500)
        np.testing.assert_array_almost_equal(result, data, decimal=6)

    def test_flac_stereo(self, tmp_path):
        # Use sine waves to stay within [-1, 1]
        t = np.linspace(0, 1, 1000, dtype=np.float32)
        data = np.stack(
            [0.5 * np.sin(2 * np.pi * 440 * t), 0.5 * np.sin(2 * np.pi * 880 * t)]
        )
        path = tmp_path / "stereo.flac"

        write_audio(path, data, 48000, bit_depth=24)
        result, sr = read_audio(path)

        assert sr == 48000
        assert result.shape == (2, 1000)
        np.testing.assert_allclose(result, data, atol=1.0 / 8388608 + 1e-6)

    def test_flac_mono(self, tmp_path):
        t = np.linspace(0, 1, 1000, dtype=np.float32)
        data = (0.5 * np.sin(2 * np.pi * 440 * t)).reshape(1, -1)
        path = tmp_path / "mono.flac"

        write_audio(path, data, 44100, bit_depth=24)
        result, sr = read_audio(path)

        assert sr == 44100
        assert result.shape == (1, 1000)
        np.testing.assert_allclose(result, data, atol=1.0 / 8388608 + 1e-6)


class TestGetAudioInfo:
    """Test get_audio_info metadata extraction."""

    def test_basic_info(self, tmp_path):
        data = np.zeros((2, 48000), dtype=np.float32)
        path = tmp_path / "info_test.wav"

        write_audio(path, data, 48000, bit_depth=24)
        info = get_audio_info(path)

        assert info["channels"] == 2
        assert info["sample_rate"] == 48000
        assert info["frames"] == 48000
        assert abs(info["duration"] - 1.0) < 0.01

    def test_file_not_found(self):
        with pytest.raises(FileNotFoundError):
            get_audio_info("/nonexistent/path/audio.wav")


class TestErrors:
    """Test error handling."""

    def test_read_nonexistent_file(self):
        with pytest.raises(FileNotFoundError):
            read_audio("/nonexistent/file.wav")

    def test_write_unsupported_format(self, tmp_path):
        data = np.zeros((2, 100), dtype=np.float32)
        with pytest.raises(ValueError, match="Unsupported"):
            write_audio(tmp_path / "test.mp3", data, 48000)

    def test_flac_32bit_raises(self, tmp_path):
        data = np.zeros((2, 100), dtype=np.float32)
        with pytest.raises(ValueError, match="32-bit"):
            write_audio(tmp_path / "test.flac", data, 48000, bit_depth=32)

    def test_write_invalid_bit_depth(self, tmp_path):
        data = np.zeros((2, 100), dtype=np.float32)
        with pytest.raises(ValueError, match="bit_depth"):
            write_audio(tmp_path / "test.wav", data, 48000, bit_depth=8)

    def test_default_bit_depth_is_24(self, tmp_path):
        data = np.zeros((2, 100), dtype=np.float32)
        path = tmp_path / "default.wav"
        write_audio(path, data, 48000)
        info = get_audio_info(path)
        # With miniaudio, 24-bit WAV stores as PCM_24
        assert info["channels"] == 2
        assert info["sample_rate"] == 48000
        assert info["frames"] == 100


class TestResample:
    """Test sample rate conversion."""

    def test_upsample_44100_to_48000(self):
        """Upsample 1 second of sine from 44.1k to 48k."""
        sr_in, sr_out = 44100, 48000
        t = np.arange(sr_in, dtype=np.float32) / sr_in
        data = (0.5 * np.sin(2 * np.pi * 440 * t)).reshape(1, -1).astype(np.float32)

        out = resample(data, sr_in, sr_out)
        assert out.shape[0] == 1
        assert out.shape[1] == sr_out
        assert out.dtype == np.float32
        # Peak should be preserved approximately
        assert abs(np.max(np.abs(out)) - 0.5) < 0.05

    def test_downsample_48000_to_44100(self):
        sr_in, sr_out = 48000, 44100
        t = np.arange(sr_in, dtype=np.float32) / sr_in
        data = (0.5 * np.sin(2 * np.pi * 440 * t)).reshape(1, -1).astype(np.float32)

        out = resample(data, sr_in, sr_out)
        assert out.shape[0] == 1
        assert out.shape[1] == sr_out

    def test_same_rate_is_identity(self):
        data = np.random.randn(2, 1000).astype(np.float32) * 0.5
        out = resample(data, 48000, 48000)
        assert out.shape == data.shape
        np.testing.assert_array_equal(out, data)

    def test_stereo(self):
        sr_in, sr_out = 44100, 48000
        data = np.random.randn(2, sr_in).astype(np.float32) * 0.3
        out = resample(data, sr_in, sr_out)
        assert out.shape[0] == 2
        assert out.shape[1] == sr_out

    def test_preserves_silence(self):
        data = np.zeros((1, 44100), dtype=np.float32)
        out = resample(data, 44100, 48000)
        assert np.max(np.abs(out)) == 0.0

    def test_large_ratio(self):
        """Resample from 8000 to 48000 (6x upsample)."""
        data = np.random.randn(1, 8000).astype(np.float32) * 0.3
        out = resample(data, 8000, 48000)
        assert out.shape[0] == 1
        assert out.shape[1] == 48000

    def test_round_trip_preserves_length(self):
        """44100 -> 48000 -> 44100 should produce approximately the same frame count."""
        data = np.random.randn(1, 44100).astype(np.float32) * 0.3
        up = resample(data, 44100, 48000)
        down = resample(up, 48000, 44100)
        # Allow small rounding differences
        assert abs(down.shape[1] - 44100) <= 2
