"""Orchestrator end-to-end with a FAKE exchange: targets -> gates -> router -> place -> reconcile -> persist.
Maker fills here (fake fills the maker leg), so no IOC needed; reconciliation is clean."""
from __future__ import annotations

from fxhnt.adapters.persistence.exec_store import ExecStore
from fxhnt.application.crypto_execution import CryptoExecutionService, ExecConfig
from fxhnt.ports.crypto_exchange import (
    ChildOrder, CryptoAccount, Fill, OrderBook, PerpPosition, SymbolFilter,
)


class FakeExchange:
    name = "fake"
    def __init__(self):
        self._pos: dict[str, float] = {}
        self.placed: list[ChildOrder] = []
    def exchange_info(self): return {s: SymbolFilter(s, 0.001, 0.1, 5.0) for s in ("AAA", "BBB")}
    def order_book(self, s): return OrderBook(s, 100.0, 100.1, 100.0, 100.0)   # deep book
    def positions(self): return [PerpPosition(s, q, 100.0, 3) for s, q in self._pos.items() if q]
    def account(self): return CryptoAccount(nlv=10_000.0, available_balance=10_000.0, positions=dict(self._pos))
    def set_leverage(self, s, l): pass
    def ensure_one_way_mode(self): pass
    def place(self, o: ChildOrder) -> Fill:
        self.placed.append(o)
        self._pos[o.symbol] = self._pos.get(o.symbol, 0.0) + (o.qty if o.side == "BUY" else -o.qty)
        return Fill(o.client_id, o.symbol, o.side, o.qty, o.price, 0.0, "FILLED")
    def cancel(self, s, cid): pass
    def order_status(self, symbol: str, client_id: str) -> Fill:
        # Default: authoritative state matches the place-time fill (maker fully filled)
        for o in self.placed:
            if o.client_id == client_id:
                return Fill(client_id, symbol, o.side, o.qty, o.price, 0.0, "FILLED")
        return Fill(client_id, symbol, "", 0.0, 0.0, 0.0, "CANCELED")


class FakeExchangePartialMaker(FakeExchange):
    """Maker leg only partially fills at place time; order_status returns the same partial amount
    (i.e., the maker did NOT fill further during the sleep). IOC picks up the remainder."""

    def __init__(self, maker_fill_frac: float = 0.4):
        super().__init__()
        self._maker_fill_frac = maker_fill_frac
        # track maker client_ids so we can return correct order_status
        self._maker_fills: dict[str, Fill] = {}

    def place(self, o: ChildOrder) -> Fill:
        self.placed.append(o)
        if o.time_in_force == "GTX":
            # maker leg: only partially fills
            filled_qty = round(o.qty * self._maker_fill_frac, 12)
            fill = Fill(o.client_id, o.symbol, o.side, filled_qty, o.price, 0.0,
                        "PARTIAL" if filled_qty > 0 else "CANCELED")
            self._maker_fills[o.client_id] = fill
            signed = filled_qty if o.side == "BUY" else -filled_qty
            self._pos[o.symbol] = self._pos.get(o.symbol, 0.0) + signed
            return fill
        else:
            # IOC leg: fills the remainder fully
            fill = Fill(o.client_id, o.symbol, o.side, o.qty, o.price, 0.0, "FILLED")
            signed = o.qty if o.side == "BUY" else -o.qty
            self._pos[o.symbol] = self._pos.get(o.symbol, 0.0) + signed
            return fill

    def order_status(self, symbol: str, client_id: str) -> Fill:
        # Returns the authoritative post-cancel maker fill (same as place-time partial here)
        if client_id in self._maker_fills:
            return self._maker_fills[client_id]
        return Fill(client_id, symbol, "", 0.0, 0.0, 0.0, "CANCELED")


class FakeExchangeMakerFillsDuringSleep(FakeExchange):
    """Maker fills MORE during the sleep than at place time: place returns 40%, order_status returns 100%.
    The IOC remainder must be 0 — no over-fill."""

    def __init__(self):
        super().__init__()
        self._maker_fills: dict[str, tuple[Fill, float]] = {}  # client_id -> (place_fill, full_qty)

    def place(self, o: ChildOrder) -> Fill:
        self.placed.append(o)
        if o.time_in_force == "GTX":
            # place sees only 40% filled
            partial_qty = round(o.qty * 0.4, 12)
            fill = Fill(o.client_id, o.symbol, o.side, partial_qty, o.price, 0.0, "PARTIAL")
            self._maker_fills[o.client_id] = (fill, o.qty)
            signed = partial_qty if o.side == "BUY" else -partial_qty
            self._pos[o.symbol] = self._pos.get(o.symbol, 0.0) + signed
            return fill
        else:
            # IOC: should NOT be called if remainder == 0
            fill = Fill(o.client_id, o.symbol, o.side, o.qty, o.price, 0.0, "FILLED")
            signed = o.qty if o.side == "BUY" else -o.qty
            self._pos[o.symbol] = self._pos.get(o.symbol, 0.0) + signed
            return fill

    def order_status(self, symbol: str, client_id: str) -> Fill:
        # Authoritative: maker fully filled during the sleep window
        if client_id in self._maker_fills:
            place_fill, full_qty = self._maker_fills[client_id]
            # Update position: add the remaining fill that happened during sleep
            extra = full_qty - place_fill.qty
            signed_extra = extra if place_fill.side == "BUY" else -extra
            self._pos[symbol] = self._pos.get(symbol, 0.0) + signed_extra
            return Fill(client_id, symbol, place_fill.side, full_qty, place_fill.price, 0.0, "FILLED")
        return Fill(client_id, symbol, "", 0.0, 0.0, 0.0, "CANCELED")


def _cfg(**kw):
    base = dict(testnet=True, allow_live=False, kill_switch=False, leverage=3, gross_cap=1.0,
                per_symbol_cap=0.5, participation_rate=1.0, depth_cap=1.0, max_slippage_bps=25.0,
                hysteresis=0.001, kelly_cap=0.25, maker_wait_s=0.0)
    base.update(kw); return ExecConfig(**base)


def test_dry_run_places_nothing() -> None:
    fx = FakeExchange()
    svc = CryptoExecutionService(fx, ExecStore("sqlite://"), _cfg())
    plan = svc.rebalance({"AAA": 0.5, "BBB": -0.5}, adv_usd={"AAA": 1e9, "BBB": 1e9},
                         edge_theta=1.0, kelly_fraction=1.0, rebalance_id="r1", execute=False)
    assert plan.n_orders > 0 and fx.placed == []           # planned, not placed


def test_execute_places_and_reconciles_clean() -> None:
    fx = FakeExchange()
    store = ExecStore("sqlite://")
    svc = CryptoExecutionService(fx, store, _cfg())
    plan = svc.rebalance({"AAA": 0.5, "BBB": -0.5}, adv_usd={"AAA": 1e9, "BBB": 1e9},
                         edge_theta=1.0, kelly_fraction=1.0, rebalance_id="r1", execute=True)
    assert len(fx.placed) == 2 and plan.drift == {}        # both legs placed, reconciliation clean
    assert plan.halted is False
    assert len(store.fills("r1")) == 2                     # fills persisted


def test_kill_switch_halts_before_placing() -> None:
    fx = FakeExchange()
    svc = CryptoExecutionService(fx, ExecStore("sqlite://"), _cfg(kill_switch=True))
    plan = svc.rebalance({"AAA": 0.5}, adv_usd={"AAA": 1e9}, edge_theta=1.0, kelly_fraction=1.0,
                         rebalance_id="r1", execute=True)
    assert plan.halted is True and plan.block_reason == "kill_switch" and fx.placed == []


# ---------------------------------------------------------------------------
# IOC-path tests (maker -> partial -> cancel -> order_status -> IOC)
# ---------------------------------------------------------------------------

def test_ioc_fallback_sizes_remainder_and_reconciles_clean() -> None:
    """C2+C1: maker fills 40% at place; order_status confirms 40%; IOC fills the remaining 60%.
    Final position == target, drift == {}, halted is False, BOTH legs persisted."""
    fx = FakeExchangePartialMaker(maker_fill_frac=0.4)
    store = ExecStore("sqlite://")
    svc = CryptoExecutionService(fx, store, _cfg(maker_wait_s=0.0))
    plan = svc.rebalance({"AAA": 0.5}, adv_usd={"AAA": 1e9},
                         edge_theta=1.0, kelly_fraction=1.0, rebalance_id="r2", execute=True)

    # Both maker (partial) and IOC legs must have been placed
    placed_tif = [o.time_in_force for o in fx.placed]
    assert "GTX" in placed_tif, "maker leg not placed"
    assert "IOC" in placed_tif, "IOC leg not placed"

    # Two fills persisted (one per leg)
    fills = store.fills("r2")
    aaa_fills = [f for f in fills if f.symbol == "AAA"]
    assert len(aaa_fills) == 2, f"expected 2 fills for AAA, got {len(aaa_fills)}"

    # Clean reconcile — no drift, no halt
    assert plan.drift == {}, f"unexpected drift: {plan.drift}"
    assert plan.halted is False


def test_ioc_not_oversized_when_maker_fills_during_wait() -> None:
    """C2: order_status returns 100% fill (maker filled during sleep after 40% at place).
    IOC remainder must be 0 — no over-fill, IOC must NOT be placed."""
    fx = FakeExchangeMakerFillsDuringSleep()
    store = ExecStore("sqlite://")
    svc = CryptoExecutionService(fx, store, _cfg(maker_wait_s=0.0))
    plan = svc.rebalance({"AAA": 0.5}, adv_usd={"AAA": 1e9},
                         edge_theta=1.0, kelly_fraction=1.0, rebalance_id="r3", execute=True)

    # Only the maker leg should have been placed — no IOC
    placed_tif = [o.time_in_force for o in fx.placed]
    assert "GTX" in placed_tif, "maker leg not placed"
    assert "IOC" not in placed_tif, "IOC must NOT be placed when maker fully filled during wait"

    # One fill persisted (maker only)
    fills = store.fills("r3")
    aaa_fills = [f for f in fills if f.symbol == "AAA"]
    assert len(aaa_fills) == 1, f"expected 1 fill for AAA (maker only), got {len(aaa_fills)}"

    # Clean reconcile
    assert plan.drift == {}, f"unexpected drift: {plan.drift}"
    assert plan.halted is False


def test_zero_scale_holds() -> None:
    """I5: edge_theta=0.0 -> gross_scaler==0 -> HOLD (no orders), block_reason set, nothing placed."""
    fx = FakeExchange()
    svc = CryptoExecutionService(fx, ExecStore("sqlite://"), _cfg())
    plan = svc.rebalance({"AAA": 0.5, "BBB": -0.5}, adv_usd={"AAA": 1e9, "BBB": 1e9},
                         edge_theta=0.0, kelly_fraction=1.0, rebalance_id="r4", execute=True)
    assert plan.block_reason == "zero_gross_scale_hold"
    assert plan.n_orders == 0
    assert fx.placed == []
    assert plan.halted is False   # hold is not a halt; no drift


def test_dry_run_surfaces_gate_block_reason() -> None:
    """I3: a tripped gate must appear in block_reason even on a dry-run (execute=False)."""
    fx = FakeExchange()
    svc = CryptoExecutionService(fx, ExecStore("sqlite://"), _cfg(kill_switch=True))
    plan = svc.rebalance({"AAA": 0.5}, adv_usd={"AAA": 1e9}, edge_theta=1.0, kelly_fraction=1.0,
                         rebalance_id="r5", execute=False)
    # dry-run: nothing placed, but block_reason is exposed for monitoring
    assert fx.placed == []
    assert plan.block_reason == "kill_switch"
    assert plan.halted is False   # dry-run never halts; it just reports


class FakeExchangeWithDriftedUntraded(FakeExchange):
    """FakeExchange that seeds a position in 'CCC' (not in target weights / exchange_info),
    then returns a DIFFERENT qty from positions() to simulate external drift / partial liquidation."""

    def __init__(self, seeded_qty: float, drifted_qty: float) -> None:
        super().__init__()
        # Pre-seed CCC so account() includes it in positions (feeds into `intended` via acct.positions)
        self._pos["CCC"] = seeded_qty
        self._drifted_qty = drifted_qty

    def positions(self) -> list[PerpPosition]:
        """Post-trade reconcile call: return traded symbols at their real qty, but CCC at drifted_qty."""
        result = []
        for sym, qty in self._pos.items():
            if sym == "CCC":
                # Simulate external drift: exchange reports a different qty than expected
                if self._drifted_qty != 0.0:
                    result.append(PerpPosition("CCC", self._drifted_qty, 100.0, 3))
                # if drifted_qty is 0 we omit CCC entirely (position was liquidated externally)
            elif qty:
                result.append(PerpPosition(sym, qty, 100.0, 3))
        return result


def test_reconcile_flags_unexpected_drift_on_untraded_symbol() -> None:
    """Fix 4: reconciliation must catch drift on symbols NOT traded in this cycle.

    Scenario: the exchange holds 5.0 CCC (seeded pre-trade).  CCC is NOT in target weights
    and NOT in exchange_info, so the orchestrator never places an order for it.
    Post-trade, positions() returns 2.0 CCC (external partial liquidation / drift).
    Expected: plan.drift contains 'CCC' and plan.halted is True.
    """
    seeded_qty = 5.0
    drifted_qty = 2.0   # external partial liquidation: 3.0 units disappeared silently

    fx = FakeExchangeWithDriftedUntraded(seeded_qty=seeded_qty, drifted_qty=drifted_qty)
    svc = CryptoExecutionService(fx, ExecStore("sqlite://"), _cfg())

    # Only trade AAA/BBB — CCC is absent from weights and from exchange_info
    plan = svc.rebalance({"AAA": 0.5, "BBB": -0.5}, adv_usd={"AAA": 1e9, "BBB": 1e9},
                         edge_theta=1.0, kelly_fraction=1.0, rebalance_id="r6", execute=True)

    assert "CCC" in plan.drift, (
        f"CCC drift not detected; plan.drift={plan.drift}"
    )
    assert plan.halted is True, "unexpected drift on untraded symbol must halt"
