from __future__ import annotations

import datetime as dt
from pathlib import Path
from zoneinfo import ZoneInfo

from apps.backtester.run import BacktestRunner, _extend_store_to_requested_window
from libs.backtest.domain import FilledTrade
from libs.backtest.domain import BacktestConfig
from libs.backtest.domain import Candidate, ExitReason, OpenPosition, PlannedOrder
from libs.backtest.snapshot_store import SnapshotStore


def _make_config() -> BacktestConfig:
    config = BacktestConfig(
        strategy_name="test_strategy",
        dataset_snapshot_id="snap",
    )
    config.risk.cash_parking_enabled = True
    config.risk.cash_parking_preset = None
    return config


def test_extend_store_to_requested_window_backfills_macro_prefix(monkeypatch, tmp_path: Path) -> None:
    snapshot_dir = tmp_path / "snap"
    snapshot_dir.mkdir(parents=True)
    (snapshot_dir / "manifest.json").write_text("{}")
    (snapshot_dir / "train.parquet").write_text("x")
    (snapshot_dir / "valid.parquet").write_text("x")
    (snapshot_dir / "test.parquet").write_text("x")

    store = SnapshotStore(
        candidates_by_exec_date={},
        bars_by_symbol_date={},
        macro_by_date={
            dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0},
            dt.date(2022, 3, 2): {"spy_close": 431.0, "qqqm_close": 154.0, "sgov_close": 100.0},
        },
    )

    async def _fake_fetch_macro(date_range, _db_dsn):
        assert date_range == (dt.date(2022, 1, 1), dt.date(2022, 2, 28))
        return {
            dt.date(2022, 1, 3): {"VIXCLS": 20.0},
            dt.date(2022, 1, 4): {"VIXCLS": 21.0},
        }

    async def _fake_fetch_spy_macro(date_range, _oracle_url):
        assert date_range == (dt.date(2022, 1, 1), dt.date(2022, 2, 28))
        return {
            dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1},
            dt.date(2022, 1, 4): {"spy_close": 471.0, "qqqm_close": 180.5, "sgov_close": 100.1},
        }

    monkeypatch.setattr("apps.backtester.run.resolve_snapshot_path", lambda *_args, **_kwargs: snapshot_dir)
    monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_fake_fetch_macro))
    monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_fake_fetch_spy_macro))

    extended = _extend_store_to_requested_window(
        store=store,
        config=_make_config(),
        start_date=dt.date(2022, 1, 1),
        end_date=dt.date(2022, 4, 1),
    )

    assert min(extended._macro) == dt.date(2022, 1, 3)
    assert extended.all_trading_days()[0] == dt.date(2022, 1, 3)
    assert extended.get_macro_for_date(dt.date(2022, 1, 3))["qqqm_close"] == 180.0


def test_extend_store_to_requested_window_reuses_cached_macro_window(monkeypatch, tmp_path: Path) -> None:
    snapshot_dir = tmp_path / "snap"
    snapshot_dir.mkdir(parents=True)
    (snapshot_dir / "manifest.json").write_text("{}")
    (snapshot_dir / "train.parquet").write_text("x")
    (snapshot_dir / "valid.parquet").write_text("x")
    (snapshot_dir / "test.parquet").write_text("x")

    cache_file = snapshot_dir / "macro_window_2022-01-01_2022-02-28.pkl"
    cached_store = SnapshotStore(
        candidates_by_exec_date={},
        bars_by_symbol_date={},
        macro_by_date={
            dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0},
        },
    )

    async def _initial_fetch_macro(date_range, _db_dsn):
        return {dt.date(2022, 1, 3): {"VIXCLS": 20.0}}

    async def _initial_fetch_spy_macro(date_range, _oracle_url):
        return {dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1}}

    monkeypatch.setattr("apps.backtester.run.resolve_snapshot_path", lambda *_args, **_kwargs: snapshot_dir)
    monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_initial_fetch_macro))
    monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_initial_fetch_spy_macro))

    _extend_store_to_requested_window(
        store=cached_store,
        config=_make_config(),
        start_date=dt.date(2022, 1, 1),
        end_date=dt.date(2022, 2, 28),
    )
    assert cache_file.exists()

    store = SnapshotStore(
        candidates_by_exec_date={},
        bars_by_symbol_date={},
        macro_by_date={
            dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0},
        },
    )

    async def _unexpected_fetch_macro(*_args, **_kwargs):
        raise AssertionError("macro fetch should not run when cached macro window exists")

    async def _unexpected_fetch_spy_macro(*_args, **_kwargs):
        raise AssertionError("spy macro fetch should not run when cached macro window exists")

    monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_unexpected_fetch_macro))
    monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_unexpected_fetch_spy_macro))

    extended = _extend_store_to_requested_window(
        store=store,
        config=_make_config(),
        start_date=dt.date(2022, 1, 1),
        end_date=dt.date(2022, 2, 28),
    )

    assert min(extended._macro) == dt.date(2022, 1, 3)
    assert extended.get_macro_for_date(dt.date(2022, 1, 3))["sgov_close"] == 100.1


def test_backtest_runner_uses_requested_window_for_simulation_dates() -> None:
    store = SnapshotStore(
        candidates_by_exec_date={
            dt.date(2022, 3, 3): [{"event_id": "evt", "symbol": "AAPL"}],
        },
        bars_by_symbol_date={},
        macro_by_date={
            dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1},
            dt.date(2022, 3, 3): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0},
        },
    )
    setattr(store, "_requested_start_date", dt.date(2022, 1, 3))
    setattr(store, "_requested_end_date", dt.date(2022, 3, 4))

    runner = BacktestRunner.__new__(BacktestRunner)
    runner.store = store
    runner.config = _make_config()
    runner._active_strategy_engines = []

    simulation_dates = runner._get_simulation_dates()

    assert simulation_dates[0] == dt.date(2022, 1, 3)
    assert simulation_dates[-1] == dt.date(2022, 3, 4)


def test_record_parking_trade_keeps_actual_sgov_share_count() -> None:
    runner = BacktestRunner.__new__(BacktestRunner)
    runner._parking_entry_date = dt.date(2022, 1, 3)
    runner._parking_trade_counter = 0
    runner._closed_trades = []

    runner._record_parking_trade(
        exit_date=dt.date(2022, 4, 12),
        symbol="sgov",
        shares=117,
        entry_price=85.16,
        exit_price=85.19,
    )

    assert len(runner._closed_trades) == 1
    trade: FilledTrade = runner._closed_trades[0]
    assert trade.symbol == "SGOV"
    assert trade.shares == 117
    assert round(trade.net_pnl, 2) == round((85.19 - 85.16) * 117, 2)


def test_force_close_all_uses_requested_end_bar_and_end_of_backtest_reason() -> None:
    utc = ZoneInfo("UTC")
    candidate = Candidate(
        event_id="EVT::TEST",
        symbol="ENB",
        score=0.9,
        sector="Energy",
        event_type="earnings",
        event_timestamp=dt.datetime(2026, 3, 23, 21, 0, tzinfo=utc),
        event_date=dt.date(2026, 3, 23),
        filing_time_bucket="post_market",
        reaction_date=dt.date(2026, 3, 24),
        execution_date=dt.date(2026, 3, 25),
        entry_price_est=52.9,
        avg_dollar_volume=10_000_000.0,
        atr_14=1.2,
        score_bucket="high",
    )
    plan = PlannedOrder(
        candidate=candidate,
        shares=100,
        entry_price_limit=52.9,
        stop_price=50.0,
        target_price=60.0,
        risk_dollars=290.0,
    )
    position = OpenPosition(
        position_id="pos-1",
        plan=plan,
        entry_date=dt.date(2026, 3, 25),
        entry_price=52.9,
        entry_fill_slippage_bps=10.0,
        current_stop=50.0,
        target_price=60.0,
        peak_price=53.5,
        shares_open=100,
        shares_total=100,
    )
    store = SnapshotStore(
        candidates_by_exec_date={dt.date(2026, 3, 31): [{"event_id": "evt"}]},
        bars_by_symbol_date={
            "ENB": {
                dt.date(2026, 3, 31): {"close": 53.75},
                dt.date(2026, 4, 1): {"close": 54.25},
            }
        },
        macro_by_date={
            dt.date(2026, 3, 31): {"spy_close": 560.0},
            dt.date(2026, 4, 1): {"spy_close": 562.0},
        },
    )

    runner = BacktestRunner.__new__(BacktestRunner)
    runner.store = store
    runner.config = _make_config()
    runner._open_positions = [position]
    runner._closed_trades = []
    runner._candidate_map = {}
    runner._realized_pnl = 0.0
    runner._cash = 0.0

    runner._force_close_all(dt.date(2026, 4, 1), reason="end_of_backtest")

    assert not runner._open_positions
    assert len(runner._closed_trades) == 1
    trade = runner._closed_trades[0]
    assert trade.exit_date == dt.date(2026, 4, 1)
    assert trade.exit_reason == ExitReason.END_OF_BACKTEST
    assert trade.exit_price > 54.0
