"""Tests for PartialAudioBuffer class."""

from __future__ import annotations

import numpy as np

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio.partial_buffer import PartialAudioBuffer

# Audio constants for test assertions
SAMPLE_RATE_16K = DEFAULT_SAMPLE_RATE
SAMPLE_RATE_48K = 48000
CHUNK_100MS = 1600  # 100ms at 16kHz
CHUNK_50MS = 800  # 50ms at 16kHz
ONE_SECOND_16K = DEFAULT_SAMPLE_RATE  # 1 second at 16kHz


class TestPartialAudioBufferInit:
    """Tests for buffer initialization."""

    def test_default_capacity(self) -> None:
        """Buffer should have 5 seconds capacity by default at 16kHz."""
        buffer = PartialAudioBuffer()
        assert buffer.capacity_samples == 5 * SAMPLE_RATE_16K, "Default capacity should be 5 seconds"

    def test_custom_duration(self) -> None:
        """Buffer should respect custom max duration."""
        buffer = PartialAudioBuffer(max_duration_seconds=3.0, sample_rate=SAMPLE_RATE_16K)
        assert buffer.capacity_samples == 3 * SAMPLE_RATE_16K, "Capacity should match custom duration"

    def test_custom_sample_rate(self) -> None:
        """Buffer should respect custom sample rate."""
        buffer = PartialAudioBuffer(max_duration_seconds=1.0, sample_rate=SAMPLE_RATE_48K)
        assert buffer.capacity_samples == SAMPLE_RATE_48K, "Capacity should match sample rate"
        assert buffer.sample_rate == SAMPLE_RATE_48K, "Sample rate property should match"

    def test_starts_empty(self) -> None:
        """Buffer should start empty."""
        buffer = PartialAudioBuffer()
        assert buffer.is_empty, "New buffer should be empty"
        assert buffer.samples_buffered == 0, "No samples should be buffered"
        assert buffer.duration_seconds == 0.0, "Duration should be zero"


class TestPartialAudioBufferAppend:
    """Tests for buffer append operations."""

    def test_append_single_chunk(self) -> None:
        """Appending a chunk should increase buffer size."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        chunk = np.ones(CHUNK_100MS, dtype=np.float32)

        result = buffer.append(chunk)

        assert result is True, "Append should succeed"
        assert buffer.samples_buffered == CHUNK_100MS, "Should have 1600 samples"
        assert not buffer.is_empty, "Buffer should not be empty"

    def test_append_multiple_chunks(self) -> None:
        """Appending multiple chunks should accumulate."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        chunk = np.ones(CHUNK_100MS, dtype=np.float32)

        for _ in range(10):
            buffer.append(chunk)

        assert buffer.samples_buffered == ONE_SECOND_16K, "Should have 10 chunks"
        assert buffer.duration_seconds == 1.0, "Should be 1 second of audio"

    def test_append_copies_data(self) -> None:
        """Append should copy data, not hold reference."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        chunk = np.ones(CHUNK_100MS, dtype=np.float32) * 0.5

        buffer.append(chunk)

        # Modify original - should not affect buffer
        chunk[:] = 0.0

        retrieved = buffer.get_audio()
        assert np.allclose(retrieved, 0.5), "Buffer should have copied data"

    def test_append_overflow_returns_false(self) -> None:
        """Append should return False on overflow."""
        buffer = PartialAudioBuffer(max_duration_seconds=0.1, sample_rate=SAMPLE_RATE_16K)
        # 0.1s = 1600 samples, try to add 2000
        chunk = np.ones(2000, dtype=np.float32)

        result = buffer.append(chunk)

        assert result is False, "Overflow should return False"
        assert buffer.is_empty, "Buffer should remain empty on overflow"


class TestPartialAudioBufferGetAudio:
    """Tests for audio retrieval."""

    def test_get_audio_empty_buffer(self) -> None:
        """Getting audio from empty buffer should return empty array."""
        buffer = PartialAudioBuffer()

        audio = buffer.get_audio()

        assert len(audio) == 0, "Empty buffer should return empty array"
        assert audio.dtype == np.float32, "Should be float32"

    def test_get_audio_returns_copy(self) -> None:
        """get_audio should return a copy, not a view."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(CHUNK_100MS, dtype=np.float32))

        audio1 = buffer.get_audio()
        audio2 = buffer.get_audio()

        # Modify first copy
        audio1[:] = 0.0

        # Second copy should be unaffected
        assert np.allclose(audio2, 1.0), "Second copy should be independent"

    def test_get_audio_view_is_view(self) -> None:
        """get_audio_view should return a view into buffer."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(CHUNK_100MS, dtype=np.float32))

        view = buffer.get_audio_view()

        # Verify it's the same data
        assert len(view) == CHUNK_100MS, "View should have correct length"
        assert np.allclose(view, 1.0), "View should have correct values"

    def test_get_audio_preserves_values(self) -> None:
        """Retrieved audio should match appended audio."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        chunk1 = np.full(CHUNK_100MS, 0.5, dtype=np.float32)
        chunk2 = np.full(CHUNK_100MS, -0.5, dtype=np.float32)

        buffer.append(chunk1)
        buffer.append(chunk2)

        audio = buffer.get_audio()

        assert np.allclose(audio[:CHUNK_100MS], 0.5), "First chunk should match"
        assert np.allclose(audio[CHUNK_100MS:], -0.5), "Second chunk should match"


class TestPartialAudioBufferClear:
    """Tests for buffer clearing."""

    def test_clear_empties_buffer(self) -> None:
        """Clear should empty the buffer."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(CHUNK_100MS, dtype=np.float32))

        buffer.clear()

        assert buffer.is_empty, "Buffer should be empty after clear"
        assert buffer.samples_buffered == 0, "Sample count should be zero"

    def test_clear_allows_reuse(self) -> None:
        """Buffer should be reusable after clear."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(CHUNK_100MS, dtype=np.float32))
        buffer.clear()

        # Should be able to append again
        buffer.append(np.full(CHUNK_50MS, 0.5, dtype=np.float32))

        assert buffer.samples_buffered == CHUNK_50MS, "Should have new samples"
        audio = buffer.get_audio()
        assert np.allclose(audio, 0.5), "New data should be correct"

    def test_clear_is_idempotent(self) -> None:
        """Multiple clears should be safe."""
        buffer = PartialAudioBuffer()

        buffer.clear()
        buffer.clear()
        buffer.clear()

        assert buffer.is_empty, "Buffer should be empty"


class TestPartialAudioBufferDunderMethods:
    """Tests for special methods (__len__, __bool__)."""

    def test_len_empty(self) -> None:
        """len() on empty buffer should return 0."""
        buffer = PartialAudioBuffer()
        assert len(buffer) == 0, "Empty buffer should have len 0"

    def test_len_with_samples(self) -> None:
        """len() should return sample count."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(CHUNK_100MS, dtype=np.float32))
        assert len(buffer) == CHUNK_100MS, "len should match sample count"

    def test_bool_empty(self) -> None:
        """Empty buffer should be falsy."""
        buffer = PartialAudioBuffer()
        assert not buffer, "Empty buffer should be falsy"

    def test_bool_with_samples(self) -> None:
        """Buffer with samples should be truthy."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(100, dtype=np.float32))
        assert buffer, "Non-empty buffer should be truthy"


class TestPartialAudioBufferDuration:
    """Tests for duration calculation."""

    def test_duration_calculation(self) -> None:
        """Duration should be calculated from samples and sample rate."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_16K)
        buffer.append(np.ones(8000, dtype=np.float32))

        assert buffer.duration_seconds == 0.5, "8000 samples at 16kHz = 0.5 seconds"

    def test_duration_different_sample_rate(self) -> None:
        """Duration should respect sample rate."""
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE_48K)
        buffer.append(np.ones(48000, dtype=np.float32))

        assert buffer.duration_seconds == 1.0, "48000 samples at 48kHz = 1 second"
