from __future__ import annotations

import numpy as np
import pytest

from cognitive_signal.data.generate_test_data import generate_synthetic_entries
from cognitive_signal.semantic.motion import SemanticMotionAnalyzer
from cognitive_signal.signals.extractors import SemanticEmbedder


@pytest.fixture(scope="module")
def real_embeddings() -> list[np.ndarray]:
    entries = generate_synthetic_entries()
    try:
        embedder = SemanticEmbedder()
        embeddings = [embedder.embed(entry.text) for entry in entries]
    except Exception as exc:
        pytest.skip(f"MiniLM embeddings unavailable for semantic motion tests: {exc}")
    return embeddings


def test_pca_runs_on_60_entries_384_dim(real_embeddings: list[np.ndarray]) -> None:
    analyzer = SemanticMotionAnalyzer()
    reduced = analyzer.reduce_dimensions(real_embeddings, n_components=10)
    assert reduced.shape[0] == 60
    assert reduced.shape[1] <= 10


def test_step_distances_show_phase_transition_spikes(real_embeddings: list[np.ndarray]) -> None:
    analyzer = SemanticMotionAnalyzer()
    distances = analyzer.compute_step_distances(real_embeddings)

    assert len(distances) == 60
    assert distances[0] == pytest.approx(0.0, abs=1e-9)

    top_k = max(6, len(distances) // 5)
    top_indices = set(np.argsort(np.asarray(distances))[-top_k:].tolist())
    assert any(18 <= index <= 25 for index in top_indices)
    assert any(38 <= index <= 45 for index in top_indices)

    stable_values = [
        distance
        for index, distance in enumerate(distances)
        if index != 0 and not (18 <= index <= 25 or 38 <= index <= 45)
    ]
    stable_p75 = float(np.percentile(np.asarray(stable_values), 75))
    assert max(distances[18:26]) > stable_p75
    assert max(distances[38:46]) > stable_p75


def test_identity_shift_dual_check_defaults_and_tuned(
    real_embeddings: list[np.ndarray],
) -> None:
    analyzer = SemanticMotionAnalyzer()
    default_shifts = analyzer.detect_identity_shifts(real_embeddings)
    assert len(default_shifts) >= 1

    distances = analyzer.compute_step_distances(real_embeddings)
    non_zero = [value for value in distances[1:] if value > 0.0]
    epsilon = float(np.percentile(np.asarray(non_zero), 80)) if non_zero else 0.5
    tuned_shifts = analyzer.detect_identity_shifts(
        real_embeddings, epsilon=epsilon, tau=0.4, window=7
    )
    timesteps = [shift.timestep for shift in tuned_shifts]
    assert any(20 <= timestep <= 25 for timestep in timesteps)
    assert any(40 <= timestep <= 45 for timestep in timesteps)


def test_reduce_dimensions_single_entry_returns_original_shape(
    real_embeddings: list[np.ndarray],
) -> None:
    analyzer = SemanticMotionAnalyzer()
    reduced = analyzer.reduce_dimensions([real_embeddings[0]], n_components=10)
    assert reduced.shape == (1, 384)


def test_compute_drift_vector_basic_contract(real_embeddings: list[np.ndarray]) -> None:
    analyzer = SemanticMotionAnalyzer()
    reduced = analyzer.reduce_dimensions(real_embeddings, n_components=10)
    drift_vectors = analyzer.compute_drift_vector(reduced, window=7)

    assert len(drift_vectors) == reduced.shape[0]
    assert np.allclose(drift_vectors[0], np.zeros(reduced.shape[1], dtype=np.float32))
    assert all(vector.shape == drift_vectors[0].shape for vector in drift_vectors)


def test_empty_input_contracts() -> None:
    analyzer = SemanticMotionAnalyzer()
    assert analyzer.compute_step_distances([]) == []
    reduced = analyzer.reduce_dimensions([])
    assert reduced.shape == (0, 0)
    assert analyzer.compute_drift_vector(np.empty((0, 0), dtype=np.float32)) == []
    assert analyzer.detect_identity_shifts([]) == []

    output = analyzer.analyze([])
    assert output.step_distances == []
    assert output.reduced_embeddings.shape == (0, 0)
    assert output.drift_vectors == []
    assert output.identity_shifts == []
