"""Unit tests for the activation qualification predicate (momentum-based gate)."""

from __future__ import annotations

from types import SimpleNamespace

from app.services.qualification import (
    best_target_probability,
    primary_target_probability,
    setup_qualifies,
)

# Default gate: floors only; the momentum selection is off (0). Conviction /
# conflict are optional tighteners, off here. Target probability is always
# required because qualified means the headline target is evidence-backed.
DEFAULT_GATE = {
    "min_momentum_percentile": 0.0,
    "min_rr": 1.2,
    "min_confidence": 55.0,
    "require_high_conviction": False,
    "exclude_conflicts": False,
}

# Gate with the cross-sectional momentum selection on (top quintile).
MOMENTUM_GATE = {**DEFAULT_GATE, "min_momentum_percentile": 80.0}

# Strict gate: every optional tightener turned on.
STRICT_GATE = {
    "min_momentum_percentile": 0.0,
    "min_rr": 2.0,
    "min_confidence": 70.0,
    "require_high_conviction": True,
    "exclude_conflicts": True,
}


def _setup(**kwargs):
    base = dict(
        direction="long",
        rr_ratio=3.0,
        confidence_score=80.0,
        recommended_action="LONG_HIGH",
        risk_level="Low",
        targets=[{"probability": 50.0, "is_primary": True}],
    )
    base.update(kwargs)
    return SimpleNamespace(**base)


class TestFloors:
    def test_passes_floors(self):
        assert setup_qualifies(_setup(), DEFAULT_GATE) is True

    def test_low_rr_floor_fails(self):
        assert setup_qualifies(_setup(rr_ratio=1.0), DEFAULT_GATE) is False

    def test_low_confidence_fails(self):
        assert setup_qualifies(_setup(confidence_score=40.0), DEFAULT_GATE) is False

    def test_conviction_and_conflict_ignored_by_default(self):
        s = _setup(recommended_action="LONG_MODERATE", risk_level="Medium")
        assert setup_qualifies(s, DEFAULT_GATE) is True

    def test_over_progressed_setup_fails_on_live_rr(self):
        s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=117.0)
        assert setup_qualifies(s, DEFAULT_GATE) is False

    def test_fresh_setup_passes_live_rr(self):
        s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=101.0)
        assert setup_qualifies(s, DEFAULT_GATE) is True

    def test_past_stop_fails_live_rr(self):
        s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=94.0)
        assert setup_qualifies(s, DEFAULT_GATE) is False

    def test_missing_target_probability_fails(self):
        assert setup_qualifies(_setup(targets=[]), DEFAULT_GATE) is False

    def test_non_primary_target_probability_still_passes(self):
        assert setup_qualifies(
            _setup(targets=[{"probability": 42.0}]),
            DEFAULT_GATE,
        ) is True

    def test_lottery_primary_target_fails(self):
        # A far target pinned at the model's 3% clamp floor: its distance keeps
        # both stored and live R:R above the gate, so only the probability floor
        # can reject it (the stale pre-fix lottery-headline case).
        s = _setup(rr_ratio=3.09, targets=[{"probability": 3.0, "is_primary": True}])
        assert setup_qualifies(s, DEFAULT_GATE) is False

    def test_probability_at_floor_passes(self):
        assert setup_qualifies(
            _setup(targets=[{"probability": 20.0, "is_primary": True}]),
            DEFAULT_GATE,
        ) is True

    def test_probability_just_below_floor_fails(self):
        assert setup_qualifies(
            _setup(targets=[{"probability": 19.9, "is_primary": True}]),
            DEFAULT_GATE,
        ) is False

    def test_best_target_fallback_below_floor_fails(self):
        # No starred primary: the fallback takes the best target, which must
        # still clear the probability floor.
        s = _setup(targets=[{"probability": 12.0}, {"probability": 8.0}])
        assert setup_qualifies(s, DEFAULT_GATE) is False


class TestMomentumGate:
    def test_top_momentum_passes(self):
        assert setup_qualifies(_setup(momentum_percentile=92.0), MOMENTUM_GATE) is True

    def test_below_threshold_fails(self):
        assert setup_qualifies(_setup(momentum_percentile=50.0), MOMENTUM_GATE) is False

    def test_missing_percentile_fails_when_gate_active(self):
        # No residual rank means the production momentum edge was not measured.
        assert setup_qualifies(_setup(), MOMENTUM_GATE) is False

    def test_threshold_zero_disables_gate(self):
        # min_momentum_percentile 0 → a low-momentum name still passes.
        assert setup_qualifies(_setup(momentum_percentile=10.0), DEFAULT_GATE) is True

    def test_threshold_zero_allows_missing_percentile(self):
        assert setup_qualifies(_setup(), DEFAULT_GATE) is True

    def test_missing_key_defaults_off(self):
        legacy = {k: v for k, v in DEFAULT_GATE.items() if k != "min_momentum_percentile"}
        assert setup_qualifies(_setup(momentum_percentile=10.0), legacy) is True

    def test_short_excluded_when_gate_active(self):
        # The momentum edge is long-only — a short never qualifies while the gate
        # is on, even on a top-momentum name.
        assert setup_qualifies(_setup(direction="short", momentum_percentile=95.0), MOMENTUM_GATE) is False

    def test_short_allowed_when_gate_off(self):
        # With the momentum gate disabled, shorts pass on the floors as before.
        assert setup_qualifies(_setup(direction="short", momentum_percentile=10.0), DEFAULT_GATE) is True


class TestStrictTighteners:
    def test_clean_high_conviction_passes(self):
        assert setup_qualifies(_setup(targets=[{"probability": 65.0, "is_primary": True}]), STRICT_GATE) is True

    def test_moderate_action_fails(self):
        s = _setup(recommended_action="LONG_MODERATE", targets=[{"probability": 65.0, "is_primary": True}])
        assert setup_qualifies(s, STRICT_GATE) is False

    def test_non_low_risk_fails(self):
        s = _setup(risk_level="Medium", targets=[{"probability": 65.0, "is_primary": True}])
        assert setup_qualifies(s, STRICT_GATE) is False


NEUTRAL_GATE = {**DEFAULT_GATE, "exclude_neutral": True}


class TestExcludeNeutral:
    def test_neutral_excluded_when_on(self):
        assert setup_qualifies(_setup(recommended_action="NEUTRAL"), NEUTRAL_GATE) is False

    def test_missing_action_treated_as_neutral(self):
        assert setup_qualifies(_setup(recommended_action=None), NEUTRAL_GATE) is False

    def test_directional_passes_when_on(self):
        assert setup_qualifies(_setup(recommended_action="LONG_MODERATE"), NEUTRAL_GATE) is True

    def test_opposing_short_action_fails_for_long_setup(self):
        assert setup_qualifies(_setup(direction="long", recommended_action="SHORT_MODERATE"), NEUTRAL_GATE) is False

    def test_matching_short_action_still_fails_long_only_momentum_gate(self):
        assert setup_qualifies(
            _setup(direction="short", recommended_action="SHORT_MODERATE", momentum_percentile=95.0),
            {**NEUTRAL_GATE, "min_momentum_percentile": 80.0},
        ) is False

    def test_neutral_allowed_when_off(self):
        # Flag absent from the config → NEUTRAL still qualifies (backward compatible).
        assert setup_qualifies(_setup(recommended_action="NEUTRAL"), DEFAULT_GATE) is True


class TestBestTargetProbability:
    def test_returns_max(self):
        s = _setup(targets=[{"probability": 40.0}, {"probability": 72.0}, {"probability": 55.0}])
        assert best_target_probability(s) == 72.0

    def test_empty_is_zero(self):
        assert best_target_probability(_setup(targets=[])) == 0.0

    def test_primary_probability_prefers_starred_target(self):
        s = _setup(targets=[
            {"probability": 70.0},
            {"probability": 45.0, "is_primary": True},
        ])
        assert primary_target_probability(s) == 45.0

    def test_primary_probability_falls_back_to_best(self):
        s = _setup(targets=[{"probability": 40.0}, {"probability": 72.0}])
        assert primary_target_probability(s) == 72.0
