"""Tests for the audio waveform extraction API."""

import json
import shutil
import struct
import subprocess
import time
from pathlib import Path
from unittest.mock import patch

import numpy as np
import pytest
from fastapi.testclient import TestClient

from sltk.api.main import app

client = TestClient(app)

_has_ffmpeg = shutil.which("ffmpeg") is not None


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def test_video(tmp_path):
    """Create a tiny test video with audio via ffmpeg.

    Generates a 1-second video with a 440Hz sine tone.
    Uses mpeg4 encoder for broad ffmpeg compatibility (no libx264 needed).
    """
    if not _has_ffmpeg:
        pytest.skip("ffmpeg not available")
    video_path = tmp_path / "test_video.mp4"
    cmd = [
        "ffmpeg",
        "-y",
        "-f",
        "lavfi",
        "-i",
        "sine=frequency=440:duration=1",
        "-f",
        "lavfi",
        "-i",
        "color=c=black:s=64x64:d=1:r=25",
        "-c:v",
        "mpeg4",
        "-c:a",
        "aac",
        "-shortest",
        "-v",
        "error",
        str(video_path),
    ]
    result = subprocess.run(cmd, capture_output=True, timeout=30)
    if result.returncode != 0:
        pytest.skip(f"ffmpeg failed to create test video: {result.stderr.decode()[:200]}")
    return video_path


@pytest.fixture
def test_video_no_audio(tmp_path):
    """Create a tiny test video without any audio track."""
    if not _has_ffmpeg:
        pytest.skip("ffmpeg not available")
    video_path = tmp_path / "no_audio.mp4"
    cmd = [
        "ffmpeg",
        "-y",
        "-f",
        "lavfi",
        "-i",
        "color=c=black:s=64x64:d=1:r=25",
        "-c:v",
        "mpeg4",
        "-an",
        "-v",
        "error",
        str(video_path),
    ]
    result = subprocess.run(cmd, capture_output=True, timeout=30)
    if result.returncode != 0:
        pytest.skip("ffmpeg failed to create no-audio test video")
    return video_path


# ============================================================================
# Basic endpoint tests
# ============================================================================


class TestWaveformEndpoint:
    """Tests for GET /api/audio/waveform."""

    def test_waveform_basic(self, test_video):
        """Waveform extraction returns correct format."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "peaks" in data
        assert "duration" in data
        assert "sample_rate" in data
        assert "num_samples" in data
        assert data["sample_rate"] == 16000
        assert data["num_samples"] == 100
        assert len(data["peaks"]) == 100
        assert data["duration"] > 0

    def test_waveform_peaks_format(self, test_video):
        """Each peak is a [min, max] pair with min <= max."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 200},
        )
        assert resp.status_code == 200
        data = resp.json()
        for peak in data["peaks"]:
            assert isinstance(peak, list)
            assert len(peak) == 2
            assert peak[0] <= peak[1], f"min > max: {peak}"

    def test_waveform_sine_has_nonzero_peaks(self, test_video):
        """A sine tone should produce peaks that are not all zero."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp.status_code == 200
        data = resp.json()
        # At least some peaks should be non-zero for a 440Hz tone
        max_vals = [p[1] for p in data["peaks"]]
        assert any(v > 0 for v in max_vals), "Expected non-zero peaks for sine audio"

    def test_waveform_custom_samples(self, test_video):
        """Custom sample counts are respected."""
        for count in [100, 500, 2000]:
            resp = client.get(
                "/api/audio/waveform",
                params={"path": str(test_video), "samples": count},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["num_samples"] == count
            assert len(data["peaks"]) == count

    def test_waveform_default_samples(self, test_video):
        """Default sample count is 8000."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video)},
        )
        assert resp.status_code == 200
        data = resp.json()
        # For a 1s video at 16kHz = 16000 samples, 8000 peaks is fine
        assert data["num_samples"] == 8000


# ============================================================================
# Caching tests
# ============================================================================


class TestWaveformCaching:
    """Tests for waveform cache behavior."""

    def test_caching_creates_file(self, test_video):
        """First request creates a cache file."""
        cache_path = test_video.parent / f".{test_video.stem}.waveform.json"
        assert not cache_path.exists()

        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp.status_code == 200
        assert cache_path.exists()

        # Verify cache content is valid JSON
        with open(cache_path) as f:
            cached = json.load(f)
        assert cached["num_samples"] == 100
        assert len(cached["peaks"]) == 100

    def test_cache_hit_returns_same_data(self, test_video):
        """Second request with same samples returns cached data."""
        resp1 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        resp2 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp1.json() == resp2.json()

    def test_cache_miss_on_different_samples(self, test_video):
        """Cache miss when requesting different sample count."""
        resp1 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp1.status_code == 200

        resp2 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 200},
        )
        assert resp2.status_code == 200
        assert resp2.json()["num_samples"] == 200

    def test_cache_invalidation_on_video_change(self, test_video):
        """Cache is invalidated when video is newer than cache."""
        # First request to populate cache
        resp1 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp1.status_code == 200

        cache_path = test_video.parent / f".{test_video.stem}.waveform.json"
        assert cache_path.exists()

        # Make cache older than video by backdating it
        old_time = time.time() - 1000
        import os

        os.utime(cache_path, (old_time, old_time))

        # Touch the video to make it newer
        test_video.touch()

        # Should re-extract (cache mtime < video mtime)
        resp2 = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100},
        )
        assert resp2.status_code == 200


# ============================================================================
# Error handling tests
# ============================================================================


class TestWaveformErrors:
    """Tests for error responses."""

    def test_missing_path_param(self):
        """Missing path parameter returns 422."""
        resp = client.get("/api/audio/waveform")
        assert resp.status_code == 422

    def test_nonexistent_file(self, tmp_path):
        """Non-existent file returns 404."""
        fake = tmp_path / "nonexistent.mp4"
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(fake)},
        )
        assert resp.status_code == 404

    def test_non_video_extension(self, tmp_path):
        """Non-video file extension returns 400."""
        txt_file = tmp_path / "notes.txt"
        txt_file.write_text("not a video")
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(txt_file)},
        )
        assert resp.status_code == 400

    def test_path_traversal_blocked(self, tmp_path):
        """Path traversal attempts are blocked."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(tmp_path / ".." / ".." / "etc" / "passwd")},
        )
        assert resp.status_code in (400, 403, 404)

    def test_samples_below_minimum(self, test_video):
        """Samples below 100 returns 422."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 50},
        )
        assert resp.status_code == 422

    def test_samples_above_maximum(self, test_video):
        """Samples above 50000 returns 422."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video), "samples": 100000},
        )
        assert resp.status_code == 422

    def test_video_no_audio_track(self, test_video_no_audio):
        """Video without audio track returns 400."""
        resp = client.get(
            "/api/audio/waveform",
            params={"path": str(test_video_no_audio), "samples": 100},
        )
        assert resp.status_code == 400


# ============================================================================
# Peak computation unit tests
# ============================================================================


class TestComputePeaks:
    """Unit tests for _compute_peaks function."""

    def test_simple_peaks(self):
        """Known input produces expected peak values."""
        from sltk.api.routers.audio import _compute_peaks

        # 8 samples -> 2 peaks (window=4)
        samples = np.array([-1.0, 0.5, 0.0, 0.8, -0.3, 0.2, -0.9, 1.0], dtype=np.float32)
        raw = samples.tobytes()
        peaks = _compute_peaks(raw, 2)
        assert len(peaks) == 2
        # First window: [-1.0, 0.5, 0.0, 0.8] -> min=-1.0, max=0.8
        assert peaks[0][0] == pytest.approx(-1.0, abs=1e-4)
        assert peaks[0][1] == pytest.approx(0.8, abs=1e-4)
        # Second window: [-0.3, 0.2, -0.9, 1.0] -> min=-0.9, max=1.0
        assert peaks[1][0] == pytest.approx(-0.9, abs=1e-4)
        assert peaks[1][1] == pytest.approx(1.0, abs=1e-4)

    def test_empty_bytes(self):
        """Empty input returns empty peaks."""
        from sltk.api.routers.audio import _compute_peaks

        peaks = _compute_peaks(b"", 100)
        assert peaks == []

    def test_more_peaks_than_samples(self):
        """Requesting more peaks than samples clamps to sample count."""
        from sltk.api.routers.audio import _compute_peaks

        samples = np.array([0.5, -0.5], dtype=np.float32)
        peaks = _compute_peaks(samples.tobytes(), 1000)
        # Should clamp to 2 peaks (one per sample)
        assert len(peaks) == 2

    def test_single_peak(self):
        """Single peak covers all samples."""
        from sltk.api.routers.audio import _compute_peaks

        samples = np.array([0.1, -0.2, 0.3, -0.4], dtype=np.float32)
        peaks = _compute_peaks(samples.tobytes(), 1)
        assert len(peaks) == 1
        assert peaks[0][0] == pytest.approx(-0.4, abs=1e-4)
        assert peaks[0][1] == pytest.approx(0.3, abs=1e-4)

    def test_silence_produces_zero_peaks(self):
        """All-zero audio produces zero peaks."""
        from sltk.api.routers.audio import _compute_peaks

        samples = np.zeros(1000, dtype=np.float32)
        peaks = _compute_peaks(samples.tobytes(), 10)
        assert all(p == [0.0, 0.0] for p in peaks)
