from __future__ import annotations

import datetime as dt
from collections import defaultdict

from apps.backtester.run import BacktestRunner
from libs.backtest.domain import (
    BacktestConfig,
    ExperimentManifest,
    SignalConfig,
    StrategyEngineConfig,
)


class _DummyStore:
    def __init__(self, rows_by_date: dict[dt.date, list[dict]], macro_by_date: dict[dt.date, dict]):
        self._rows_by_date = rows_by_date
        self._macro_by_date = macro_by_date

    def get_candidates_for_date(self, date: dt.date) -> list[dict]:
        return list(self._rows_by_date.get(date, []))

    def get_candidates_for_reaction_date(self, date: dt.date) -> list[dict]:
        return []

    def get_macro_for_date(self, date: dt.date) -> dict:
        return dict(self._macro_by_date.get(date, {}))


class _DummyAttentionService:
    def engine_requires_attention(self, engine: StrategyEngineConfig) -> bool:
        return False

    def apply_filters(self, candidates, engine, signal):
        return candidates


def _make_runner(
    *,
    engine: StrategyEngineConfig,
    rows_by_date: dict[dt.date, list[dict]],
    macro_by_date: dict[dt.date, dict],
    simulation_dates: list[dt.date],
) -> BacktestRunner:
    runner = BacktestRunner.__new__(BacktestRunner)
    runner.manifest = ExperimentManifest(
        experiment_name="test_volatility_crush",
        dataset_snapshot_id="snap",
        base_config="configs/backtest/return_max_long_v1.json",
    )
    runner.config = BacktestConfig(
        strategy_name="test_strategy",
        dataset_snapshot_id="snap",
        signal=SignalConfig(score_threshold=0.45, max_candidates_per_day=5),
        strategy_engines=[engine],
    )
    runner.store = _DummyStore(rows_by_date, macro_by_date)
    runner._active_strategy_engines = [engine]
    runner._shadow_strategy_engines = []
    runner._strategy_engine_lookup = {engine.engine_id: engine}
    runner._scheduled_add_ons = defaultdict(list)
    runner._attention_service = _DummyAttentionService()
    runner._simulation_dates = simulation_dates
    runner._simulation_date_index = {
        sim_date: idx for idx, sim_date in enumerate(simulation_dates)
    }
    return runner


def _make_row(**updates) -> dict:
    row = {
        "event_id": "EVT::AAPL::2026-01-06",
        "symbol": "AAPL",
        "issuer_id": "ISSUER::AAPL",
        "sector": "Technology",
        "event_type": "earnings_release",
        "event_timestamp": "2026-01-05T21:00:00+00:00",
        "event_date": "2026-01-05",
        "reaction_date": "2026-01-05",
        "entry_date": "2026-01-06",
        "entry_price": 150.0,
        "event_close": 149.0,
        "reaction_day_low": 146.0,
        "reaction_day_high": 151.0,
        "score": 0.41,
        "avg_dollar_volume": 25_000_000.0,
        "avg_dollar_volume_20d": 25_000_000.0,
        "atr_14": 3.5,
        "filing_time_bucket": "post_market",
        "entry_convention": "next_open_after_reaction_close",
        "reaction_day_return": 0.05,
        "close_location": 0.72,
        "volume_ratio": 2.0,
        "gap_size": 0.02,
        "macro_vix": 33.0,
    }
    row.update(updates)
    return row


def test_volatility_crush_state_computes_from_prev_day_macro() -> None:
    engine = StrategyEngineConfig(
        engine_id="test_engine",
        event_types=["earnings_release"],
        volatility_crush_vix_drop_pct_min=0.10,
        volatility_crush_spy_return_min=0.0,
    )
    prev_date = dt.date(2026, 1, 5)
    date = dt.date(2026, 1, 6)
    runner = _make_runner(
        engine=engine,
        rows_by_date={},
        macro_by_date={
            prev_date: {"VIXCLS": 40.0, "spy_close": 500.0},
            date: {"VIXCLS": 35.0, "spy_close": 505.0},
        },
        simulation_dates=[prev_date, date],
    )

    crush = runner._volatility_crush_state_for_date(date)

    assert crush is not None
    assert round(crush["vix_drop_pct"], 4) == 0.125
    assert round(crush["spy_return"], 4) == 0.01


def test_select_candidates_for_date_applies_volatility_crush_overrides() -> None:
    engine = StrategyEngineConfig(
        engine_id="crush_next_open",
        event_types=["earnings_release"],
        direction="long_only",
        entry_timing_policy="next_open",
        score_threshold_override=0.45,
        macro_vix_max=30.0,
        volatility_crush_vix_drop_pct_min=0.10,
        volatility_crush_spy_return_min=0.0,
        volatility_crush_score_threshold_override=0.40,
        volatility_crush_macro_vix_max_override=40.0,
        volatility_crush_per_trade_risk_pct_override=0.08,
    )
    prev_date = dt.date(2026, 1, 5)
    date = dt.date(2026, 1, 6)
    rows = {date: [_make_row()]}
    macro = {
        prev_date: {"VIXCLS": 40.0, "spy_close": 500.0},
        date: {"VIXCLS": 35.0, "spy_close": 505.0},
    }
    runner = _make_runner(
        engine=engine,
        rows_by_date=rows,
        macro_by_date=macro,
        simulation_dates=[prev_date, date],
    )

    selected = runner._select_candidates_for_date(date)

    assert len(selected) == 1
    assert selected[0].score == 0.41
    assert selected[0].engine_per_trade_risk_pct == 0.08
    assert selected[0].engine_id == "crush_next_open"


def test_select_candidates_for_date_uses_base_engine_without_crush() -> None:
    engine = StrategyEngineConfig(
        engine_id="crush_next_open",
        event_types=["earnings_release"],
        direction="long_only",
        entry_timing_policy="next_open",
        score_threshold_override=0.45,
        macro_vix_max=30.0,
        volatility_crush_vix_drop_pct_min=0.10,
        volatility_crush_spy_return_min=0.0,
        volatility_crush_score_threshold_override=0.40,
        volatility_crush_macro_vix_max_override=40.0,
    )
    prev_date = dt.date(2026, 1, 5)
    date = dt.date(2026, 1, 6)
    rows = {date: [_make_row()]}
    macro = {
        prev_date: {"VIXCLS": 40.0, "spy_close": 500.0},
        date: {"VIXCLS": 38.0, "spy_close": 499.0},
    }
    runner = _make_runner(
        engine=engine,
        rows_by_date=rows,
        macro_by_date=macro,
        simulation_dates=[prev_date, date],
    )

    selected = runner._select_candidates_for_date(date)

    assert selected == []


def test_engine_allowed_for_date_blocks_crush_only_engine_when_condition_not_met() -> None:
    engine = StrategyEngineConfig(
        engine_id="crush_only_engine",
        event_types=["earnings_release"],
        volatility_crush_only=True,
        volatility_crush_vix_drop_pct_min=0.10,
        volatility_crush_spy_return_min=0.0,
    )
    prev_date = dt.date(2026, 1, 5)
    date = dt.date(2026, 1, 6)
    runner = _make_runner(
        engine=engine,
        rows_by_date={},
        macro_by_date={
            prev_date: {"VIXCLS": 40.0, "spy_close": 500.0},
            date: {"VIXCLS": 38.0, "spy_close": 499.0},
        },
        simulation_dates=[prev_date, date],
    )

    assert runner._engine_allowed_for_date(engine, date) is False
