"""BybitExecution ccxt adapter (design §4.1) against a FAKE ccxt exchange (no network).

The exchange factory is injected so these tests never touch ccxt or the wire. Covers: sandbox set when
testnet; place_market issues the right create_order call + reduceOnly param; mid midpoint + None on an
empty/one-sided book (the thin-testnet sentinel feeding low_confidence); instrument_limits parsing;
positions/equity parsing; BrokerError wrapping; the api_secret never appears in repr.
"""
from __future__ import annotations

from typing import Any

import pytest

from fxhnt.adapters.broker.bybit import BrokerError, BybitExecution, Fill
from fxhnt.application.bybit_testnet_execution import InstrumentLimit


class FakeExchange:
    """A minimal stand-in for ccxt.bybit recording every call the adapter makes."""

    def __init__(self, config: dict[str, Any]) -> None:
        self.config = config
        self.sandbox: bool | None = None
        self.created_orders: list[dict[str, Any]] = []
        self.order_books: dict[str, dict[str, list[list[float]]]] = {}
        self.balance: dict[str, Any] = {"total": {"USDT": 0.0}}
        self.positions_data: list[dict[str, Any]] = []
        self.markets: dict[str, Any] = {}
        self.raise_on_create: Exception | None = None
        self.open_orders_data: list[dict[str, Any]] = []
        self.open_orders_calls: list[str | None] = []

    def set_sandbox_mode(self, flag: bool) -> None:
        self.sandbox = flag

    def fetch_balance(self) -> dict[str, Any]:
        return self.balance

    def fetch_positions(self) -> list[dict[str, Any]]:
        return self.positions_data

    def load_markets(self) -> dict[str, Any]:
        return self.markets

    def fetch_order_book(self, symbol: str, limit: int | None = None) -> dict[str, list[list[float]]]:
        return self.order_books.get(symbol, {"bids": [], "asks": []})

    def fetch_open_orders(self, symbol: str | None = None) -> list[dict[str, Any]]:
        self.open_orders_calls.append(symbol)
        return self.open_orders_data

    def create_order(self, symbol: str, type: str, side: str, amount: float,  # noqa: A002 - ccxt sig
                     price: float | None = None, params: dict[str, Any] | None = None) -> dict[str, Any]:
        if self.raise_on_create is not None:
            raise self.raise_on_create
        self.created_orders.append(
            {"symbol": symbol, "type": type, "side": side, "amount": amount, "params": params or {}})
        return {"id": "ord-1", "filled": amount, "average": 100.5,
                "fee": {"cost": 0.07}, "timestamp": 1_700_000_000_000}


def _adapter(fake: FakeExchange, *, testnet: bool = True) -> BybitExecution:
    return BybitExecution("KEY", "SECRET", testnet=testnet, exchange_factory=lambda cfg: fake)


def test_sandbox_set_when_testnet() -> None:
    fake = FakeExchange({})
    _adapter(fake, testnet=True)
    assert fake.sandbox is True


def test_sandbox_off_when_not_testnet() -> None:
    fake = FakeExchange({})
    _adapter(fake, testnet=False)
    assert fake.sandbox is False


def test_place_market_buy_calls_create_order() -> None:
    fake = FakeExchange({})
    adapter = _adapter(fake)
    fill = adapter.place_market("BTCUSDT", 0.5, reduce_only=False)
    assert fake.created_orders == [
        {"symbol": "BTCUSDT", "type": "market", "side": "buy", "amount": 0.5,
         "params": {"reduceOnly": False}}]
    assert isinstance(fill, Fill)
    assert fill.symbol == "BTCUSDT" and fill.side == "BUY"
    assert fill.qty == 0.5 and fill.avg_price == 100.5
    assert fill.fee == 0.07 and fill.order_id == "ord-1"


def test_place_market_sell_uses_abs_qty_and_sell_side() -> None:
    fake = FakeExchange({})
    fill = _adapter(fake).place_market("ETHUSDT", -2.0, reduce_only=True)
    assert fake.created_orders[0]["side"] == "sell"
    assert fake.created_orders[0]["amount"] == 2.0
    assert fake.created_orders[0]["params"] == {"reduceOnly": True}
    assert fill.side == "SELL" and fill.qty == 2.0


def test_mid_returns_midpoint() -> None:
    fake = FakeExchange({})
    fake.order_books["BTCUSDT"] = {"bids": [[100.0, 1.0]], "asks": [[102.0, 1.0]]}
    assert _adapter(fake).mid("BTCUSDT") == 101.0


def test_mid_none_on_empty_book() -> None:
    fake = FakeExchange({})
    fake.order_books["BTCUSDT"] = {"bids": [], "asks": []}
    assert _adapter(fake).mid("BTCUSDT") is None


def test_mid_none_on_one_sided_book() -> None:
    fake = FakeExchange({})
    fake.order_books["BTCUSDT"] = {"bids": [[100.0, 1.0]], "asks": []}
    assert _adapter(fake).mid("BTCUSDT") is None


def test_instrument_limits_parses() -> None:
    fake = FakeExchange({})
    fake.markets = {
        "BTC/USDT:USDT": {"id": "BTCUSDT", "swap": True, "quote": "USDT", "active": True,
                          "limits": {"amount": {"min": 0.001}, "cost": {"min": 5.0}},
                          "precision": {"amount": 0.001, "price": 0.1}},
    }
    limits = _adapter(fake).instrument_limits()
    assert limits["BTCUSDT"] == InstrumentLimit(min_order_qty=0.001, qty_step=0.001, min_notional=5.0)


def test_positions_signed() -> None:
    fake = FakeExchange({})
    fake.positions_data = [
        {"symbol": "BTC/USDT:USDT", "info": {"symbol": "BTCUSDT"}, "contracts": 0.5, "side": "long"},
        {"symbol": "ETH/USDT:USDT", "info": {"symbol": "ETHUSDT"}, "contracts": 2.0, "side": "short"},
        {"symbol": "XRP/USDT:USDT", "info": {"symbol": "XRPUSDT"}, "contracts": 0.0, "side": None},
    ]
    pos = _adapter(fake).positions()
    assert pos == {"BTCUSDT": 0.5, "ETHUSDT": -2.0}


def test_equity_parses_total_usdt() -> None:
    fake = FakeExchange({})
    fake.balance = {"total": {"USDT": 12_345.6}}
    assert _adapter(fake).equity() == 12_345.6


def test_broker_error_wraps_ccxt_error() -> None:
    fake = FakeExchange({})
    fake.raise_on_create = RuntimeError("ccxt boom")
    with pytest.raises(BrokerError):
        _adapter(fake).place_market("BTCUSDT", 0.5)


def test_open_orders_returns_resting_orders() -> None:
    fake = FakeExchange({})
    fake.open_orders_data = [{"id": "o1", "symbol": "BTC/USDT:USDT", "info": {"symbol": "BTCUSDT"}}]
    orders = _adapter(fake).open_orders()
    assert orders == fake.open_orders_data
    assert fake.open_orders_calls == [None]


def test_open_orders_passes_symbol_and_empty_default() -> None:
    fake = FakeExchange({})
    assert _adapter(fake).open_orders("BTCUSDT") == []
    assert fake.open_orders_calls == ["BTCUSDT"]


def test_secret_never_in_repr() -> None:
    fake = FakeExchange({})
    adapter = _adapter(fake)
    assert "SECRET" not in repr(adapter)
