from __future__ import annotations

import json
from pathlib import Path
from datetime import datetime, timezone

from decimal import Decimal

from src.strategies.v76_fee_aware_anti_chase import AntiChaseContext, anti_chase_blockers
from src.tools import v76_paper_runtime
from src.tools.v76_replay_from_journals import _decision


def _write_market_confluence(tmp_path: Path, coin: str = "BTC", *, recommendation: str = "paper_candidate", score: str = "80.00", blockers: list[str] | None = None) -> None:
    report = tmp_path / "runtime" / "reports" / "market_confluence_latest.json"
    report.parent.mkdir(parents=True, exist_ok=True)
    report.write_text(json.dumps({
        "live_order_allowed": False,
        "mainnet_signed_action": False,
        "confluence": {
            "market_regime": "selective",
            "scores": {
                coin: {
                    "final_score": score,
                    "final_trade_score": score,
                    "recommendation": recommendation,
                    "blockers": blockers or [],
                    "components": {"buy_impact_1k_pct": "0.02", "sell_impact_1k_pct": "0.02"},
                    "live_order_allowed": False,
                    "mainnet_signed_action": False,
                }
            },
        },
    }), encoding="utf-8")


def test_v76_runtime_supervisor_status_detects_missing_process(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    status = v76_paper_runtime.supervisor_status([v76_paper_runtime.STRICT_ID])
    row = status[v76_paper_runtime.STRICT_ID]
    assert row["running"] is False
    assert row["trade_journal"].endswith("trade_journal.jsonl")
    assert row["signal_journal"].endswith("signal_journal.jsonl")


def test_v76_shadow_signal_schema_written_with_mocked_marketdata(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100", "ETH": "50"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.RESEARCH_ID, ["BTC", "ETH"])
    assert payload["mainnet_signed_action"] is False
    journal = Path(payload["runtime_dir"]) / "signal_journal.jsonl"
    rows = [json.loads(line) for line in journal.read_text().splitlines()]
    assert rows
    required = {"coin", "timestamp", "strategy_id", "setup_type", "signal_score", "would_enter", "block_reason", "spread_pct", "depth_ok", "expected_roundtrip_cost_pct", "expected_move_pct", "expected_move_vs_cost", "funding", "data_quality_allowed", "tradingview_context", "breadth_state", "trend_state", "leakage_state", "risk_gate_result", "final_decision"}
    assert required.issubset(rows[0])


def test_fee_aware_anti_chase_blocks_weak_rr_and_down_market():
    ctx = AntiChaseContext(
        coin="BTC",
        symbol="BTC/USDC:USDC",
        current_price=Decimal("100"),
        recent_high=Decimal("101"),
        retest_low=Decimal("99.2"),
        sma_fast=Decimal("99.6"),
        sma_slow=Decimal("98.8"),
        atr_pct=Decimal("0.30"),
        move_15m_pct=Decimal("-0.20"),
        move_1h_pct=Decimal("-0.30"),
        pullback_from_high_pct=Decimal("0.99"),
        breadth_positive_candidates=4,
        relative_strength_rank=1,
        volume_24h=Decimal("50000000"),
        spread_pct=Decimal("0.03"),
        expected_slippage_pct=Decimal("0.02"),
        funding_rate_hourly_pct=Decimal("0"),
        wallet_equity_usdc=Decimal("75"),
        market_trend_direction="down",
        market_breadth_positive_pct=Decimal("25"),
    )

    blockers = anti_chase_blockers(ctx)

    assert "risk_reward_below_1_4x" in blockers
    assert "market_trend_down" in blockers
    assert "market_breadth_not_supportive" in blockers


def test_v76_runtime_can_run_fee_aware_anti_chase_paper_candidate(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    _write_market_confluence(tmp_path, "BTC")
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "101.0"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["status"] == "ok"
    assert payload["paper_trading"] is True
    assert payload["mainnet_signed_action"] is False
    assert payload["trades"] == 1
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["setup_type"] == "fee_aware_anti_chase_retest"
    assert signals[-1]["final_decision"] == "paper_opened"
    assert signals[-1]["market_trend"]["direction"] == "unknown"
    assert signals[-1]["confluence_gate"]["allowed"] is True


def test_fee_aware_anti_chase_blocks_low_market_confluence(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    _write_market_confluence(tmp_path, "BTC", recommendation="block_new_entry", score="55.00", blockers=["weak_liquidity"])
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "101.0"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["trades"] == 0
    assert payload["blocked"]["confluence_block_new_entry"] == 1
    assert payload["blocked"]["confluence_weak_liquidity"] == 1
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["confluence_gate"]["allowed"] is False
    assert signals[-1]["confluence_gate"]["live_order_allowed"] is False


def test_fee_aware_anti_chase_blocks_new_longs_when_market_trend_is_down(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"BTC": "101.0", "ETH": "51.0", "SOL": "81.0", "LINK": "9.0"},
        "recent_trade_coins": [],
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100", "ETH": "50", "SOL": "80", "LINK": "8"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC", "ETH", "SOL", "LINK"])

    assert payload["trades"] == 0
    assert payload["blocked"]["market_trend_down"] == 4
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["market_trend"]["direction"] == "down"
    assert "market_breadth_not_supportive" in signals[-1]["block_reason"]


def test_fee_aware_anti_chase_cools_down_coin_after_stop_loss(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"BTC": "100"},
        "recent_trade_coins": [],
        "open_positions": {
            "BTC": {
                "entry": "100",
                "size": "0.10",
                "stop_loss": "99",
                "opened_at": "2026-06-26T00:00:00+00:00",
                "strategy_id": v76_paper_runtime.ANTI_CHASE_ID,
                "entry_fee_usd": "0.004",
                "spread_cost_usd": "0.001",
                "slippage_cost_usd": "0.001",
                "tradingview_impulse": {"category": "buy_opportunity", "action": "paper_candidate_if_other_gates_pass", "live_order_allowed": False, "mainnet_signed_action": False},
            }
        },
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "98.9"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["closed_positions"] == 1
    assert payload["trades"] == 0
    assert payload["blocked"]["recent_stop_cooldown"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert state["cooldowns"]["BTC"] == 5
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert "recent_stop_cooldown" in signals[-1]["block_reason"]
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "exit"
    assert trades[-1]["live_order_allowed"] is False
    assert trades[-1]["extra"]["tradingview_impulse"]["category"] == "buy_opportunity"


def test_fee_aware_anti_chase_blocks_tradingview_conflicted_long(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    _write_market_confluence(tmp_path, "BTC")
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "101.0"}, "recent_trade_coins": []}))
    tv = Path("runtime/research/tradingview_latest.json")
    tv.parent.mkdir(parents=True, exist_ok=True)
    tv.write_text(json.dumps({"coins": [{
        "coin": "BTC",
        "symbol": "BTCUSDT",
        "ts": "2026-07-04T22:00:00+00:00",
        "bias": "bearish",
        "trend_alignment": "conflict",
        "volume_state": "normal",
        "confidence": "0.9",
        "research_only": True,
        "source": "tradingview_mcp",
    }]}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    class FixedDateTime:
        @classmethod
        def now(cls, tz=None):
            return __import__("datetime").datetime(2026, 7, 4, 22, 10, tzinfo=tz)

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["trades"] == 0
    assert payload["blocked"]["tv_bias_bearish"] == 1
    assert payload["blocked"]["tv_mtf_conflict"] == 1
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["tradingview_context"]["available"] is True
    assert signals[-1]["tradingview_context"]["live_order_allowed"] is False
    assert signals[-1]["tradingview_impulse"]["category"] == "avoid_long"


def test_fee_aware_anti_chase_reacts_to_mcp_buy_opportunity_in_paper_only(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    _write_market_confluence(tmp_path, "BTC")
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "101.0"}, "recent_trade_coins": []}))
    tv = Path("runtime/research/tradingview_latest.json")
    tv.parent.mkdir(parents=True, exist_ok=True)
    tv.write_text(json.dumps({"coins": [{
        "coin": "BTC",
        "symbol": "BTCUSDT",
        "ts": datetime.now(timezone.utc).isoformat(),
        "bias": "bullish",
        "trend_alignment": "aligned",
        "volume_state": "breakout",
        "volatility_state": "squeeze",
        "confidence": "0.82",
        "research_only": True,
        "source": "tradingview_mcp",
    }]}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["trades"] == 1
    assert payload["mainnet_signed_action"] is False
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["final_decision"] == "paper_opened"
    assert signals[-1]["tradingview_impulse"]["category"] == "buy_opportunity"
    assert signals[-1]["tradingview_impulse"]["action"] == "paper_candidate_if_other_gates_pass"
    assert signals[-1]["tradingview_impulse"]["live_order_allowed"] is False
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["paper_trading"] is True
    assert trades[-1]["live_order_allowed"] is False
    assert trades[-1]["extra"]["tradingview_impulse"]["category"] == "buy_opportunity"


def test_fee_aware_anti_chase_does_not_reopen_same_coin_every_tick(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    _write_market_confluence(tmp_path, "BTC")
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "101.0"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    first = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])
    second = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert first["trades"] == 1
    assert second["trades"] == 0
    state = json.loads((runtime / "state.json").read_text())
    assert "BTC" in state["open_positions"]
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["final_decision"] == "blocked:already_open"


def test_fee_aware_anti_chase_closes_open_paper_position_on_profit_target(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.ANTI_CHASE_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"BTC": "100"},
        "recent_trade_coins": [],
        "open_positions": {
            "BTC": {
                "entry": "100",
                "size": "0.10",
                "stop_loss": "99",
                "opened_at": "2026-06-26T00:00:00+00:00",
                "strategy_id": v76_paper_runtime.ANTI_CHASE_ID,
                "entry_fee_usd": "0.004",
                "spread_cost_usd": "0.001",
                "slippage_cost_usd": "0.001",
            }
        },
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "101.30"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.ANTI_CHASE_ID, ["BTC"])

    assert payload["closed_positions"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert "BTC" not in state.get("open_positions", {})
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "exit"
    assert trades[-1]["exit_reason"] == "profit_target"
    assert float(trades[-1]["net_pnl_usd"]) > 0


def test_v76_runtime_strict_opens_lifecycle_position_instead_of_synthetic_exit(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "99"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC"])

    assert payload["trades"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert "BTC" in state["open_positions"]
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "entry"
    assert trades[-1]["extra"]["strict_anti_chase"] is True
    assert trades[-1]["live_order_allowed"] is False
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["final_decision"] == "paper_opened_lifecycle"
    assert signals[-1]["confluence_gate"]["depth_penalty_pct"] == "0.02"


def test_v76_runtime_strict_closes_lifecycle_position_on_take_profit(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"BTC": "100"},
        "recent_trade_coins": [],
        "open_positions": {
            "BTC": {
                "entry": "100",
                "size": "0.10",
                "stop_loss": "98.75",
                "take_profit": "101.25",
                "strategy_id": v76_paper_runtime.STRICT_ID,
                "risk_usd": "0.1875",
            }
        },
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "101.50"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC"])

    assert payload["closed_positions"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert "BTC" not in state.get("open_positions", {})
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "exit"
    assert trades[-1]["exit_reason"] == "take_profit"
    assert trades[-1]["mainnet_signed_action"] is False
    assert float(trades[-1]["net_pnl_usd"]) > 0


def test_v76_runtime_strict_dead_fish_exits_before_max_hold_when_no_mfe(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"WLD": "100"},
        "recent_trade_coins": [],
        "open_positions": {
            "WLD": {
                "entry": "100",
                "size": "0.10",
                "stop_loss": "98.75",
                "take_profit": "101.25",
                "strategy_id": v76_paper_runtime.STRICT_ID,
                "risk_usd": "0.1875",
                "ticks_held": 1,
                "max_hold_ticks": 4,
                "dead_fish_ticks": 2,
                "dead_fish_min_mfe_pct": "0.25",
                "mfe_pct": "0.05",
                "mae_pct": "-0.20",
            }
        },
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"WLD": "100.04"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["WLD"])

    assert payload["closed_positions"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert "WLD" not in state.get("open_positions", {})
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "exit"
    assert trades[-1]["exit_reason"] == "dead_fish_time_exit"
    assert trades[-1]["candles_held"] == 2
    assert trades[-1]["mainnet_signed_action"] is False


def test_v76_runtime_strict_time_exits_after_max_hold_ticks(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({
        "prev_mids": {"BTC": "100"},
        "recent_trade_coins": [],
        "open_positions": {
            "BTC": {
                "entry": "100",
                "size": "0.10",
                "stop_loss": "98.75",
                "take_profit": "101.25",
                "strategy_id": v76_paper_runtime.STRICT_ID,
                "risk_usd": "0.1875",
                "ticks_held": 23,
                "max_hold_ticks": 24,
                "mfe_pct": "0.10",
                "mae_pct": "-0.20",
            }
        },
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100.20"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC"])

    assert payload["closed_positions"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert "BTC" not in state.get("open_positions", {})
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "exit"
    assert trades[-1]["exit_reason"] == "time_exit"
    assert trades[-1]["candles_held"] == 24
    assert trades[-1]["mainnet_signed_action"] is False

def test_v76_runtime_strict_blocks_overextended_no_retest(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "100"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "104"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC"])

    assert payload["trades"] == 0
    assert payload["blocked"]["anti_chase_overextended_no_retest"] == 1
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert "anti_chase_overextended_no_retest" in signals[-1]["block_reason"]
    assert signals[-1]["final_decision"].startswith("blocked:")

def test_v76_runtime_strict_blocks_when_confluence_latest_blocks_coin(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.STRICT_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "99"}, "recent_trade_coins": []}))
    reports = Path("runtime/reports")
    reports.mkdir(parents=True, exist_ok=True)
    (reports / "market_confluence_latest.json").write_text(json.dumps({
        "confluence": {
            "scores": {
                "BTC": {
                    "final_score": "72.10",
                    "recommendation": "block_new_entry",
                    "blockers": ["weak_liquidity"],
                }
            }
        },
        "live_order_allowed": False,
        "mainnet_signed_action": False,
    }))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "100"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC"])

    assert payload["trades"] == 0
    assert payload["blocked"]["confluence_block_new_entry"] == 1
    assert payload["blocked"]["confluence_weak_liquidity"] == 1
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["confluence_gate"]["source"] == "market_confluence_latest"
    assert signals[-1]["confluence_gate"]["live_order_allowed"] is False
    assert signals[-1]["confluence_gate"]["mainnet_signed_action"] is False
    assert "confluence_weak_liquidity" in signals[-1]["block_reason"]


def test_v76_runtime_degraded_market_data_does_not_crash_or_trade(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)

    class BrokenMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            raise ConnectionError("No route to host")

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", BrokenMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.STRICT_ID, ["BTC", "ETH"])
    assert payload["status"] == "degraded"
    assert payload["trades"] == 0
    assert payload["mainnet_signed_action"] is False
    runtime = Path(payload["runtime_dir"])
    health = [json.loads(line) for line in (runtime / "runtime_health.jsonl").read_text().splitlines()]
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert health[-1]["event"] == "market_data_unavailable"
    assert all(row["final_decision"] == "blocked:api_degraded" for row in signals)


def test_v76_replay_decision_blocks_wld_leakage_and_losers():
    counts = {"WLD": 6, "BTC": 4}
    from collections import Counter
    reasons = _decision({"coin": "WLD", "net_pnl_usd": "1.0"}, Counter(counts), 10)
    assert "blocked_by_leakage" in reasons
    reasons = _decision({"coin": "BTC", "net_pnl_usd": "-1.0"}, Counter(counts), 10)
    assert "blocked_by_trend" in reasons


def test_v76_runtime_swing_retest_opens_lifecycle_research_position(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    monkeypatch.delenv("CTB_LIVE_TRADING_ALLOWED", raising=False)
    runtime = Path("runtime/experiments") / v76_paper_runtime.SWING_ID
    runtime.mkdir(parents=True, exist_ok=True)
    (runtime / "state.json").write_text(json.dumps({"prev_mids": {"BTC": "100"}, "recent_trade_coins": []}))

    class FakeMarketData:
        def __init__(self, env: str):
            assert env == "mainnet"
        def get_all_mids(self):
            return {"BTC": "101"}

    monkeypatch.setattr(v76_paper_runtime, "HyperliquidMarketData", FakeMarketData)
    payload = v76_paper_runtime.scan_once(v76_paper_runtime.SWING_ID, ["BTC"])

    assert payload["status"] == "ok"
    assert payload["paper_trading"] is True
    assert payload["mainnet_signed_action"] is False
    assert payload["trades"] == 1
    state = json.loads((runtime / "state.json").read_text())
    assert state["open_positions"]["BTC"]["strategy_id"] == v76_paper_runtime.SWING_ID
    assert state["open_positions"]["BTC"]["max_hold_ticks"] == 96
    signals = [json.loads(line) for line in (runtime / "signal_journal.jsonl").read_text().splitlines()]
    assert signals[-1]["setup_type"] == "swing_trend_retest_reclaim"
    assert signals[-1]["final_decision"] == "paper_opened_lifecycle"
    trades = [json.loads(line) for line in (runtime / "trade_journal.jsonl").read_text().splitlines()]
    assert trades[-1]["event"] == "entry"
    assert trades[-1]["research"] is True
    assert trades[-1]["live_order_allowed"] is False
