"""Unit tests for libs/backtest/domain.py."""
from __future__ import annotations

import datetime as dt
from zoneinfo import ZoneInfo

import pytest
from pydantic import ValidationError

from libs.backtest.domain import (
    BacktestConfig,
    Candidate,
    DailyPortfolioState,
    EventTypeProfile,
    ExitReason,
    ExecutionConfig,
    ExperimentManifest,
    FilledTrade,
    MetricsBundle,
    OpenPosition,
    PlannedOrder,
    PositionStatus,
    ReportingConfig,
    RiskConfig,
    SignalConfig,
    StrategyEngineConfig,
    UniverseConfig,
)

_UTC = ZoneInfo("UTC")
_NOW = dt.datetime(2026, 1, 5, 14, 30, tzinfo=_UTC)
_TODAY = dt.date(2026, 1, 5)
_TOMORROW = dt.date(2026, 1, 6)


def _make_candidate(**kwargs) -> Candidate:
    defaults = dict(
        event_id="EVT::DOC::TEST::earnings::0",
        symbol="AAPL",
        issuer_id="ISSUER::0000320193",
        score=0.75,
        sector="Technology",
        event_type="earnings",
        event_timestamp=_NOW,
        filing_time_bucket="post_market",
        reaction_date=_TODAY,
        execution_date=_TOMORROW,
        entry_price_est=150.0,
        avg_dollar_volume=5_000_000.0,
        atr_14=3.5,
        score_bucket="high",
    )
    defaults.update(kwargs)
    return Candidate(**defaults)


def _make_filled_trade(**kwargs) -> FilledTrade:
    defaults = dict(
        trade_id="t1",
        position_id="p1",
        event_id="EVT::TEST",
        symbol="AAPL",
        entry_date=_TODAY,
        exit_date=_TOMORROW,
        entry_price=150.0,
        exit_price=160.0,
        exit_reason=ExitReason.TARGET,
        shares=10,
        commission=0.10,
        slippage_bps=10.0,
        gross_pnl=100.0,
        net_pnl=99.9,
        pnl_pct=0.0667,
        r_multiple=2.0,
        holding_days=1,
    )
    defaults.update(kwargs)
    return FilledTrade(**defaults)


class TestPositionStatus:
    def test_values(self):
        assert PositionStatus.PLANNED == "PLANNED"
        assert PositionStatus.CLOSED == "CLOSED"

    def test_all_statuses(self):
        expected = {"PLANNED", "ENTERED", "PARTIALLY_EXITED", "OPEN", "EXIT_PENDING", "CLOSED", "ARCHIVED"}
        assert {s.value for s in PositionStatus} == expected


class TestExitReason:
    def test_values(self):
        assert ExitReason.STOP == "STOP"
        assert ExitReason.TARGET == "TARGET"
        assert ExitReason.TIME == "TIME"
        assert ExitReason.TRAILING == "TRAILING"
        assert ExitReason.KILL_SWITCH == "KILL_SWITCH"
        assert ExitReason.MISSING_BAR == "MISSING_BAR"


class TestCandidate:
    def test_basic_creation(self):
        c = _make_candidate()
        assert c.symbol == "AAPL"
        assert c.score == 0.75
        assert c.event_timestamp.tzinfo is not None

    def test_frozen(self):
        c = _make_candidate()
        with pytest.raises(Exception):  # frozen model
            c.score = 0.9

    def test_timezone_aware_timestamp(self):
        c = _make_candidate(event_timestamp=dt.datetime(2026, 1, 5, 20, 0, tzinfo=_UTC))
        assert c.event_timestamp.tzinfo is not None

    def test_features_default_empty(self):
        c = _make_candidate()
        assert c.features == {}

    def test_features_stored(self):
        c = _make_candidate(features={"foo": 1.0, "bar": "baz"})
        assert c.features["foo"] == 1.0


class TestFilledTrade:
    def test_basic(self):
        t = _make_filled_trade()
        assert t.net_pnl == 99.9
        assert t.exit_reason == ExitReason.TARGET

    def test_frozen(self):
        t = _make_filled_trade()
        with pytest.raises(Exception):
            t.net_pnl = 0.0

    def test_stop_exit_reason(self):
        t = _make_filled_trade(exit_reason=ExitReason.STOP, net_pnl=-50.0)
        assert t.exit_reason == ExitReason.STOP


class TestOpenPosition:
    def test_mutable(self):
        c = _make_candidate()
        plan = PlannedOrder(
            candidate=c,
            shares=10,
            entry_price_limit=150.0,
            stop_price=144.0,
            target_price=162.0,
            risk_dollars=60.0,
        )
        pos = OpenPosition(
            position_id="p1",
            plan=plan,
            entry_date=_TOMORROW,
            entry_price=150.5,
            entry_fill_slippage_bps=10.0,
            current_stop=144.0,
            target_price=162.0,
            peak_price=150.5,
            shares_open=10,
            shares_total=10,
        )
        # Should be mutable
        pos.days_held = 3
        assert pos.days_held == 3
        pos.current_stop = 146.0
        assert pos.current_stop == 146.0


class TestDailyPortfolioState:
    def test_basic(self):
        s = DailyPortfolioState(
            date=_TODAY,
            equity=100_000.0,
            cash_available=90_000.0,
            gross_exposure=10_000.0,
            net_exposure=10_000.0,
            reserved_risk_budget=1_000.0,
            unrealized_pnl=500.0,
            realized_pnl=200.0,
            open_positions=["p1"],
            daily_new_risk_used=500.0,
            peak_equity=100_500.0,
            current_drawdown_pct=0.5,
        )
        assert s.equity == 100_000.0
        assert len(s.open_positions) == 1


class TestMetricsBundle:
    def test_defaults(self):
        m = MetricsBundle()
        assert m.trade_count == 0
        assert m.win_rate is None
        assert m.score_bucket_hit_rate == {}

    def test_with_values(self):
        m = MetricsBundle(trade_count=10, win_rate=0.6, total_return_pct=15.0)
        assert m.trade_count == 10
        assert m.win_rate == 0.6


class TestConfigModels:
    def test_universe_config_defaults(self):
        u = UniverseConfig()
        assert u.min_price == 5.0
        assert u.exclude_asset_types == []

    def test_risk_config(self):
        r = RiskConfig(
            per_trade_risk_pct=0.01,
            max_daily_new_risk_pct=0.03,
            max_positions=10,
            max_positions_per_sector=3,
        )
        assert r.per_trade_risk_pct == 0.01

    def test_backtest_config(self):
        cfg = BacktestConfig(strategy_name="test", dataset_snapshot_id="snap_001")
        assert cfg.strategy_name == "test"
        assert isinstance(cfg.risk, RiskConfig)
        assert isinstance(cfg.execution, ExecutionConfig)

    def test_execution_config_target_model(self):
        e = ExecutionConfig(target_model="atr_multiple", target_atr_multiplier=2.0)
        assert e.target_model == "atr_multiple"
        assert e.target_atr_multiplier == 2.0

    def test_execution_config_defaults(self):
        e = ExecutionConfig()
        assert e.target_model == "fixed_r"
        assert e.target_atr_multiplier == 1.5
        assert e.target_1_fraction is None

    def test_experiment_manifest(self):
        m = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="snap_001",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        assert m.experiment_name == "test_exp"
        assert m.splits == []


class TestEventTypeProfile:
    def test_defaults(self):
        p = EventTypeProfile()
        assert p.enabled is True
        assert p.score_threshold_override is None
        assert p.direction_filter == "any"

    def test_disabled(self):
        p = EventTypeProfile(enabled=False)
        assert p.enabled is False

    def test_overrides(self):
        p = EventTypeProfile(
            max_holding_days_override=15,
            stop_atr_multiplier_override=2.5,
            target_atr_multiplier_override=1.0,
        )
        assert p.max_holding_days_override == 15

    def test_backtest_config_with_profiles(self):
        cfg = BacktestConfig(
            strategy_name="test",
            dataset_snapshot_id="snap_001",
            event_type_profiles={
                "earnings_release": EventTypeProfile(max_holding_days_override=15),
                "management_change": EventTypeProfile(enabled=False),
            },
        )
        p = cfg.get_event_profile("earnings_release")
        assert p is not None
        assert p.max_holding_days_override == 15
        assert cfg.get_event_profile("unknown") is None

    def test_backtest_config_default_empty_profiles(self):
        cfg = BacktestConfig(strategy_name="test", dataset_snapshot_id="snap_001")
        assert cfg.event_type_profiles == {}
        assert cfg.get_event_profile("anything") is None

    def test_backtest_config_strategy_engines_helpers(self):
        cfg = BacktestConfig(
            strategy_name="test",
            dataset_snapshot_id="snap_001",
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="active_engine",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                ),
                StrategyEngineConfig(
                    engine_id="shadow_engine",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="short_only",
                    shadow_only=True,
                ),
            ],
        )
        assert [engine.engine_id for engine in cfg.get_strategy_engines()] == [
            "active_engine",
            "shadow_engine",
        ]
        assert [engine.engine_id for engine in cfg.get_active_strategy_engines()] == [
            "active_engine",
        ]
        assert [engine.engine_id for engine in cfg.get_shadow_strategy_engines()] == [
            "shadow_engine",
        ]

    def test_backtest_config_resolves_strategy_engine_inheritance(self):
        cfg = BacktestConfig(
            strategy_name="test",
            dataset_snapshot_id="snap_001",
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="parent_core",
                    event_types=["guidance_update"],
                    filing_time_buckets=["post_market"],
                    close_location_min=0.8,
                    entry_timing_policy="next_open",
                ),
                StrategyEngineConfig(
                    engine_id="child_core",
                    inherits_from_engine_id="parent_core",
                    close_location_min=0.9,
                ),
            ],
        )

        engines = {engine.engine_id: engine for engine in cfg.get_strategy_engines()}
        child = engines["child_core"]
        assert child.event_types == ["guidance_update"]
        assert child.filing_time_buckets == ["post_market"]
        assert child.close_location_min == pytest.approx(0.9)
        assert child.entry_timing_policy == "next_open"

    def test_backtest_config_strategy_engines_respect_selection_priority(self):
        cfg = BacktestConfig(
            strategy_name="test",
            dataset_snapshot_id="snap_001",
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="base_engine",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                ),
                StrategyEngineConfig(
                    engine_id="proxy_engine",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                    selection_priority=100,
                ),
                StrategyEngineConfig(
                    engine_id="shadow_engine",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                    shadow_only=True,
                    selection_priority=50,
                ),
            ],
        )

        assert [engine.engine_id for engine in cfg.get_strategy_engines()] == [
            "proxy_engine",
            "shadow_engine",
            "base_engine",
        ]
        assert [engine.engine_id for engine in cfg.get_active_strategy_engines()] == [
            "proxy_engine",
            "base_engine",
        ]
        assert [engine.engine_id for engine in cfg.get_shadow_strategy_engines()] == [
            "shadow_engine",
        ]

    def test_strategy_engine_config_supports_engine_specific_thresholds(self):
        engine = StrategyEngineConfig(
            engine_id="after_close_long_quality",
            score_threshold_override=0.75,
            pead_reaction_threshold_override=0.12,
            pead_volume_threshold_override=3.0,
            reaction_day_return_min=-0.45,
            reaction_day_return_max=0.35,
            gap_size_min=0.05,
            gap_size_max=0.30,
        )
        assert engine.score_threshold_override == 0.75
        assert engine.pead_reaction_threshold_override == 0.12
        assert engine.pead_volume_threshold_override == 3.0
        assert engine.reaction_day_return_min == -0.45
        assert engine.reaction_day_return_max == 0.35
        assert engine.gap_size_min == 0.05
        assert engine.gap_size_max == 0.30


class TestMetricsBundleBootstrap:
    def test_bootstrap_cis_field(self):
        m = MetricsBundle(bootstrap_cis={"win_rate_ci_95": (0.2, 0.6)})
        assert m.bootstrap_cis["win_rate_ci_95"] == (0.2, 0.6)

    def test_bootstrap_cis_default_empty(self):
        m = MetricsBundle()
        assert m.bootstrap_cis == {}
