from __future__ import annotations

import numpy as np
import pytest

from cognitive_signal.changepoint.detector import ChangePointDetector, SCALAR_SIGNALS
from cognitive_signal.models.types import SignalVector


def _build_phase_fixture() -> tuple[list[SignalVector], list[np.ndarray]]:
    rng = np.random.default_rng(7)
    embedding_centers = [
        np.concatenate([np.full(128, 2.0), np.zeros(256)]).astype(np.float32),
        np.concatenate([np.zeros(128), np.full(128, 2.0), np.zeros(128)]).astype(np.float32),
        np.concatenate([np.zeros(256), np.full(128, 2.0)]).astype(np.float32),
    ]
    scalar_profiles = [
        (0.66, 0.34, 0.80, 0.24, 0.20, 0.86),
        (0.20, 0.62, 0.45, 0.56, 0.74, 0.42),
        (0.06, 0.24, 0.22, 0.84, 0.36, 0.21),
    ]

    signal_vectors: list[SignalVector] = []
    embeddings: list[np.ndarray] = []

    for index in range(60):
        phase = 0 if index < 20 else 1 if index < 40 else 2
        embedding = embedding_centers[phase] + rng.normal(0.0, 0.05, size=384).astype(np.float32)
        embeddings.append(embedding)

        noise = rng.normal(0.0, 0.02, size=6)
        base = scalar_profiles[phase]
        signal_vectors.append(
            SignalVector(
                embedding=embedding,
                valence=float(base[0] + noise[0]),
                arousal=float(base[1] + noise[1]),
                agency=float(base[2] + noise[2]),
                self_focus=float(base[3] + noise[3]),
                social_orientation=float(base[4] + noise[4]),
                action_ratio=float(base[5] + noise[5]),
            )
        )

    return signal_vectors, embeddings


@pytest.fixture(scope="module")
def phase_fixture() -> tuple[list[SignalVector], list[np.ndarray]]:
    return _build_phase_fixture()


def test_detect_signal_changepoints_near_20_and_40(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    signal_vectors, _ = phase_fixture
    values = [vector.valence for vector in signal_vectors]
    changepoints = detector.detect_signal_changepoints(values, method="pelt", penalty=3.0)
    assert any(18 <= cp <= 22 for cp in changepoints)
    assert any(38 <= cp <= 42 for cp in changepoints)


def test_should_not_detect_middle_of_stable_phases(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    signal_vectors, _ = phase_fixture
    values = [vector.valence for vector in signal_vectors]
    changepoints = detector.detect_signal_changepoints(values, method="pelt", penalty=3.0)

    stable_interior = (
        set(range(5, 15))
        | set(range(26, 35))
        | set(range(46, 55))
    )
    assert all(cp not in stable_interior for cp in changepoints)


def test_semantic_changepoints_default_quantile(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    _, embeddings = phase_fixture
    changepoints = detector.detect_semantic_changepoints(embeddings, window=8)
    timesteps = [item.timestep for item in changepoints]
    assert any(18 <= step <= 25 for step in timesteps)
    assert any(38 <= step <= 45 for step in timesteps)


def test_semantic_quantile_parameter_affects_sensitivity(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    _, embeddings = phase_fixture
    low_quantile = detector.detect_semantic_changepoints(embeddings, window=8, quantile=0.7)
    high_quantile = detector.detect_semantic_changepoints(embeddings, window=8, quantile=0.95)
    assert len(low_quantile) >= len(high_quantile)


def test_invalid_quantile_raises(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    _, embeddings = phase_fixture
    with pytest.raises(ValueError):
        detector.detect_semantic_changepoints(embeddings, quantile=-0.1)
    with pytest.raises(ValueError):
        detector.detect_semantic_changepoints(embeddings, quantile=1.2)


def test_detect_all_merges_and_deduplicates(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    signal_vectors, embeddings = phase_fixture
    output = detector.detect_all(signal_vectors, embeddings, semantic_quantile=0.85)

    assert set(output.signal_changepoints.keys()) == set(SCALAR_SIGNALS)
    merged = output.merged_changepoints
    assert merged == sorted(set(merged))
    assert all((right - left) > 3 for left, right in zip(merged, merged[1:]))
    assert any(18 <= cp <= 25 for cp in merged)
    assert any(38 <= cp <= 45 for cp in merged)


def test_phase_annotation_should_produce_roughly_three_phases(
    phase_fixture: tuple[list[SignalVector], list[np.ndarray]],
) -> None:
    detector = ChangePointDetector()
    signal_vectors, embeddings = phase_fixture
    output = detector.detect_all(signal_vectors, embeddings)
    phases = detector.annotate_phases(output.merged_changepoints, n_entries=60)

    assert 2 <= len(phases) <= 4
    assert phases[0].start == 0
    assert phases[-1].end == 59
    for previous, current in zip(phases, phases[1:]):
        assert previous.end + 1 == current.start


def test_uniform_signal_has_no_change_points() -> None:
    detector = ChangePointDetector()
    values = [0.5] * 60
    pelt = detector.detect_signal_changepoints(values, method="pelt", penalty=10.0)
    binseg = detector.detect_signal_changepoints(values, method="binseg", penalty=10.0)
    assert pelt == []
    assert binseg == []


def test_edge_cases_short_inputs() -> None:
    detector = ChangePointDetector()
    assert detector.detect_signal_changepoints([]) == []
    assert detector.detect_semantic_changepoints([]) == []
    output = detector.detect_all([], [])
    assert output.signal_changepoints == {name: [] for name in SCALAR_SIGNALS}
    assert output.semantic_changepoints == []
    assert output.merged_changepoints == []
    assert detector.annotate_phases([], 0) == []
