from __future__ import annotations

import json
from pathlib import Path

import pytest

from apps.web.routers import intraday


MOMENTUM_CONFIG = """\
_meta:
  name: Leader Intraday Momentum
  description: Test momentum config
  id: 7
strategy_mode: momentum
strategy:
  entry_minutes_after_open: 10
  exit_minutes_before_close: 5
  stop_loss_pct: null
  trailing_stop_pct: -0.06
  min_morning_gain_pct: 0.02
  max_morning_gain_pct: null
  min_entry_volume: 250000
  ticker_cooldown_days: 0
  top_n: 5
  initial_capital: 10000.0
  slippage_bps: 5.0
  market_regime_spy_threshold: null
universe:
  source: midcap
  min_price: 10.0
backtest:
  lookback_trading_days: 200
  pre_screen_threshold: 0.02
cache:
  enabled: true
  dir: data/cache/intraday
output:
  dir: runs/intraday
  verbose: false
"""

MOMENTUM_YAML_UNIVERSE_CONFIG = """\
_meta:
  name: Leader Intraday Momentum
  description: Test momentum config
  id: 7
strategy_mode: momentum
strategy:
  entry_minutes_after_open: 10
  exit_minutes_before_close: 5
  stop_loss_pct: null
  trailing_stop_pct: -0.06
  min_morning_gain_pct: 0.02
  max_morning_gain_pct: null
  min_entry_volume: 250000
  ticker_cooldown_days: 0
  top_n: 5
  initial_capital: 10000.0
  slippage_bps: 5.0
  market_regime_spy_threshold: null
universe:
  source: yaml
  symbols_file: configs/symbols_midcap_smallmid.yaml
  min_price: 10.0
backtest:
  lookback_trading_days: 200
  pre_screen_threshold: 0.02
cache:
  enabled: true
  dir: data/cache/intraday
output:
  dir: runs/intraday
verbose: false
"""

ORB_ADVANCED_CONFIG = """\
_meta:
  name: ORB Advanced
  description: Advanced ORB config
  id: 42
strategy_mode: orb
orb_strategy:
  orb_minutes: 5
  sim_bar_minutes: 5
  entry_direction: long_only
  order_timeout_minutes: 45
  min_price: 15.0
  min_avg_dollar_volume: 40000000
  min_premarket_dollar_vol: 2500000
  min_breakout_rel_vol: 1.2
  min_atr_14: 0.50
  min_rvol: 1.5
  max_candidates: 20
  risk_per_trade_pct: 0.05
  max_position_pct: 0.70
  atr_stop_multiplier: 0.75
  breakeven_at_r: 1.0
  trailing_at_r: 1.0
  trailing_stop_atr_multiplier: 0.8
  daily_max_loss_pct: 0.05
  max_stops_per_day: 5
  settlement_days: 1
  max_gap_pct: 0.04
  compound_returns: false
  daily_budget_reset: true
  daily_bar_snapshot_id: orb_daily_v46_20260423
  prior_event_snapshot_id: orb_pead_d10_v46_20260423
universe:
  source: broad
backtest:
  lookback_trading_days: 200
cache:
  enabled: true
  dir: data/cache/intraday
output:
  dir: runs/intraday_orb
  verbose: false
"""

ORB_EXTENDS_CHILD_CONFIG = """\
extends: orb_advanced_base.yaml
_meta:
  name: ORB Extends Child
  description: Child config
  id: 43
orb_strategy:
  min_rvol: 3.0
"""


def _write_config(tmp_path: Path, slug: str, text: str = MOMENTUM_CONFIG) -> Path:
    strategies_dir = tmp_path / "configs" / "intraday" / "strategies"
    strategies_dir.mkdir(parents=True, exist_ok=True)
    path = strategies_dir / f"{slug}.yaml"
    path.write_text(text)
    return path


def test_load_user_strategy_supports_momentum(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "leader_intraday_momentum")
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    payload = intraday._load_user_strategy("leader_intraday_momentum")

    assert payload is not None
    assert payload["strategy_mode"] == "momentum"
    assert payload["output_dir"] == "runs/intraday"
    assert payload["top_n"] == 5
    assert payload["min_morning_gain_pct"] == 0.02


def test_load_user_strategy_exposes_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    payload = intraday._load_user_strategy("orb_advanced")

    assert payload is not None
    assert payload["strategy_mode"] == "orb"
    assert payload["universe"] == "broad"
    assert payload["min_price"] == 15.0
    assert payload["min_avg_dollar_volume"] == 40000000
    assert payload["min_premarket_dollar_vol"] == 2500000
    assert payload["min_breakout_rel_vol"] == 1.2
    assert payload["daily_budget_reset"] is True


def test_create_comp_strategy_materializes_deployment_overlay(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    generated = intraday.create_comp_strategy("orb_advanced")
    generated_path = tmp_path / "configs" / "intraday" / "strategies" / f"{generated['slug']}.yaml"
    raw = intraday.yaml.safe_load(generated_path.read_text())

    assert generated["slug"] == "orb_advanced_comp"
    assert generated["name"] == "ORB Advanced Comp"
    assert generated["compound_returns"] is True
    assert generated["daily_budget_reset"] is False
    assert generated["single_trade_loss_cap_basis"] == "equity"
    assert generated["is_comp_equity_deployment"] is True
    assert raw["orb_strategy"]["compound_returns"] is True
    assert raw["orb_strategy"]["daily_budget_reset"] is False
    assert raw["orb_strategy"]["single_trade_loss_cap_basis"] == "equity"
    assert raw["_meta"]["parent"] is None
    assert raw["_meta"]["deployment_kind"] == "compound_equity_losscap"
    assert raw["_meta"]["source_slug"] == "orb_advanced"


def test_load_strategy_yaml_rejects_extends(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG)
    _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    with pytest.raises(ValueError, match="extends.*no longer supported"):
        intraday._load_strategy_yaml_standalone(
            tmp_path / "configs" / "intraday" / "strategies" / "orb_extends_child.yaml"
        )


def test_create_comp_strategy_refreshes_existing_generated_variant(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    first = intraday.create_comp_strategy("orb_advanced")
    first_path = tmp_path / "configs" / "intraday" / "strategies" / f"{first['slug']}.yaml"
    raw = intraday.yaml.safe_load(first_path.read_text())
    raw["orb_strategy"]["min_rvol"] = 9.9
    first_path.write_text(intraday.yaml.dump(raw, default_flow_style=False, sort_keys=False))

    second = intraday.create_comp_strategy("orb_advanced")
    refreshed = intraday.yaml.safe_load(first_path.read_text())

    assert second["slug"] == first["slug"]
    assert second["id"] == first["id"]
    assert refreshed["orb_strategy"]["min_rvol"] == 1.5
    assert refreshed["orb_strategy"]["single_trade_loss_cap_basis"] == "equity"


def test_load_user_strategy_does_not_load_extends(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG)
    _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    payload = intraday._load_user_strategy("orb_extends_child")

    assert payload is None


def test_update_strategy_preserves_unedited_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None:
    path = _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    updated = intraday.update_strategy(
        "orb_advanced",
        intraday.UpdateStrategyRequest(name="ORB Advanced Updated"),
    )
    raw = intraday.yaml.safe_load(path.read_text())

    assert updated["name"] == "ORB Advanced Updated"
    assert raw["orb_strategy"]["min_price"] == 15.0
    assert raw["orb_strategy"]["min_avg_dollar_volume"] == 40000000
    assert raw["orb_strategy"]["min_premarket_dollar_vol"] == 2500000
    assert raw["orb_strategy"]["daily_bar_snapshot_id"] == "orb_daily_v46_20260423"
    assert raw["orb_strategy"]["prior_event_snapshot_id"] == "orb_pead_d10_v46_20260423"


def test_copy_strategy_preserves_orb_advanced_fields(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    copied = intraday.copy_strategy("orb_advanced")
    copied_path = tmp_path / "configs" / "intraday" / "strategies" / f"{copied['slug']}.yaml"
    raw = intraday.yaml.safe_load(copied_path.read_text())

    assert copied["name"] == "ORB Advanced (copy)"
    assert raw["orb_strategy"]["min_price"] == 15.0
    assert raw["orb_strategy"]["min_avg_dollar_volume"] == 40000000
    assert raw["orb_strategy"]["min_premarket_dollar_vol"] == 2500000
    assert raw["orb_strategy"]["daily_bar_snapshot_id"] == "orb_daily_v46_20260423"


def test_copy_strategy_rejects_extends(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced_base", ORB_ADVANCED_CONFIG)
    _write_config(tmp_path, "orb_extends_child", ORB_EXTENDS_CHILD_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)

    with pytest.raises(intraday.HTTPException) as exc_info:
        intraday.copy_strategy("orb_extends_child")

    assert exc_info.value.status_code == 400


def test_submit_intraday_backtest_uses_config_strategy_mode(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "leader_intraday_momentum")
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)
    monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs")
    intraday._tasks.clear()
    intraday._tasks_initialized = True

    captured: dict[str, object] = {}

    class DummyProc:
        pid = 4242

    def fake_popen(cmd, stdout=None, stderr=None, cwd=None):  # noqa: ANN001
        captured["cmd"] = cmd
        captured["cwd"] = cwd
        return DummyProc()

    class DummyThread:
        def __init__(self, target=None, args=None, daemon=None):  # noqa: ANN001
            captured["thread_target"] = target
            captured["thread_args"] = args

        def start(self) -> None:
            captured["thread_started"] = True

    monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(intraday.threading, "Thread", DummyThread)

    req = intraday.IntradayBacktestRequest(
        config="leader_intraday_momentum",
        universe="midcap",
        start_date="2026-01-02",
        end_date="2026-03-31",
        compound_returns=False,
    )
    resp = intraday.submit_intraday_backtest(req)

    task = intraday._tasks[resp["task_id"]]
    cmd = captured["cmd"]

    assert task["strategy_mode"] == "momentum"
    assert task["output_dir"] == "runs/intraday"
    assert "--strategy" in cmd
    assert cmd[cmd.index("--strategy") + 1] == "momentum"
    assert "--output-dir" in cmd
    assert cmd[cmd.index("--output-dir") + 1] == str(tmp_path / "runs" / "intraday")


def test_submit_intraday_backtest_preserves_yaml_strategy_universe(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "leader_intraday_momentum_yaml", MOMENTUM_YAML_UNIVERSE_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)
    monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs")
    intraday._tasks.clear()
    intraday._tasks_initialized = True

    captured: dict[str, object] = {}

    class DummyProc:
        pid = 4242

    def fake_popen(cmd, stdout=None, stderr=None, cwd=None):  # noqa: ANN001
        captured["cmd"] = cmd
        captured["cwd"] = cwd
        return DummyProc()

    class DummyThread:
        def __init__(self, target=None, args=None, daemon=None):  # noqa: ANN001
            captured["thread_target"] = target
            captured["thread_args"] = args

        def start(self) -> None:
            captured["thread_started"] = True

    monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(intraday.threading, "Thread", DummyThread)

    req = intraday.IntradayBacktestRequest(
        config="leader_intraday_momentum_yaml",
        universe="yaml",
        start_date="2026-01-02",
        end_date="2026-03-31",
        compound_returns=False,
    )
    resp = intraday.submit_intraday_backtest(req)

    task = intraday._tasks[resp["task_id"]]
    cmd = captured["cmd"]

    assert task["universe"] == "yaml"
    assert task["universe_label"] == "yaml:symbols_midcap_smallmid.yaml"
    assert "--strategy" in cmd
    assert cmd[cmd.index("--strategy") + 1] == "momentum"
    assert "--universe" not in cmd


def test_submit_intraday_backtest_accepts_broad_universe(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "leader_intraday_momentum")
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)
    monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs")
    intraday._tasks.clear()
    intraday._tasks_initialized = True

    captured: dict[str, object] = {}

    class DummyProc:
        pid = 4242

    def fake_popen(cmd, stdout=None, stderr=None, cwd=None):  # noqa: ANN001
        captured["cmd"] = cmd
        captured["cwd"] = cwd
        return DummyProc()

    class DummyThread:
        def __init__(self, target=None, args=None, daemon=None):  # noqa: ANN001
            captured["thread_target"] = target
            captured["thread_args"] = args

        def start(self) -> None:
            captured["thread_started"] = True

    monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(intraday.threading, "Thread", DummyThread)

    req = intraday.IntradayBacktestRequest(
        config="leader_intraday_momentum",
        universe="broad",
        start_date="2026-01-02",
        end_date="2026-03-31",
        compound_returns=False,
    )
    resp = intraday.submit_intraday_backtest(req)

    task = intraday._tasks[resp["task_id"]]
    cmd = captured["cmd"]

    assert task["universe"] == "broad"
    assert task["universe_label"] == "broad"
    assert "--universe" in cmd
    assert cmd[cmd.index("--universe") + 1] == "broad"


def test_submit_intraday_backtest_records_command_and_compound_flags(monkeypatch, tmp_path: Path) -> None:
    _write_config(tmp_path, "orb_advanced", ORB_ADVANCED_CONFIG)
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)
    monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs")
    intraday._tasks.clear()
    intraday._tasks_initialized = True

    captured: dict[str, object] = {}

    class DummyProc:
        pid = 4242

    def fake_popen(cmd, stdout=None, stderr=None, cwd=None):  # noqa: ANN001
        captured["cmd"] = cmd
        captured["cwd"] = cwd
        return DummyProc()

    class DummyThread:
        def __init__(self, target=None, args=None, daemon=None):  # noqa: ANN001
            captured["thread_target"] = target
            captured["thread_args"] = args

        def start(self) -> None:
            captured["thread_started"] = True

    monkeypatch.setattr(intraday.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(intraday.threading, "Thread", DummyThread)

    req = intraday.IntradayBacktestRequest(
        config="orb_advanced",
        universe="midlarge",
        days=200,
        compound_returns=True,
        daily_budget_reset=False,
    )
    resp = intraday.submit_intraday_backtest(req)

    task = intraday._tasks[resp["task_id"]]
    cmd = captured["cmd"]
    log_text = (tmp_path / "runs" / ".intraday_tasks" / f"{resp['task_id']}.log").read_text()

    assert task["command"] == cmd
    assert "--compound-returns" in cmd
    assert "--no-daily-budget-reset" in cmd
    assert "Command:" in log_text
    assert "--compound-returns" in log_text
    assert "--no-daily-budget-reset" in log_text


def test_result_summary_from_file_includes_loss_containment_fields(tmp_path: Path) -> None:
    result_path = tmp_path / "intraday_result.json"
    result_path.write_text(
        """{
  "metrics": {
    "total_return_pct": 0.0688,
    "max_drawdown_pct": -0.1325,
    "sharpe_ratio": 1.23,
    "win_rate": 0.569,
    "total_trades": 116,
    "final_equity": 10688.0,
    "calmar_ratio": 0.52,
    "loss_containment_score": 43.78,
    "avg_loss_day_pct": -0.0168,
    "tail_loss_20_pct": -0.0377,
    "worst_day_return_pct": -0.0672
  }
}"""
    )

    summary = intraday._result_summary_from_file(result_path)

    assert summary is not None
    assert summary["return_pct"] == 0.0688
    assert summary["loss_containment_score"] == 43.78
    assert summary["avg_loss_day_pct"] == -0.0168
    assert summary["tail_loss_20_pct"] == -0.0377
    assert summary["worst_day_return_pct"] == -0.0672


def test_detect_result_file_from_log_prefers_explicit_saved_path(tmp_path: Path) -> None:
    good = tmp_path / "runs" / "intraday_orb" / "intraday_good.json"
    good.parent.mkdir(parents=True, exist_ok=True)
    good.write_text("{}")

    bad = tmp_path / "runs" / "intraday_orb" / "intraday_bad.json"
    bad.write_text("{}")

    log_path = tmp_path / "runs" / ".intraday_tasks" / "task.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_path.write_text(
        "\n".join([
            f"Results saved to: {bad}",
            f"Results saved to: {good}",
        ])
    )

    assert intraday._detect_result_file_from_log(log_path) == good


def test_try_resolve_dead_task_repairs_completed_result_from_log(monkeypatch, tmp_path: Path) -> None:
    monkeypatch.setattr(intraday, "get_project_root", lambda: tmp_path)
    monkeypatch.setattr(intraday, "get_runs_dir", lambda: tmp_path / "runs")
    intraday._tasks.clear()
    intraday._tasks_initialized = True

    results_dir = tmp_path / "runs" / "intraday_orb"
    results_dir.mkdir(parents=True, exist_ok=True)

    wrong_result = results_dir / "intraday_wrong.json"
    wrong_result.write_text(
        json.dumps({"metrics": {"total_return_pct": -0.1177, "max_drawdown_pct": -0.1765}})
    )
    right_result = results_dir / "intraday_right.json"
    right_result.write_text(
        json.dumps({"metrics": {"total_return_pct": 1.302, "max_drawdown_pct": -0.11}})
    )

    task_id = "task-123"
    log_path = tmp_path / "runs" / ".intraday_tasks" / f"{task_id}.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_path.write_text(f"Results saved to: {right_result}\n")

    intraday._tasks[task_id] = {
        "task_id": task_id,
        "status": "completed",
        "pid": None,
        "started_at": "2026-04-22T23:28:35.765827+00:00",
        "finished_at": "2026-04-23T00:33:45.996122+00:00",
        "returncode": None,
        "error": None,
        "result_file": str(wrong_result),
        "result_summary": {"return_pct": -0.1177, "max_dd_pct": -0.1765},
        "output_dir": "runs/intraday_orb",
    }
    intraday._persist_task(intraday._tasks[task_id])

    intraday._try_resolve_dead_task(task_id)

    task = intraday._tasks[task_id]
    persisted = json.loads((tmp_path / "runs" / ".intraday_tasks" / f"{task_id}.task.json").read_text())

    assert task["result_file"] == str(right_result)
    assert task["result_summary"]["return_pct"] == 1.302
    assert task["result_summary"]["max_dd_pct"] == -0.11
    assert task["returncode"] == 0
    assert persisted["result_file"] == str(right_result)
    assert persisted["result_summary"]["return_pct"] == 1.302
