from datetime import date, timedelta

import pytest

from scripts.run_daily_reentry_matrix import (
    PrecomputedDailyEngine,
    ReentryPolicy,
    _live_universe_rank_map,
)


RANKING_KEY = "strategy_rank"
ORD = date(2025, 1, 6).toordinal()


def _candidate(day_ord: int, *, stop: float = 91.0, rank: float = 81.0) -> dict:
    return {
        "qualified": True,
        "direction": "long",
        "symbol": "AAA",
        "date": date.fromordinal(day_ord).isoformat(),
        "entry": 100.0,
        "stop": stop,
        "target": 120.0,
        RANKING_KEY: rank,
    }


def _state(sessions: int = 0) -> dict:
    return {
        "sessions_since_stop": sessions,
        "previous_stop": 90.0,
        "previous_rank": 80.0,
        "gate_went_unqualified": False,
    }


def _call(policy: ReentryPolicy, day_ord: int, state: dict, sessions: int):
    state["sessions_since_stop"] = sessions
    return policy("AAA", day_ord, state, object())


def test_next_session_blocks_only_stop_day():
    engine = PrecomputedDailyEngine([_candidate(ORD), _candidate(ORD + 1)])
    policy = ReentryPolicy("next_session", engine, RANKING_KEY)
    state = _state()

    assert _call(policy, ORD, state, 0) is None
    assert _call(policy, ORD + 1, state, 1) is not None


@pytest.mark.parametrize("sessions", [2, 3, 5])
def test_cooldown_unlocks_at_exact_boundary(sessions):
    engine = PrecomputedDailyEngine([
        _candidate(ORD + sessions - 1),
        _candidate(ORD + sessions),
    ])
    policy = ReentryPolicy(f"cooldown_{sessions}", engine, RANKING_KEY)
    state = _state()

    assert _call(policy, ORD + sessions - 1, state, sessions - 1) is None
    assert _call(policy, ORD + sessions, state, sessions) is not None


def test_gate_reset_requires_failure_before_requalification():
    engine = PrecomputedDailyEngine([_candidate(ORD), _candidate(ORD + 2)])
    policy = ReentryPolicy("gate_reset", engine, RANKING_KEY)
    state = _state()

    assert _call(policy, ORD, state, 0) is None
    assert _call(policy, ORD + 1, state, 1) is None
    emitted = _call(policy, ORD + 2, state, 2)
    assert emitted is not None
    assert emitted["_reentry_reason"] == "gate_failed_then_requalified"


def test_strict_gate_reset_ignores_stop_day_failure():
    engine = PrecomputedDailyEngine([
        _candidate(ORD + 1),
        _candidate(ORD + 3),
    ])
    policy = ReentryPolicy("strict_gate_reset", engine, RANKING_KEY)
    state = _state()

    # An unqualified stop-day close alone does not reset the strict policy.
    assert _call(policy, ORD, state, 0) is None
    assert state["gate_went_unqualified"] is False
    assert _call(policy, ORD + 1, state, 1) is None

    # A later unqualified close establishes the reset; only then may the next
    # qualified setup re-enter.
    assert _call(policy, ORD + 2, state, 2) is None
    assert state["gate_went_unqualified"] is True
    emitted = _call(policy, ORD + 3, state, 3)
    assert emitted is not None
    assert emitted["_reentry_reason"] == (
        "post_stop_gate_failed_then_requalified"
    )


def test_improved_gate_reset_requires_better_stop_and_non_weaker_rank():
    engine = PrecomputedDailyEngine([
        _candidate(ORD + 1, stop=89.0, rank=82.0),
        _candidate(ORD + 2, stop=92.0, rank=79.0),
        _candidate(ORD + 3, stop=92.0, rank=81.0),
    ])
    policy = ReentryPolicy("gate_reset_improved", engine, RANKING_KEY)
    state = _state()

    assert _call(policy, ORD, state, 0) is None
    assert _call(policy, ORD + 1, state, 1) is None
    assert _call(policy, ORD + 2, state, 2) is None
    emitted = _call(policy, ORD + 3, state, 3)
    assert emitted is not None
    assert emitted["_reentry_reason"] == "gate_reset_with_improved_stop_and_rank"


def test_two_session_confirmation_excludes_stop_day_close():
    engine = PrecomputedDailyEngine([
        _candidate(ORD),
        _candidate(ORD + 1),
        _candidate(ORD + 2),
    ])
    policy = ReentryPolicy("two_session_confirmation", engine, RANKING_KEY)
    state = _state()

    assert _call(policy, ORD, state, 0) is None
    assert _call(policy, ORD + 1, state, 1) is None
    emitted = _call(policy, ORD + 2, state, 2)
    assert emitted is not None
    assert emitted["_reentry_reason"] == "two_qualified_post_stop_closes"


def _rank_observation(
    symbol: str,
    *,
    raw: float,
    residual: float,
    volatility: float,
) -> dict:
    return {
        "symbol": symbol,
        "date": date.fromordinal(ORD).isoformat(),
        "ranking_period": ("date", ORD),
        "momentum": raw,
        "residual_momentum": residual,
        "vol_6m": volatility,
    }


def test_live_universe_rank_uses_each_ticker_once_and_residual_when_available():
    observations = [
        _rank_observation("AAA", raw=0.1, residual=0.3, volatility=0.1),
        _rank_observation("BBB", raw=0.3, residual=0.1, volatility=0.2),
        _rank_observation("CCC", raw=0.2, residual=0.2, volatility=0.3),
    ]
    first_benchmark_day = date.fromordinal(ORD) - timedelta(days=300)
    benchmark = {
        first_benchmark_day + timedelta(days=offset): 100.0
        for offset in range(252)
    }

    ranks = _live_universe_rank_map(observations, benchmark, 0.8)

    assert ranks[("AAA", date.fromordinal(ORD).isoformat())] == {
        "momentum_percentile": 100.0,
        "volatility_percentile": 0.0,
        "strategy_rank": 80.0,
    }
    assert ranks[("BBB", date.fromordinal(ORD).isoformat())][
        "momentum_percentile"
    ] == 0.0
    assert ranks[("CCC", date.fromordinal(ORD).isoformat())][
        "strategy_rank"
    ] == 60.0


def test_live_universe_rank_uses_raw_fallback_before_benchmark_is_ready():
    observations = [
        _rank_observation("AAA", raw=0.1, residual=0.3, volatility=0.1),
        _rank_observation("BBB", raw=0.3, residual=0.1, volatility=0.2),
    ]

    ranks = _live_universe_rank_map(observations, {}, 0.8)

    assert ranks[("AAA", date.fromordinal(ORD).isoformat())][
        "momentum_percentile"
    ] == 0.0
    assert ranks[("BBB", date.fromordinal(ORD).isoformat())][
        "momentum_percentile"
    ] == 100.0


def test_live_universe_rank_rejects_duplicate_ticker_date():
    observation = _rank_observation(
        "AAA", raw=0.1, residual=0.2, volatility=0.1
    )

    with pytest.raises(ValueError, match="one observation"):
        _live_universe_rank_map([observation, dict(observation)], {}, 0.8)
