"""Tests for the eval metrics module."""
from __future__ import annotations

import numpy as np
import pytest


class TestKinematics:
    def test_compute_speed_constant_velocity(self):
        """Constant-velocity trajectory has constant speed."""
        from tools.eval.metrics import compute_speed
        # 10 points, moving 10 px in 100 ms each step → speed = 0.1 px/ms
        xs = np.arange(0, 100, 10, dtype=float)
        ys = np.zeros(10, dtype=float)
        ts = np.arange(0, 1000, 100, dtype=float)
        v = compute_speed(xs, ys, ts)
        # All speeds should be ≈ 0.1 px/ms
        assert v.shape == (9,)  # n-1 differences
        np.testing.assert_allclose(v, 0.1, rtol=1e-4)

    def test_compute_speed_handles_zero_dt(self):
        """Adjacent points with same timestamp must not produce NaN/inf."""
        from tools.eval.metrics import compute_speed
        xs = np.array([0.0, 10.0, 20.0])
        ys = np.array([0.0, 0.0, 0.0])
        ts = np.array([0.0, 0.0, 100.0])  # zero dt between [0] and [1]
        v = compute_speed(xs, ys, ts)
        assert np.isfinite(v).all()

    def test_compute_acceleration(self):
        """Linearly increasing speed → constant acceleration."""
        from tools.eval.metrics import compute_acceleration
        # speeds: 0.1, 0.2, 0.3, 0.4 over dt = 100 ms each → a = 0.001 px/ms²
        speeds = np.array([0.1, 0.2, 0.3, 0.4])
        ts = np.array([100.0, 200.0, 300.0, 400.0])
        a = compute_acceleration(speeds, ts)
        np.testing.assert_allclose(a, 0.001, rtol=1e-4)

    def test_compute_jerk(self):
        from tools.eval.metrics import compute_jerk
        # accelerations: 0.001, 0.002, 0.003 over dt = 100 ms → j = 0.00001
        accels = np.array([0.001, 0.002, 0.003])
        ts = np.array([200.0, 300.0, 400.0])
        j = compute_jerk(accels, ts)
        np.testing.assert_allclose(j, 1e-5, rtol=1e-4)


class TestStatsSummary:
    def test_compute_stats_returns_expected_keys(self):
        from tools.eval.metrics import compute_stats
        x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
        s = compute_stats(x)
        assert "mean" in s
        assert "std" in s
        assert "cv" in s
        assert "p25" in s
        assert "p50" in s
        assert "p75" in s
        assert "p95" in s

    def test_cv_for_constant_is_zero(self):
        from tools.eval.metrics import compute_stats
        x = np.full(10, 3.0)
        s = compute_stats(x)
        assert s["cv"] == 0.0


class TestFftSpectrum:
    def test_finds_dominant_frequency(self):
        """A pure 8 Hz signal should have its peak near 8 Hz."""
        from tools.eval.metrics import fft_spectrum
        # Sample at 100 Hz for 1 second
        sample_rate_hz = 100.0
        ts_ms = np.arange(0, 1000, 1000 / sample_rate_hz)
        signal = np.sin(2 * np.pi * 8 * ts_ms / 1000)  # 8 Hz sine
        freqs, mags = fft_spectrum(signal, sample_rate_hz)
        peak_freq = freqs[np.argmax(mags)]
        assert abs(peak_freq - 8.0) < 1.0  # within 1 Hz

    def test_returns_only_positive_frequencies(self):
        from tools.eval.metrics import fft_spectrum
        signal = np.random.randn(64)
        freqs, mags = fft_spectrum(signal, 50.0)
        assert (freqs >= 0).all()
        assert len(freqs) == len(mags)


class TestKlDivergence:
    def test_identical_distributions_zero_kl(self):
        """KL(p, p) ≈ 0."""
        from tools.eval.metrics import kl_divergence_histograms
        rng = np.random.default_rng(42)
        x = rng.normal(0, 1, 5000)
        y = rng.normal(0, 1, 5000)
        kl = kl_divergence_histograms(x, y, bins=50)
        assert kl < 0.05

    def test_different_distributions_positive_kl(self):
        """Different means → positive KL."""
        from tools.eval.metrics import kl_divergence_histograms
        rng = np.random.default_rng(42)
        x = rng.normal(0, 1, 5000)
        y = rng.normal(3, 1, 5000)
        kl = kl_divergence_histograms(x, y, bins=50)
        assert kl > 0.5

    def test_handles_disjoint_supports(self):
        """No NaN even when histograms have non-overlapping bins."""
        from tools.eval.metrics import kl_divergence_histograms
        x = np.array([1.0, 1.1, 1.2, 1.3, 1.4])
        y = np.array([10.0, 10.1, 10.2, 10.3, 10.4])
        kl = kl_divergence_histograms(x, y, bins=10)
        assert np.isfinite(kl)


class TestReportGeneration:
    def test_generates_report_md(self, tmp_path):
        """Smoke test: build_report writes an MD file with all expected sections."""
        from tools.eval.report import build_report

        # Synthetic generated traces (3 traces, 50 points each)
        rng = np.random.default_rng(0)
        gen_traces = []
        for _ in range(3):
            xs = np.cumsum(rng.uniform(0, 5, 50))
            ys = np.cumsum(rng.uniform(-1, 1, 50))
            ts = np.cumsum(rng.uniform(5, 20, 50))
            gen_traces.append({"xs": xs, "ys": ys, "ts": ts})

        # Synthetic reference
        ref_traces = []
        for _ in range(5):
            xs = np.cumsum(rng.uniform(0, 5, 50))
            ys = np.cumsum(rng.uniform(-1, 1, 50))
            ts = np.cumsum(rng.uniform(5, 20, 50))
            ref_traces.append({"xs": xs, "ys": ys, "ts": ts})

        out_md = tmp_path / "report.md"
        build_report(
            generated_traces=gen_traces,
            reference_traces=ref_traces,
            output_md=out_md,
            tag="smoke-test",
            model_dir="/fake/model/dir",
        )
        assert out_md.exists()
        content = out_md.read_text(encoding="utf-8")
        assert "# Eval Report" in content
        assert "smoke-test" in content
        assert "速度" in content or "speed" in content.lower()
        assert "FFT" in content.upper()
        # PNG plots should exist next to MD
        plot_dir = tmp_path / "plots"
        assert plot_dir.exists()
        assert any(plot_dir.iterdir())
