"""Integration tests for the full backtest pipeline (no DB/HTTP — uses SnapshotStore directly)."""
from __future__ import annotations

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

import pytest

_UTC = ZoneInfo("UTC")


def _build_synthetic_store() -> object:
    """Build a SnapshotStore with synthetic data for end-to-end testing."""
    from libs.backtest.snapshot_store import SnapshotStore

    # 5 trading days, 2 symbols
    dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8, 9]]

    candidates = {
        dt.date(2026, 1, 5): [
            {
                "event_id": "EVT::001",
                "symbol": "AAPL",
                "execution_date": dt.date(2026, 1, 5),
                "entry_date": "2026-01-05",
                "entry_price": 150.0,
                "score": 0.85,
                "sector": "Technology",
                "event_type": "earnings",
                "event_timestamp": "2026-01-02T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-02",
                "avg_dollar_volume": 5_000_000.0,
                "atr_14": 3.0,
            },
        ],
        dt.date(2026, 1, 6): [
            {
                "event_id": "EVT::002",
                "symbol": "MSFT",
                "execution_date": dt.date(2026, 1, 6),
                "entry_date": "2026-01-06",
                "entry_price": 300.0,
                "score": 0.70,
                "sector": "Technology",
                "event_type": "guidance",
                "event_timestamp": "2026-01-05T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-05",
                "avg_dollar_volume": 10_000_000.0,
                "atr_14": 5.0,
            },
        ],
    }

    bars = {
        "AAPL": {
            dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 150.0, "high": 160.0, "low": 148.0, "close": 158.0, "volume": 1_000_000},
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 158.0, "high": 170.0, "low": 155.0, "close": 165.0, "volume": 900_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 165.0, "high": 175.0, "low": 160.0, "close": 170.0, "volume": 800_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 170.0, "high": 180.0, "low": 165.0, "close": 175.0, "volume": 750_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 175.0, "high": 185.0, "low": 170.0, "close": 180.0, "volume": 700_000},
        },
        "MSFT": {
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 300.0, "high": 305.0, "low": 280.0, "close": 282.0, "volume": 500_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 282.0, "high": 290.0, "low": 270.0, "close": 272.0, "volume": 480_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 272.0, "high": 280.0, "low": 260.0, "close": 265.0, "volume": 450_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 265.0, "high": 270.0, "low": 255.0, "close": 258.0, "volume": 420_000},
        },
    }

    return SnapshotStore(
        candidates_by_exec_date=candidates,
        bars_by_symbol_date=bars,
    )


def _build_multi_engine_store() -> object:
    from libs.backtest.snapshot_store import SnapshotStore

    candidates = {
        dt.date(2026, 1, 7): [
            {
                "event_id": "EVT::SD::SHORT",
                "symbol": "NFLX",
                "execution_date": dt.date(2026, 1, 7),
                "entry_date": "2026-01-07",
                "event_date": "2026-01-06",
                "event_close": 400.0,
                "entry_price": 399.0,
                "score": 0.90,
                "sector": "Communication Services",
                "event_type": "earnings_release",
                "event_timestamp": "2026-01-06T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-06",
                "reaction_day_return": -0.12,
                "avg_dollar_volume": 8_000_000.0,
                "atr_14": 4.0,
            },
            {
                "event_id": "EVT::AC::LONG",
                "symbol": "AMD",
                "execution_date": dt.date(2026, 1, 7),
                "entry_date": "2026-01-07",
                "event_date": "2026-01-06",
                "event_close": 122.0,
                "entry_price": 123.0,
                "score": 0.88,
                "sector": "Technology",
                "event_type": "earnings_release",
                "event_timestamp": "2026-01-06T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-07",
                "reaction_day_return": 0.14,
                "avg_dollar_volume": 9_000_000.0,
                "atr_14": 3.0,
            },
        ],
    }

    bars = {
        "NFLX": {
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 395.0, "high": 405.0, "low": 390.0, "close": 400.0, "volume": 1_200_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 390.0, "high": 392.0, "low": 380.0, "close": 382.0, "volume": 1_100_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 382.0, "high": 384.0, "low": 370.0, "close": 372.0, "volume": 1_000_000},
        },
        "AMD": {
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 123.0, "high": 130.0, "low": 122.0, "close": 129.0, "volume": 1_500_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 129.0, "high": 135.0, "low": 128.0, "close": 134.0, "volume": 1_300_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 134.0, "high": 138.0, "low": 133.0, "close": 137.0, "volume": 1_250_000},
        },
    }

    return SnapshotStore(candidates_by_exec_date=candidates, bars_by_symbol_date=bars)


def _make_config(strategy_engines=None):
    from libs.backtest.domain import (
        BacktestConfig,
        ExecutionConfig,
        ReportingConfig,
        RiskConfig,
        SignalConfig,
        UniverseConfig,
    )

    return BacktestConfig(
        strategy_name="test_strategy",
        dataset_snapshot_id="test_snapshot",
        universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
        signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
        risk=RiskConfig(
            per_trade_risk_pct=0.01,
            max_daily_new_risk_pct=0.05,
            max_positions=10,
            max_positions_per_sector=5,
        ),
        execution=ExecutionConfig(
            entry_fill_model="next_open",
            exit_fill_model="daily_bar_approximation",
            slippage_bps_base=10.0,
            commission_per_share=0.005,
            same_bar_priority="stop_first_conservative",
            max_holding_days=10,
        ),
        reporting=ReportingConfig(
            write_trade_blotter=True,
            write_equity_curve=True,
            write_metrics_summary=True,
            generate_plots=False,
        ),
        strategy_engines=strategy_engines or [],
    )


@pytest.mark.integration
class TestBacktestRunIntegration:
    def test_run_completes(self, tmp_path):
        """Full run completes without error."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        assert result.run_id.startswith("bt_")
        assert result.total_trading_days >= 0
        assert result.metrics.trade_count >= 0

    def test_output_files_created(self, tmp_path):
        """All expected output files are written."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        assert run_dir.exists()
        assert (run_dir / "metadata.json").exists()
        assert (run_dir / "manifest.json").exists()
        assert (run_dir / "resolved_config.json").exists()
        assert (run_dir / "metrics" / "metrics_summary.json").exists()
        assert (run_dir / "plots").exists()  # empty dir

    def test_equity_curve_has_all_days(self, tmp_path):
        """Equity curve has one entry per candidate date."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store)
        result = runner.run()

        # Should have simulated days covering the range (all_trading_days between
        # first and last execution date), plus the initial equity state
        assert result.total_trading_days >= 2

    def test_deterministic_results(self, tmp_path):
        """Two runs with same inputs produce identical metrics."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        store1 = _build_synthetic_store()
        runner1 = BacktestRunner(manifest=manifest, config=config, store=store1)
        result1 = runner1.run()

        store2 = _build_synthetic_store()
        runner2 = BacktestRunner(manifest=manifest, config=config, store=store2)
        result2 = runner2.run()

        assert result1.metrics.trade_count == result2.metrics.trade_count
        assert result1.metrics.win_rate == result2.metrics.win_rate
        assert result1.metrics.total_return_pct == result2.metrics.total_return_pct
        assert result1.total_candidates_seen == result2.total_candidates_seen
        assert result1.total_orders_rejected == result2.total_orders_rejected

    def test_no_future_data_used(self, tmp_path):
        """Candidates for day D should not appear in a simulation of day D-1."""
        from libs.backtest.snapshot_store import SnapshotStore

        candidates = {
            dt.date(2026, 1, 5): [
                {
                    "event_id": "EVT::001",
                    "symbol": "AAPL",
                    "execution_date": dt.date(2026, 1, 5),
                    "entry_date": "2026-01-05",
                    "entry_price": 150.0,
                    "score": 0.85,
                    "sector": "Technology",
                    "event_type": "earnings",
                    "event_timestamp": "2026-01-02T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": "2026-01-02",
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 3.0,
                }
            ],
            dt.date(2026, 1, 6): [
                {
                    "event_id": "EVT::FUTURE",
                    "symbol": "FUTURE_TICKER",
                    "execution_date": dt.date(2026, 1, 6),
                    "entry_date": "2026-01-06",
                    "entry_price": 50.0,
                    "score": 0.99,
                    "sector": "Technology",
                    "event_type": "earnings",
                    "event_timestamp": "2026-01-05T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": "2026-01-05",
                    "avg_dollar_volume": 1_000_000.0,
                    "atr_14": 1.0,
                }
            ],
        }
        bars = {
            "AAPL": {dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 150.0, "high": 160.0, "low": 148.0, "close": 158.0, "volume": 1_000_000}},
            "FUTURE_TICKER": {dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 50.0, "high": 55.0, "low": 48.0, "close": 52.0, "volume": 500_000}},
        }
        store = SnapshotStore(candidates_by_exec_date=candidates, bars_by_symbol_date=bars)

        # Querying Jan 5 should NOT return FUTURE_TICKER candidate
        rows = store.get_candidates_for_date(dt.date(2026, 1, 5))
        symbols = [r["symbol"] for r in rows]
        assert "FUTURE_TICKER" not in symbols
        assert "AAPL" in symbols

    def test_multi_engine_run_writes_per_engine_metrics(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        import json
        import pyarrow.parquet as pq

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_v2",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="earnings_same_day_short_v1",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="short_only",
                    entry_timing_policy="next_open",
                    max_holding_days=3,
                    engine_risk_budget_pct=0.40,
                ),
                StrategyEngineConfig(
                    engine_id="earnings_after_close_long_v1",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.25,
                ),
                StrategyEngineConfig(
                    engine_id="earnings_after_close_short_v1",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="short_only",
                    entry_timing_policy="next_open",
                    max_holding_days=3,
                    engine_risk_budget_pct=0.10,
                    shadow_only=True,
                ),
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        per_engine_metrics = run_dir / "metrics" / "per_engine_metrics.json"
        attribution_by_engine = run_dir / "metrics" / "attribution_by_engine.csv"

        assert per_engine_metrics.exists()
        assert attribution_by_engine.exists()

        payload = per_engine_metrics.read_text()
        assert "earnings_same_day_short_v1" in payload
        assert "earnings_after_close_long_v1" in payload
        assert "earnings_after_close_short_v1" in payload
        assert result.metrics.trade_count >= 1

        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        engine_ids = {row["engine_id"] for row in trade_blotter}
        assert "earnings_after_close_short_v1" not in engine_ids

        equity_curve = pq.read_table(run_dir / "artifacts" / "daily_equity_curve.parquet").to_pylist()
        assert any(float(row["net_exposure"]) < 0 for row in equity_curve if float(row["gross_exposure"]) > 0)

        metrics_summary = json.loads((run_dir / "metrics" / "metrics_summary.json").read_text())
        assert "avg_gross_exposure_pct" in metrics_summary
        assert "avg_net_exposure_pct" in metrics_summary
        assert "days_in_market_pct" in metrics_summary

    def test_engine_execution_overrides_flow_into_effective_execution_config(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import Candidate, ExperimentManifest, StrategyEngineConfig

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_exec_overrides",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="earnings_same_day_long_trend_v1",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=12,
                    engine_risk_budget_pct=0.25,
                    target_atr_multiplier_override=2.5,
                    target_1_fraction_override=0.33,
                    trailing_model_override="pct_10",
                    trailing_warmup_days_override=2,
                ),
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)

        candidate = Candidate(
            event_id="EVT::SD::LONG",
            symbol="AMD",
            issuer_id="ISSUER::AMD",
            score=0.92,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=122.0,
            avg_dollar_volume=9_000_000.0,
            atr_14=3.0,
            score_bucket="high",
            engine_id="earnings_same_day_long_trend_v1",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            engine_max_holding_days=12,
            engine_risk_budget_pct=0.25,
            engine_target_atr_multiplier=2.5,
            engine_target_1_fraction=0.33,
            engine_trailing_model="pct_10",
            engine_trailing_warmup_days=2,
            trade_direction="long",
        )
        effective_exec = runner._build_effective_execution_config(candidate)

        assert candidate.engine_id == "earnings_same_day_long_trend_v1"
        assert effective_exec.max_holding_days == 12
        assert effective_exec.target_atr_multiplier == pytest.approx(2.5)
        assert effective_exec.target_1_fraction == pytest.approx(0.33)
        assert effective_exec.trailing_model == "pct_10"
        assert effective_exec.trailing_warmup_days == 2

    def test_attention_gate_filters_engine_candidates(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        from libs.backtest.snapshot_store import SnapshotStore
        from libs.oracle_client.models import EntityInfo, EventAttentionResponse, NewsFeatures, WikiFeatures

        date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::ATTN::PASS",
                        "symbol": "PASS",
                        "execution_date": date,
                        "entry_date": "2026-01-07",
                        "event_date": "2026-01-06",
                        "event_close": 100.0,
                        "gap_size": 0.12,
                        "entry_price": 100.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": -0.12,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                    {
                        "event_id": "EVT::ATTN::FAIL",
                        "symbol": "FAIL",
                        "execution_date": date,
                        "entry_date": "2026-01-07",
                        "event_date": "2026-01-06",
                        "event_close": 101.0,
                        "gap_size": 0.11,
                        "entry_price": 101.0,
                        "score": 0.89,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": -0.11,
                        "avg_dollar_volume": 7_500_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={
                "PASS": {date: {"date": date, "open": 100.0, "high": 100.0, "low": 95.0, "close": 96.0, "volume": 1_000_000}},
                "FAIL": {date: {"date": date, "open": 101.0, "high": 102.0, "low": 98.0, "close": 99.0, "volume": 900_000}},
            },
        )
        manifest = ExperimentManifest(
            experiment_name="attention_gate_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_short_attention",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="short_only",
                    attention_min_wiki_spike_10d=1.2,
                )
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store)

        def _fake_attention(candidate):
            spike = 1.5 if candidate.symbol == "PASS" else 0.9
            return EventAttentionResponse(
                ticker=candidate.symbol,
                event_date="2026-01-06",
                entity=EntityInfo(
                    ticker=candidate.symbol,
                    canonical_name=candidate.symbol,
                    resolver_confidence=0.9,
                ),
                wiki=WikiFeatures(spike_10d=spike, zscore_20d=1.0),
                news=NewsFeatures(),
                metadata={},
            )

        runner._get_event_attention = _fake_attention  # type: ignore[method-assign]
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["PASS"]
        assert selected[0].features["attention_wiki_spike_10d"] == pytest.approx(1.5)

    def test_attention_max_gate_allows_missing_payload(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        from libs.backtest.snapshot_store import SnapshotStore

        date = dt.date(2026, 1, 6)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::ATTN::KNOWN",
                        "symbol": "KNOWN",
                        "execution_date": date,
                        "entry_date": "2026-01-06",
                        "event_date": "2026-01-06",
                        "event_close": 100.0,
                        "gap_size": 0.12,
                        "entry_price": 100.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": 0.15,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                    {
                        "event_id": "EVT::ATTN::MISSING",
                        "symbol": "MISSING",
                        "execution_date": date,
                        "entry_date": "2026-01-06",
                        "event_date": "2026-01-06",
                        "event_close": 101.0,
                        "gap_size": 0.11,
                        "entry_price": 101.0,
                        "score": 0.89,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": 0.14,
                        "avg_dollar_volume": 7_500_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={
                "KNOWN": {date: {"date": date, "open": 100.0, "high": 105.0, "low": 99.0, "close": 103.0, "volume": 1_000_000}},
                "MISSING": {date: {"date": date, "open": 101.0, "high": 104.0, "low": 100.0, "close": 102.0, "volume": 900_000}},
            },
        )
        manifest = ExperimentManifest(
            experiment_name="attention_max_gate_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_long_attention_cap",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    gap_size_min=0.10,
                    attention_max_wiki_spike_10d=1.5,
                )
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store)

        def _fake_attention(candidate):
            if candidate.symbol == "KNOWN":
                from libs.oracle_client.models import EntityInfo, EventAttentionResponse, NewsFeatures, WikiFeatures

                return EventAttentionResponse(
                    ticker=candidate.symbol,
                    event_date="2026-01-06",
                    entity=EntityInfo(
                        ticker=candidate.symbol,
                        canonical_name=candidate.symbol,
                        resolver_confidence=0.9,
                    ),
                    wiki=WikiFeatures(spike_10d=1.4, zscore_20d=0.5),
                    news=NewsFeatures(),
                    metadata={},
                )
            return None

        runner._get_event_attention = _fake_attention  # type: ignore[method-assign]
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["KNOWN", "MISSING"]
        assert selected[0].features["attention_wiki_spike_10d"] == pytest.approx(1.4)
        assert "attention_wiki_spike_10d" not in selected[1].features
