from __future__ import annotations

from dataclasses import replace
from decimal import Decimal

import pytest

from tests.test_v78_research_archetypes import external, feature
from src.market.trend_retest_features import _adx
from src.strategies.confirmed_range_reversion_v78_2 import (
    STRATEGY_ID,
    STRATEGY_VERSION,
    classify_true_range,
    confirmed_range_reversion,
    build_maker_intent,
)
from src.tools import v78_2_range_runtime as runtime


def test_adx_distinguishes_directional_trend_from_choppy_range():
    trend = []
    choppy = []
    for idx in range(50):
        price = Decimal("100") + Decimal(idx)
        trend.append({"h": price + 1, "l": price - 1, "c": price + Decimal("0.5"), "v": Decimal("1")})
        center = Decimal("100") + (Decimal("0.5") if idx % 2 else Decimal("-0.5"))
        choppy.append({"h": center + 1, "l": center - 1, "c": center, "v": Decimal("1")})
    assert _adx(trend) > Decimal("40")
    assert _adx(choppy) < Decimal("22")


def _range_feature(coin="ETH"):
    return replace(
        feature(coin),
        current_price=Decimal("99.4"), close_15m=Decimal("99.4"), low_15m=Decimal("98.8"), high_15m=Decimal("100.1"),
        rsi_15m=Decimal("38"), vwap_15m=Decimal("100"), bb_lower_15m=Decimal("99"), bb_upper_15m=Decimal("101"),
        bb_width_pct=Decimal("2"), adx_15m=Decimal("17"), sma_1h_fast=Decimal("100"), sma_1h_slow=Decimal("99.8"),
        sma_4h_fast=Decimal("100"), sma_4h_slow=Decimal("99.7"), sma_1h_spread_pct=Decimal("0.2"), sma_4h_spread_pct=Decimal("0.3"),
        volume_ratio=Decimal("1.1"), spread_pct=Decimal("0.02"), best_bid=Decimal("99.39"), best_ask=Decimal("99.41"),
    )


def test_true_range_requires_low_adx_and_flat_primary_trends():
    rows = {"BTC": _range_feature("BTC"), "ETH": _range_feature("ETH")}
    decision = classify_true_range(rows)
    assert decision.allowed is True
    assert decision.regime == "confirmed_range"
    bad = dict(rows)
    bad["BTC"] = replace(rows["BTC"], adx_15m=Decimal("27"), sma_1h_spread_pct=Decimal("1.5"))
    decision = classify_true_range(bad)
    assert decision.allowed is False
    assert "primary_adx_not_range" in decision.blockers


def test_long_requires_lower_band_reentry_and_blocks_bearish_higher_timeframe():
    f = _range_feature()
    decision = confirmed_range_reversion(f, range_allowed=True, external=external())
    assert decision.action == "paper_candidate"
    assert decision.side == "long"
    no_reentry = confirmed_range_reversion(replace(f, low_15m=Decimal("99.2")), range_allowed=True, external=external())
    assert no_reentry.action == "no_trade"
    assert "lower_band_reentry_missing" in no_reentry.blockers
    bearish = confirmed_range_reversion(replace(f, sma_4h_fast=Decimal("97"), sma_4h_slow=Decimal("100"), sma_4h_spread_pct=Decimal("-3")), range_allowed=True, external=external())
    assert "higher_timeframe_bearish_blocks_long" in bearish.blockers
    dislocated = confirmed_range_reversion(replace(f, best_bid=Decimal("100"), best_ask=Decimal("100.02")), range_allowed=True, external=external())
    assert "live_mid_dislocated_from_signal_close" in dislocated.blockers


def test_short_requires_upper_band_reentry_and_blocks_bullish_higher_timeframe():
    f = replace(_range_feature(), current_price=Decimal("100.6"), close_15m=Decimal("100.6"), high_15m=Decimal("101.2"), low_15m=Decimal("99.9"), rsi_15m=Decimal("62"), best_bid=Decimal("100.59"), best_ask=Decimal("100.61"))
    decision = confirmed_range_reversion(f, range_allowed=True, external=external())
    assert decision.action == "paper_candidate"
    assert decision.side == "short"
    bullish = confirmed_range_reversion(replace(f, sma_4h_fast=Decimal("103"), sma_4h_slow=Decimal("100"), sma_4h_spread_pct=Decimal("3")), range_allowed=True, external=external())
    assert "higher_timeframe_bullish_blocks_short" in bullish.blockers


def test_hype_uses_coingecko_and_native_market_data_without_fake_binance_context():
    context = external()
    context["sources"]["coingecko"]["data"]["HYPE"] = {"total_volume_usd": "100000000", "market_cap_usd": "1000000000"}
    decision = confirmed_range_reversion(_range_feature("HYPE"), range_allowed=True, external=context)
    assert decision.action == "paper_candidate"
    assert all("binance" not in source for source in decision.source_attribution)
    assert any("coingecko" in source for source in decision.source_attribution)


def test_v78_2_intent_is_passive_alo_targets_vwap_and_respects_risk_budget():
    f = _range_feature()
    decision = confirmed_range_reversion(f, range_allowed=True, external=external())
    intent = build_maker_intent(f, decision)
    assert intent is not None
    assert intent.strategy_id == STRATEGY_ID
    assert STRATEGY_VERSION == "v78.2.0"
    assert intent.order_type == "limit"
    assert intent.tif == "Alo"
    assert intent.take_profit == f.vwap_15m
    assert intent.estimated_notional_usd <= Decimal("10")
    assert intent.risk_usd <= Decimal("0.075")


def test_v78_2_runtime_requires_explicit_paper_and_false_live_flags(monkeypatch):
    monkeypatch.delenv("CTB_PAPER_TRADING", raising=False)
    with pytest.raises(PermissionError):
        runtime.scan_once(["BTC"], md=object())
    monkeypatch.setenv("CTB_STRATEGY_ID", STRATEGY_ID)
    monkeypatch.setenv("CTB_PAPER_TRADING", "true")
    monkeypatch.setenv("CTB_LIVE_TRADING_ALLOWED", "false")
    monkeypatch.setenv("CTB_LIVE_ORDER_ALLOWED", "true")
    monkeypatch.setenv("HL_MAINNET_SIGNED_ACTION", "false")
    with pytest.raises(PermissionError):
        runtime.scan_once(["BTC"], md=object())


def test_pending_maker_fill_and_vwap_exit_are_forward_only(tmp_path):
    f = _range_feature()
    state = {"pending_orders": {"ETH": {
        "strategy_id": STRATEGY_ID, "strategy_version": STRATEGY_VERSION, "run_id": "r", "data_window_id": "w",
        "coin": "ETH", "side": "long", "entry_price": "99.3", "size": "0.1", "estimated_notional_usd": "9.93",
        "risk_usd": "0.07", "stop_loss": "98.5", "take_profit": "100", "funding_rate_hourly_pct": "0",
        "submitted_candle_ts": 1, "last_candle_ts": 1, "wait_bars": 0,
    }}, "open_positions": {}}
    assert runtime._manage_pending(state, {"ETH": f}, tmp_path) == 0
    touched = replace(f, candle_ts=2, low_15m=Decimal("99.2"))
    assert runtime._manage_pending(state, {"ETH": touched}, tmp_path) == 1
    assert "ETH" in state["open_positions"]
    target = replace(f, candle_ts=3, high_15m=Decimal("100.2"), low_15m=Decimal("99.5"), close_15m=Decimal("100"))
    assert runtime._manage_open(state, {"ETH": target}, tmp_path) == 1
    import json
    rows = [json.loads(line) for line in (tmp_path / "trade_journal.jsonl").read_text().splitlines()]
    assert [row["event"] for row in rows] == ["entry", "exit"]
    assert rows[-1]["exit_reason"] == "vwap_take_profit"
    assert rows[-1]["live_order_allowed"] is False


def test_pending_maker_order_cancels_when_range_regime_invalidates(tmp_path):
    f = replace(_range_feature(), candle_ts=2)
    order = {
        "strategy_id": STRATEGY_ID, "strategy_version": STRATEGY_VERSION, "run_id": "r", "data_window_id": "w",
        "coin": "ETH", "side": "long", "entry_price": "99.3", "size": "0.1", "estimated_notional_usd": "9.93",
        "risk_usd": "0.07", "stop_loss": "98.5", "take_profit": "100", "funding_rate_hourly_pct": "0",
        "submitted_candle_ts": 1, "last_candle_ts": 1, "wait_bars": 0,
    }
    state = {"pending_orders": {"ETH": order}, "open_positions": {}}
    assert runtime._manage_pending(state, {"ETH": f}, tmp_path, range_allowed=False) == 0
    assert state["pending_orders"] == {}
    row = __import__("json").loads((tmp_path / "order_journal.jsonl").read_text().strip())
    assert row["reason"] == "range_or_data_quality_invalidated"


def test_same_fill_bar_stop_is_closed_conservatively(tmp_path):
    f = replace(_range_feature(), candle_ts=2, open_15m=Decimal("99.5"), low_15m=Decimal("98.0"), high_15m=Decimal("100.5"))
    order = {
        "strategy_id": STRATEGY_ID, "strategy_version": STRATEGY_VERSION, "run_id": "r", "data_window_id": "w",
        "coin": "ETH", "side": "long", "entry_price": "99.3", "size": "0.1", "estimated_notional_usd": "9.93",
        "risk_usd": "0.075", "stop_loss": "98.5", "take_profit": "100", "funding_rate_hourly_pct": "0",
        "submitted_candle_ts": 1, "last_candle_ts": 1, "wait_bars": 0,
    }
    state = {"pending_orders": {"ETH": order}, "open_positions": {}}
    assert runtime._manage_pending(state, {"ETH": f}, tmp_path) == 1
    assert state["open_positions"] == {}
    import json
    rows = [json.loads(line) for line in (tmp_path / "trade_journal.jsonl").read_text().splitlines()]
    assert [row["event"] for row in rows] == ["entry", "exit"]
    assert rows[-1]["exit_reason"] == "same_bar_stop_loss"
    assert Decimal(rows[-1]["net_pnl_usd"]) < 0


def test_gap_through_stop_uses_adverse_candle_open(tmp_path):
    state = {"open_positions": {"ETH": {
        "strategy_id": STRATEGY_ID, "strategy_version": STRATEGY_VERSION, "run_id": "r", "data_window_id": "w",
        "coin": "ETH", "side": "long", "entry_price": "99.3", "size": "0.1", "stop_loss": "98.5",
        "take_profit": "100", "entry_fee_usd": "0.0015", "funding_rate_hourly_pct": "0", "bars_held": 0,
        "mfe_pct": "0", "mae_pct": "0", "last_candle_ts": 1,
    }}}
    gap = replace(_range_feature(), candle_ts=2, open_15m=Decimal("97.8"), low_15m=Decimal("97.5"), high_15m=Decimal("98.2"), close_15m=Decimal("98"))
    assert runtime._manage_open(state, {"ETH": gap}, tmp_path) == 1
    import json
    row = json.loads((tmp_path / "trade_journal.jsonl").read_text().strip())
    assert Decimal(row["exit_price"]) < Decimal("98.5")
    assert row["exit_reason"] == "stop_loss"


def test_skipped_candles_advance_hold_time_and_current_funding(tmp_path):
    state = {"open_positions": {"ETH": {
        "strategy_id": STRATEGY_ID, "strategy_version": STRATEGY_VERSION, "run_id": "r", "data_window_id": "w",
        "coin": "ETH", "side": "long", "entry_price": "100", "size": "0.1", "stop_loss": "90",
        "take_profit": "200", "entry_fee_usd": "0.0015", "accrued_funding_usd": "0", "bars_held": 0,
        "mfe_pct": "0", "mae_pct": "0", "last_candle_ts": 1,
    }}}
    feature_after_gap = replace(_range_feature(), candle_ts=1 + 3 * 900_000, open_15m=Decimal("99.4"), low_15m=Decimal("99"), high_15m=Decimal("100"), close_15m=Decimal("99.5"), funding_rate_hourly_pct=Decimal("0.01"))
    assert runtime._manage_open(state, {"ETH": feature_after_gap}, tmp_path) == 0
    position = state["open_positions"]["ETH"]
    assert position["bars_held"] == 3
    assert Decimal(position["accrued_funding_usd"]) > 0
