from __future__ import annotations

import json
from datetime import datetime, timezone

from src.tools.paper_runtime_supervisor import CANDIDATES, inspect_candidate


def _proc_fixture(tmp_path, spec, *, pid=123, env_ok=True, command_ok=True, heartbeat_age=0):
    runtime_root = tmp_path / "runtime"
    runtime = runtime_root / spec.strategy_id
    runtime.mkdir(parents=True)
    (runtime / "bot.pid").write_text(str(pid), encoding="utf-8")
    proc_root = tmp_path / "proc"
    proc = proc_root / str(pid)
    proc.mkdir(parents=True)
    command = f"python\x00-m\x00{spec.module}\x00--strategy-id\x00{spec.strategy_id}\x00" if command_ok else "python\x00other\x00"
    (proc / "cmdline").write_bytes(command.encode())
    env = "\x00".join((
        f"CTB_STRATEGY_ID={spec.strategy_id}",
        "CTB_PAPER_TRADING=true",
        "CTB_DRY_RUN=false",
        "CTB_LIVE_TRADING_ALLOWED=false",
        "CTB_LIVE_ORDER_ALLOWED=false",
        "HL_MAINNET_SIGNED_ACTION=false" if env_ok else "HL_MAINNET_SIGNED_ACTION=true",
    )) + "\x00"
    (proc / "environ").write_bytes(env.encode())
    now = datetime.now(timezone.utc).timestamp()
    stamp = datetime.fromtimestamp(now - heartbeat_age, timezone.utc).isoformat()
    (runtime / "runtime_health.jsonl").write_text(json.dumps({"timestamp": stamp, "status": "ok"}) + "\n", encoding="utf-8")
    return runtime_root, proc_root, now


def test_inspector_accepts_only_exact_safe_fresh_process(tmp_path):
    spec = CANDIDATES["candidate_v77_trend_retest_anti_chase_long"]
    runtime, proc, now = _proc_fixture(tmp_path, spec)
    result = inspect_candidate(spec, runtime_root=runtime, proc_root=proc, now=now)
    assert result.state == "healthy"
    assert result.restart_allowed is False
    assert result.environment_ok is True


def test_inspector_allows_restart_only_when_process_is_missing(tmp_path):
    spec = CANDIDATES["candidate_v77_trend_retest_anti_chase_long"]
    runtime = tmp_path / "runtime"
    (runtime / spec.strategy_id).mkdir(parents=True)
    result = inspect_candidate(spec, runtime_root=runtime, proc_root=tmp_path / "proc", now=0)
    assert result.state == "missing"
    assert result.restart_allowed is True


def test_inspector_refuses_restart_for_unsafe_or_stale_process(tmp_path):
    spec = CANDIDATES["research_v77_bear_trend_retest_short"]
    runtime, proc, now = _proc_fixture(tmp_path, spec, env_ok=False)
    unsafe = inspect_candidate(spec, runtime_root=runtime, proc_root=proc, now=now)
    assert unsafe.state == "unsafe_or_unclear"
    assert unsafe.restart_allowed is False
    runtime2, proc2, now2 = _proc_fixture(tmp_path / "stale", spec, pid=124, heartbeat_age=181)
    stale = inspect_candidate(spec, runtime_root=runtime2, proc_root=proc2, now=now2)
    assert stale.state == "stale"
    assert stale.restart_allowed is False
    assert "heartbeat_stale" in stale.blockers


def test_relative_value_candidate_is_disabled(tmp_path):
    spec = CANDIDATES["research_v78_market_neutral_relative_value"]
    result = inspect_candidate(spec, runtime_root=tmp_path)
    assert result.state == "disabled"
    assert result.restart_allowed is False


def test_v77_2_momentum_candidate_is_registered_paper_only():
    spec = CANDIDATES["research_v77_2_relative_strength_momentum_continuation"]
    assert spec.enabled is True
    assert spec.paper_only is True
    assert spec.strategy_version == "v77.2.0"
    assert spec.coins == ("BTC", "ETH", "LINK")


def test_v78_2_confirmed_range_candidate_is_registered_separately():
    spec = CANDIDATES["research_v78_2_confirmed_range_reversion"]
    assert spec.strategy_version == "v78.2.0"
    assert spec.module == "src.tools.v78_2_range_runtime"
    assert spec.coins == ("BTC", "ETH", "SOL", "LINK", "HYPE")
    assert spec.paper_only is True
