"""IbkrBroker contract resolution + market-data for the UCITS route (fix/ucits-contract-routing).

Mocks the ib_async IB client (broker._ib) — NEVER hits a live broker. Verifies:
  - the US path builds Stock(symbol, SMART, USD) exactly as before (byte-unchanged),
  - the UCITS path builds a Contract(secIdType=ISIN, secId=..., currency=USD) and qualifies it,
  - an unresolvable contract is LOGGED + SKIPPED: place_order raises (no order), market_price returns None,
  - market_price sources a REAL USD price via delayed market data.
"""
from __future__ import annotations

from types import SimpleNamespace

import pytest

from fxhnt.adapters.broker.ibkr import ContractResolutionError, IbkrBroker
from fxhnt.ports.broker import Order


class _FakeIB:
    """Records the last contract passed to qualifyContracts/placeOrder so tests can assert on it."""
    def __init__(self, *, resolves: bool = True, price: float | None = 600.0):
        self._resolves = resolves
        self._price = price
        self.qualified: list = []
        self.placed: list = []
        self.market_data_type: int | None = None

    def isConnected(self):
        return True

    def qualifyContracts(self, contract):
        self.qualified.append(contract)
        if not self._resolves:
            return []                       # IBKR: empty list => contract unknown
        if getattr(contract, "conId", 0) in (0, None):
            contract.conId = 12345          # qualified in place
        return [contract]

    def reqMarketDataType(self, t):
        self.market_data_type = t

    def reqMktData(self, contract, generic, snapshot, regulatory):
        return SimpleNamespace(marketPrice=lambda: self._price, last=self._price,
                               close=self._price, bid=self._price, ask=self._price)

    def sleep(self, _s):
        return None

    def managedAccounts(self):
        return ["DU9600528"]

    def placeOrder(self, contract, order):
        self.placed.append((contract, order))
        return SimpleNamespace(orderStatus=SimpleNamespace(status="Submitted"),
                               order=order, fills=[])


def _stock_symbol(c):
    return getattr(c, "symbol", None)


def test_us_path_places_stock_smart_usd_unchanged():
    """--venue us: an Order with default exchange/currency builds Stock(symbol, SMART, USD) and places."""
    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=True)
    status = broker.place_order(Order(symbol="SPY", side="BUY", quantity=10))
    assert status == "Submitted"
    contract = broker._ib.placed[0][0]
    assert _stock_symbol(contract) == "SPY"
    assert contract.exchange == "SMART"
    assert contract.currency == "USD"
    assert getattr(contract, "secIdType", "") in ("", None)     # US path uses NO secId


def test_ucits_path_places_isin_contract_usd():
    """--venue ucits: an Order carrying ISIN resolves the Contract by ISIN, then places pinned by the resolved
    conId on SMART. The ambiguous ISIN/localSymbol is STRIPPED before placing (fix for Error 478, 2026-07-20)
    — the conId alone is unambiguous, and a residual conflicting identity field gets the order rejected."""
    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=True)
    order = Order(symbol="CSPX", side="BUY", quantity=5,
                  exchange="SMART", currency="USD", sec_id_type="ISIN", sec_id="IE00B5BMR087")
    # Before placing, the order carried the ISIN (that is how resolution keys the contract).
    assert order.sec_id == "IE00B5BMR087" and order.sec_id_type == "ISIN"
    status = broker.place_order(order)
    assert status == "Submitted"
    # The PLACED contract is pinned by the resolved conId on SMART, with the ambiguous ISIN stripped so IBKR
    # cannot raise Error 478 (a residual identity field conflicting with the conId).
    contract = broker._ib.placed[0][0]
    assert getattr(contract, "conId", 0) == 12345
    assert contract.exchange == "SMART"
    assert not getattr(contract, "secId", None)
    assert contract.currency == "USD"


def test_unresolvable_contract_skips_not_crashes():
    """A contract that does NOT resolve => place_order raises ContractResolutionError, places NO order."""
    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=False)
    order = Order(symbol="NOPE", side="BUY", quantity=1, sec_id_type="ISIN", sec_id="XX0000000000")
    with pytest.raises(ContractResolutionError):
        broker.place_order(order)
    assert broker._ib.placed == []          # no order placed on an unresolved contract


def test_market_price_returns_real_price_via_delayed_data():
    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=True, price=620.5)
    px = broker.market_price("CSPX", exchange="SMART", currency="USD",
                             sec_id_type="ISIN", sec_id="IE00B5BMR087")
    assert px == 620.5
    assert broker._ib.market_data_type == 3     # 3 = delayed (paper accounts lack live subscriptions)
    contract = broker._ib.qualified[-1]
    assert contract.secId == "IE00B5BMR087"


def test_market_price_none_when_contract_unresolved():
    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=False)
    px = broker.market_price("NOPE", sec_id_type="ISIN", sec_id="XX0000000000")
    assert px is None


def test_market_price_none_when_ib_api_raises():
    """F1 (defense in depth): if the IB round-trip RAISES, market_price downgrades it to None (skip), never
    propagates — one thin/broken line can't wedge the rebalance."""
    class _RaisingIB(_FakeIB):
        def reqMktData(self, *a, **k):
            raise RuntimeError("connection reset by peer")

    broker = IbkrBroker()
    broker._ib = _RaisingIB(resolves=True)
    px = broker.market_price("DBMF", sec_id_type="ISIN", sec_id="LU2951555585")
    assert px is None


def test_market_price_none_when_no_usable_price():
    import math

    broker = IbkrBroker()
    broker._ib = _FakeIB(resolves=True, price=math.nan)     # NaN/no data
    px = broker.market_price("CSPX", sec_id_type="ISIN", sec_id="IE00B5BMR087")
    assert px is None
