"""Tests for TrajectoryFlowModel architecture."""
from __future__ import annotations

import torch
import pytest

from tools.models import TrajectoryFlowModel


class TestTrajectoryFlowModel:
    """Test the Conditional Flow Matching model."""

    @pytest.fixture
    def model(self):
        return TrajectoryFlowModel(
            seq_len=64, d_model=128, nhead=4, num_layers=4,
            dim_feedforward=256, dropout=0.1, cond_dim=3,
        )

    def test_output_shape(self, model):
        """(4, 64, 3) input → (4, 64, 3) output."""
        batch = 4
        x_t = torch.randn(batch, 64, 3)
        t = torch.rand(batch)
        cond = torch.randn(batch, 3)
        out = model(x_t, t, cond)
        assert out.shape == (batch, 64, 3)

    def test_single_sample(self, model):
        """(1, 64, 3) works."""
        x_t = torch.randn(1, 64, 3)
        t = torch.rand(1)
        cond = torch.randn(1, 3)
        out = model(x_t, t, cond)
        assert out.shape == (1, 64, 3)

    def test_deterministic(self, model):
        """Eval mode, same input → same output."""
        model.eval()
        x_t = torch.randn(2, 64, 3)
        t = torch.tensor([0.3, 0.7])
        cond = torch.randn(2, 3)
        with torch.no_grad():
            out1 = model(x_t, t, cond)
            out2 = model(x_t, t, cond)
        torch.testing.assert_close(out1, out2)

    def test_different_timesteps(self, model):
        """t=0.1 vs t=0.9 gives different output."""
        model.eval()
        x_t = torch.randn(1, 64, 3)
        cond = torch.randn(1, 3)
        with torch.no_grad():
            out_early = model(x_t, torch.tensor([0.1]), cond)
            out_late = model(x_t, torch.tensor([0.9]), cond)
        assert not torch.allclose(out_early, out_late, atol=1e-5)

    def test_gradient_flows(self, model):
        """Backward works, grad on x_t exists."""
        model.train()
        x_t = torch.randn(2, 64, 3, requires_grad=True)
        t = torch.rand(2)
        cond = torch.randn(2, 3)
        out = model(x_t, t, cond)
        loss = out.sum()
        loss.backward()
        assert x_t.grad is not None
        assert x_t.grad.shape == (2, 64, 3)
        assert x_t.grad.abs().sum() > 0
