from __future__ import annotations

import numpy as np
import pandas as pd
import pytest

from psx_signal.evaluation import (
    add_cross_sectional_ranks,
    contributor_exclusions,
    feature_drift,
    feature_family_stability,
    feature_importance_stability,
    random_same_day_rank_simulation,
    ranking_diagnostics,
    signal_level_metrics,
    time_block_bootstrap,
)


def test_signal_metrics_do_not_emit_fake_portfolio_returns() -> None:
    metrics = signal_level_metrics([0.10, -0.05, 0.02])

    assert metrics["count"] == 3
    assert metrics["win_rate"] == pytest.approx(2 / 3)
    assert metrics["profit_factor"] == pytest.approx(2.4)
    assert "total_return" not in metrics
    assert "maximum_drawdown" not in metrics


def test_cross_sectional_ranks_are_score_then_symbol_deterministic() -> None:
    candidates = pd.DataFrame({
        "date": ["2025-01-02"] * 4,
        "symbol": ["C", "B", "A", "D"],
        "buy_score": [0.9, 0.9, 0.7, 0.1],
        "forward_return": [0.03, 0.02, 0.01, -0.01],
    })

    ranked = add_cross_sectional_ranks(candidates)

    assert ranked["symbol"].tolist() == ["B", "C", "A", "D"]
    assert ranked["cross_sectional_rank"].tolist() == [1, 2, 3, 4]
    assert ranked["rank_band"].tolist() == ["TOP_1", "RANK_2_3", "RANK_2_3", "RANK_4_5"]
    diagnostics = ranking_diagnostics(ranked)
    top_one_return = diagnostics.loc[
        diagnostics["group"] == "TOP_1", "average_return"
    ].item()
    assert top_one_return == pytest.approx(0.02)


def test_random_same_day_selection_is_reproducible_and_preserves_count() -> None:
    candidates = pd.DataFrame({
        "date": np.repeat(pd.date_range("2025-01-01", periods=5), 4),
        "symbol": list("ABCD") * 5,
        "forward_return": np.linspace(-0.04, 0.04, 20),
    })

    def evaluator(selected: pd.DataFrame) -> dict[str, float]:
        return {"portfolio_marker": float(selected["forward_return"].sum())}
    first = random_same_day_rank_simulation(
        candidates, simulations=20, seed=91, evaluator=evaluator
    )
    second = random_same_day_rank_simulation(
        candidates, simulations=20, seed=91, evaluator=evaluator
    )

    pd.testing.assert_frame_equal(first, second)
    assert first["count"].eq(5).all()
    assert "portfolio_marker" in first


def test_time_block_bootstrap_is_reproducible_and_reports_intervals() -> None:
    observations = pd.DataFrame({
        "date": np.repeat(pd.date_range("2024-01-01", periods=40), 2),
        "forward_return": np.tile([0.02, -0.01], 40),
    })

    first = time_block_bootstrap(observations, simulations=100, block_sessions=5, seed=8)
    second = time_block_bootstrap(observations, simulations=100, block_sessions=5, seed=8)

    pd.testing.assert_frame_equal(first, second)
    assert first["metric"].tolist() == ["win_rate", "average_return", "profit_factor"]
    assert (first["lower"] <= first["median"]).all()
    assert (first["median"] <= first["upper"]).all()


def test_contributor_exclusions_are_programmatic() -> None:
    observations = pd.DataFrame({
        "symbol": ["A", "A", "B", "B", "C"],
        "forward_return": [0.1, 0.1, 0.03, -0.01, -0.02],
    })

    contributions, exclusions = contributor_exclusions(observations, levels=(1, 2))

    assert contributions.iloc[0]["symbol"] == "A"
    top_one = exclusions[exclusions["exclusion"] == "TOP_1_CONTRIBUTORS"].iloc[0]
    assert top_one["excluded_symbols"] == "A"
    assert top_one["average_return"] == pytest.approx(0.0)


def test_feature_drift_detects_shift() -> None:
    reference = pd.DataFrame({"stable": np.arange(100), "shifted": np.arange(100)})
    comparison = pd.DataFrame({"stable": np.arange(100), "shifted": np.arange(100) + 100})

    drift = feature_drift(reference, comparison, ["stable", "shifted"])
    stable = drift.set_index("feature").loc["stable"]
    shifted = drift.set_index("feature").loc["shifted"]

    assert stable["psi"] == pytest.approx(0.0)
    assert stable["ks_statistic"] == pytest.approx(0.0)
    assert shifted["psi"] > 1
    assert shifted["ks_statistic"] == pytest.approx(1.0)


def test_feature_importance_stability_reports_fold_rank_correlation() -> None:
    importances = pd.DataFrame({
        "fold": [0, 0, 0, 1, 1, 1],
        "feature": ["a", "b", "c"] * 2,
        "importance": [0.6, 0.3, 0.1, 0.5, 0.3, 0.2],
    })

    top, correlations = feature_importance_stability(importances, top_n=2)
    families = feature_family_stability(importances)

    assert set(top["feature"]) == {"a", "b"}
    assert correlations.iloc[0]["spearman_rank_correlation"] == pytest.approx(1.0)
    assert families["feature_family"].eq("OTHER").all()
    assert families["family_importance_share"].eq(1.0).all()
