from __future__ import annotations

from decimal import Decimal
import json

from src.ctb_copy.read_only_guard import ReadOnlyGuard
from src.ctb_copy.runtime_paths import CopyResearchPaths
from src.ctb_copy.run_shadow_once import main as run_shadow_main
from src.ctb_copy.shadow.position_engine import (
    ShadowPositionEngine,
    build_shadow_portfolio,
    classify_delta,
    compute_deltas,
    compute_position_delta,
    run_shadow_engine_once,
)
from src.ctb_copy.shadow.shadow_models import ShadowDecisionType, ShadowDeltaType, ShadowLeaderPosition


def pos(symbol="BTC", size="1", side="long", **kwargs):
    defaults = {
        "leader_id": "leader-1",
        "leader_type": "wallet",
        "symbol": symbol,
        "side": side,
        "size": Decimal(size),
        "leader_entry_price": Decimal("100"),
        "current_mid": Decimal("100.2"),
        "observed_at": "2026-06-14T10:00:00+00:00",
        "position_age_hours": Decimal("8"),
        "leader_unrealized_pnl_pct": Decimal("0.2"),
        "leader_liquidation_distance_pct": Decimal("20"),
        "funding_rate": Decimal("0.001"),
        "spread_pct": Decimal("0.03"),
        "top_depth_usd": Decimal("50000"),
        "source_snapshot_id": "snap-1",
    }
    defaults.update(kwargs)
    return ShadowLeaderPosition(**defaults)


def test_delta_types_are_detected() -> None:
    assert classify_delta(Decimal("0"), Decimal("1")) == ShadowDeltaType.NEW_POSITION
    assert classify_delta(Decimal("1"), Decimal("2")) == ShadowDeltaType.INCREASE_POSITION
    assert classify_delta(Decimal("2"), Decimal("1")) == ShadowDeltaType.DECREASE_POSITION
    assert classify_delta(Decimal("1"), Decimal("0")) == ShadowDeltaType.CLOSE_POSITION
    assert classify_delta(Decimal("1"), Decimal("-1")) == ShadowDeltaType.FLIP_POSITION
    assert classify_delta(Decimal("1"), Decimal("1")) == ShadowDeltaType.UNCHANGED_POSITION


def test_compute_deltas_for_new_increase_decrease_close_flip_and_unchanged() -> None:
    previous = [
        pos("INC", size="1"),
        pos("DEC", size="2"),
        pos("CLOSE", size="1"),
        pos("FLIP", size="1"),
        pos("UNCH", size="1"),
    ]
    current = [
        pos("NEW", size="1"),
        pos("INC", size="2"),
        pos("DEC", size="1"),
        pos("FLIP", size="1", side="short"),
        pos("UNCH", size="1"),
    ]

    deltas = {delta.symbol: delta.delta_type for delta in compute_deltas(previous, current)}

    assert deltas["NEW"] == ShadowDeltaType.NEW_POSITION
    assert deltas["INC"] == ShadowDeltaType.INCREASE_POSITION
    assert deltas["DEC"] == ShadowDeltaType.DECREASE_POSITION
    assert deltas["CLOSE"] == ShadowDeltaType.CLOSE_POSITION
    assert deltas["FLIP"] == ShadowDeltaType.FLIP_POSITION
    assert deltas["UNCH"] == ShadowDeltaType.UNCHANGED_POSITION


def decision_for(current: ShadowLeaderPosition):
    delta = compute_position_delta(None, current)
    return ShadowPositionEngine().evaluate_delta(delta)


def test_pretrade_blocks_young_profit_spread_depth() -> None:
    assert "BLOCK_POSITION_TOO_YOUNG" in decision_for(pos(position_age_hours=Decimal("1"))).reason_codes
    assert "BLOCK_LEADER_TOO_FAR_IN_PROFIT" in decision_for(pos(leader_unrealized_pnl_pct=Decimal("1"))).reason_codes
    assert "BLOCK_SPREAD_TOO_WIDE" in decision_for(pos(spread_pct=Decimal("0.5"))).reason_codes
    assert "BLOCK_DEPTH_TOO_THIN" in decision_for(pos(top_depth_usd=Decimal("10"))).reason_codes


def test_allowed_decision_generates_simulated_entry_and_costs() -> None:
    decision = decision_for(pos())

    assert decision.decision == ShadowDecisionType.ALLOWED
    assert decision.reason_codes == ("ALLOW_SHADOW_POSITION",)
    assert decision.follower_sim_entry_price is not None
    assert decision.estimated_total_entry_cost_pct > Decimal("0")
    assert decision.estimated_roundtrip_cost_pct > decision.estimated_total_entry_cost_pct
    assert decision.read_only_guard["read_only"] is True
    portfolio = build_shadow_portfolio([decision])
    assert portfolio.total_notional_usd == Decimal("100")
    assert portfolio.positions[0].symbol == "BTC"


def test_non_candidate_deltas_are_ignored() -> None:
    previous = pos(size="1")
    current = pos(size="0", side="flat")
    decision = ShadowPositionEngine().evaluate_delta(compute_position_delta(previous, current))
    assert decision.decision == ShadowDecisionType.IGNORED
    assert "IGNORE_CLOSE_POSITION" in decision.reason_codes


def test_read_only_guard_failure_blocks() -> None:
    engine = ShadowPositionEngine(guard=ReadOnlyGuard(read_only=False))
    decision = engine.evaluate_delta(compute_position_delta(None, pos()))
    assert decision.decision == ShadowDecisionType.BLOCKED
    assert "BLOCK_READ_ONLY_GUARD_FAILED" in decision.reason_codes


def test_run_shadow_engine_once_writes_runtime_journals_outside_repo(tmp_path) -> None:
    paths = CopyResearchPaths(tmp_path / "copy_research")
    summary = run_shadow_engine_once([], [pos()], paths, day="2026-06-14")

    assert summary.status == "ok"
    assert summary.decisions_count == 1
    assert summary.allowed_count == 1
    assert "Crypto_Agent" not in summary.decision_journal_path
    rows = [json.loads(line) for line in paths.shadow_position_decisions_file("2026-06-14").read_text().splitlines()]
    assert rows[0]["decision"] == "allowed"
    portfolio_rows = [json.loads(line) for line in paths.shadow_portfolio_file("2026-06-14").read_text().splitlines()]
    assert portfolio_rows[0]["total_notional_usd"] == "100"


def test_run_shadow_once_without_wallet_snapshots_returns_ok(tmp_path, capsys) -> None:
    rc = run_shadow_main(["--runtime-dir", str(tmp_path / "copy_research"), "--day", "2026-06-14"])

    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "ok_no_wallet_snapshots"
    assert payload["decisions_count"] == 0
    assert payload["read_only_guard"]["read_only"] is True


def test_run_shadow_once_requires_two_snapshots_per_wallet(tmp_path, capsys) -> None:
    paths = CopyResearchPaths(tmp_path / "copy_research")
    wallet_path = paths.snapshot_file("wallets", "2026-06-14")
    wallet_path.parent.mkdir(parents=True, exist_ok=True)
    row_a = {"wallet_address": "0x" + "1" * 40, "observed_at_ms": 1, "raw": {"clearinghouseState": {"assetPositions": []}}}
    row_b = {"wallet_address": "0x" + "2" * 40, "observed_at_ms": 1, "raw": {"clearinghouseState": {"assetPositions": []}}}
    wallet_path.write_text(json.dumps(row_a) + "\n" + json.dumps(row_b) + "\n", encoding="utf-8")

    rc = run_shadow_main(["--runtime-dir", str(paths.root), "--day", "2026-06-14"])

    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "ok_no_position_deltas"
    assert payload["decisions_count"] == 0
