"""Tests for Flow Matching trajectory generator."""
from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import pytest
import torch

from ai_mouse.generator import generate
from tools.models import TrajectoryFlowModel


@pytest.fixture
def model_dir(tmp_path):
    """Create temp dir with Flow model artifacts."""
    model = TrajectoryFlowModel(seq_len=64)
    torch.save(model.state_dict(), tmp_path / "flow_model.pt")

    click_dist = {"mu": 80.0, "sigma": 30.0, "low": 20.0, "high": 300.0}
    (tmp_path / "click_dist.json").write_text(json.dumps(click_dist))

    duration_dist = {
        "bins": [0, 50, 100, 200, 400, 600, 800, 1200, float("inf")],
        "params": [
            {"mu_log": 5.5, "sigma_log": 0.3},
            {"mu_log": 5.8, "sigma_log": 0.3},
            {"mu_log": 6.0, "sigma_log": 0.3},
            {"mu_log": 6.2, "sigma_log": 0.3},
            {"mu_log": 6.5, "sigma_log": 0.3},
            {"mu_log": 6.7, "sigma_log": 0.3},
            {"mu_log": 6.9, "sigma_log": 0.3},
            {"mu_log": 7.0, "sigma_log": 0.3},
        ],
    }
    (tmp_path / "duration_dist.json").write_text(json.dumps(duration_dist))

    train_config = {
        "seq_len": 64,
        "d_model": 128,
        "nhead": 4,
        "num_layers": 4,
        "dim_feedforward": 256,
        "cond_dim": 3,
    }
    (tmp_path / "train_config.json").write_text(json.dumps(train_config))

    return tmp_path


class TestGenerate:
    def test_returns_list_of_tuples(self, model_dir):
        result = generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
        assert isinstance(result, list)
        assert all(isinstance(p, tuple) and len(p) == 3 for p in result)
        # All elements are ints
        for p in result:
            assert all(isinstance(v, int) for v in p)

    def test_timestamps_monotonically_increasing(self, model_dir):
        result = generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
        times = [p[2] for p in result]
        for i in range(1, len(times)):
            assert times[i] >= times[i - 1]

    def test_starts_near_start(self, model_dir):
        start = (100, 200)
        result = generate(start=start, end=(500, 400), model_dir=str(model_dir))
        first = result[0]
        assert abs(first[0] - start[0]) < 30
        assert abs(first[1] - start[1]) < 30

    def test_ends_near_end(self, model_dir):
        end = (500, 400)
        result = generate(start=(100, 200), end=end, model_dir=str(model_dir))
        # Last two are click events; the one before is last movement point
        last_move = result[-3]
        assert abs(last_move[0] - end[0]) < 30
        assert abs(last_move[1] - end[1]) < 30

    def test_last_two_are_click_events(self, model_dir):
        result = generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
        down = result[-2]
        up = result[-1]
        # Same x, y for click down and up
        assert down[0] == up[0]
        assert down[1] == up[1]
        # Up timestamp > down timestamp
        assert up[2] > down[2]
        # Click duration within bounds
        assert 20 <= up[2] - down[2] <= 300

    def test_different_z_gives_different_paths(self, model_dir):
        r1 = generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
        r2 = generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
        points1 = [(p[0], p[1]) for p in r1[:-2]]
        points2 = [(p[0], p[1]) for p in r2[:-2]]
        assert points1 != points2

    def test_n_points_parameter(self, model_dir):
        result = generate(
            start=(100, 200), end=(500, 400), n_points=32, model_dir=str(model_dir)
        )
        # 32 move points + 2 click events = 34
        assert len(result) == 34


class TestPostProcessing:
    def test_dt_diversity_preserved(self, model_dir):
        """After removing speed_profile + median clip, multiple generations
        should differ in their Δt sequences (not all identical)."""
        results = [generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
                   for _ in range(5)]
        # Extract Δt sequences (only move events, not click events)
        dts = []
        for r in results:
            moves = r[:-2]
            dt_seq = [moves[i+1][2] - moves[i][2] for i in range(len(moves)-1)]
            dts.append(dt_seq)
        # At least 2 of the 5 sequences should differ at any given index
        for i in range(min(len(d) for d in dts)):
            values = {tuple([d[i]]) for d in dts}
            if len(values) > 1:
                return  # at least one position has variation — pass
        pytest.fail("All 5 Δt sequences are identical at every position — diversity collapsed")


class TestGaussianSmooth:
    def test_endpoints_preserved(self):
        from ai_mouse.generator import _gaussian_smooth
        x = np.array([1.0, 5.0, 3.0, 7.0, 2.0], dtype=np.float64)
        smoothed = _gaussian_smooth(x, sigma=1.0)
        assert smoothed[0] == 1.0
        assert smoothed[-1] == 2.0

    def test_smooths_high_frequency(self):
        """A high-frequency square wave should have reduced amplitude after smoothing."""
        from ai_mouse.generator import _gaussian_smooth
        x = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], dtype=np.float64)
        smoothed = _gaussian_smooth(x, sigma=1.0)
        # Interior amplitude should be reduced
        interior_orig = x[2:-2]
        interior_smooth = smoothed[2:-2]
        assert interior_smooth.std() < interior_orig.std()

    def test_constant_signal_unchanged(self):
        from ai_mouse.generator import _gaussian_smooth
        x = np.full(20, 0.5, dtype=np.float64)
        smoothed = _gaussian_smooth(x, sigma=1.0)
        np.testing.assert_allclose(smoothed, x, rtol=1e-6)

    def test_short_array_returns_unchanged(self):
        """Arrays shorter than the kernel are returned unchanged."""
        from ai_mouse.generator import _gaussian_smooth
        x = np.array([1.0, 2.0, 3.0], dtype=np.float64)
        smoothed = _gaussian_smooth(x, sigma=1.0)
        np.testing.assert_allclose(smoothed, x, rtol=1e-6)
