"""
Smoke tests for TopstepXBroker (the V16 BrokerBase wrapper).

These tests do NOT connect to TopstepX. They use a FakeAdapter that
replaces TopstepXAdapter and verify that the wrapper translates
shapes correctly.

Run:
    cd ~/apex_v16
    python -m tests.test_topstepx_v16
"""

from __future__ import annotations

import asyncio
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from broker.broker_base import OrderResult, Position, CancelResult
from broker import topstepx_v16


# ============================================================
# FAKE ADAPTER — replaces V15 adapter for tests
# ============================================================

class FakeAdapter:
    """Mimics TopstepXAdapter surface, returns canned data."""
    def __init__(self):
        self.calls: list[tuple] = []
        self._connected = False
        # New positions_get parser in topstepx_v16 reads adapter._instrument_cache
        # to map contractId → user symbol. Mirrors what real adapter populates
        # at connect() (topstepx_adapter.py:210-217).
        self._instrument_cache = {
            "MES": {"contract_id": "CON.F.US.MES.M26", "tick_size": 0.25, "tick_value": 1.25},
            "MNQ": {"contract_id": "CON.F.US.MNQ.M26", "tick_size": 0.25, "tick_value": 0.50},
        }

    async def connect(self, instruments):
        self.calls.append(("connect", tuple(instruments)))
        self._connected = True
        return True

    async def disconnect(self):
        self.calls.append(("disconnect",))
        self._connected = False

    def is_connected(self):
        return self._connected

    async def get_current_price(self, symbol):
        self.calls.append(("get_current_price", symbol))
        return 5012.25

    async def positions_get(self, symbol=None):
        self.calls.append(("positions_get", symbol))
        return [{
            "symbol": "MES",
            "contract_id": "CON.F.US.MES.M26",
            "position_id": 999,
            "size": 2,
            "abs_size": 2,
            "avg_price": 5010.50,
            "type": "BUY",
            "is_buy": True,
            "account_id": 12462346,
        }]

    async def _raw_positions_search(self):
        # Fallback path used by the new tri-state positions_get in
        # topstepx_v16.py. Real adapter reads /Position/searchOpen via SDK
        # _make_request and returns raw dicts in ProjectX schema. In tests,
        # primary REST path (_rest_auth + _rest_post) returns None (no env
        # vars), so positions_get falls back here. Returning one canned
        # position lets test_positions_get_translates_dict_to_position
        # exercise the parsing branch.
        self.calls.append(("_raw_positions_search",))
        return [{
            "contractId": "CON.F.US.MES.M26",
            "type": 1,                 # 1=BUY, else SELL (ProjectX schema)
            "size": 2,
            "averagePrice": 5010.50,
            "id": 999,
            "accountId": 12462346,
        }]

    async def place_market_bracket(self, **kw):
        self.calls.append(("place_market_bracket", kw))
        # Mirror REAL TopstepXAdapter return shape (riga 1223-1232).
        # Keys "entry_id" / "stop_id" / "target_id" — NOT "entry_order_id"
        # etc. The wrapper must read these adapter-native keys.
        return {
            "success": True,
            "entry_id": "E-1",
            "stop_id": "S-1",
            "target_id": "T-1",
            "entry_price": 5010.75,
            "sl_price": 5008.00,
            "tp_price": 5015.50,
            "sl_source": "ATR",
            "error": None,
        }

    async def cancel_order(self, symbol, order_id):
        self.calls.append(("cancel_order", symbol, order_id))
        return True

    async def cancel_all_orders_for_contract(self, symbol):
        self.calls.append(("cancel_all_orders_for_contract", symbol))
        return {"cancelled_count": 3, "order_ids": [1, 2, 3], "error": None}

    async def close_position(self, symbol, size=None, bracket_order_ids=None):
        self.calls.append(("close_position", symbol, size))
        return {"success": True, "orderId": "CLOSE-1", "mode": "full"}

    async def modify_sl(self, symbol, stop_order_id, new_sl):
        self.calls.append(("modify_sl", symbol, stop_order_id, new_sl))
        return True


# ============================================================
# helpers
# ============================================================

def _make_broker(monkey_adapter: FakeAdapter):
    """Build a TopstepXBroker but with a FakeAdapter instead of real one."""
    b = topstepx_v16.TopstepXBroker(["MES", "MNQ"])
    b._adapter = monkey_adapter
    return b


def _run(coro):
    return asyncio.run(coro)


# ============================================================
# tests
# ============================================================

def test_connect_disconnect_is_connected():
    fake = FakeAdapter()
    b = _make_broker(fake)
    assert _run(b.is_connected()) is False
    assert _run(b.connect()) is True
    assert _run(b.is_connected()) is True
    _run(b.disconnect())
    assert _run(b.is_connected()) is False
    print("OK test_connect_disconnect_is_connected")


def test_get_last_price():
    b = _make_broker(FakeAdapter())
    p = _run(b.get_last_price("MES"))
    assert p == 5012.25
    print("OK test_get_last_price")


def test_positions_get_translates_dict_to_position():
    b = _make_broker(FakeAdapter())
    pos = _run(b.positions_get())
    assert len(pos) == 1
    assert isinstance(pos[0], Position)
    assert pos[0].symbol == "MES"
    assert pos[0].direction == "BUY"
    assert pos[0].contracts == 2
    assert pos[0].avg_price == 5010.50
    assert pos[0].raw is not None
    print("OK test_positions_get_translates_dict_to_position")


def test_place_market_bracket_success():
    b = _make_broker(FakeAdapter())
    r = _run(b.place_market_bracket(
        "MES", "BUY", 2, 5008.0, 5015.5,
        sl_ticks=11, tp_ticks=19, sl_source="ATR",
    ))
    assert isinstance(r, OrderResult)
    assert r.success is True
    assert r.entry_price == 5010.75
    assert r.sl_price == 5008.0
    assert r.tp_price == 5015.5
    assert r.entry_id == "E-1"
    assert r.stop_id == "S-1"
    assert r.target_id == "T-1"
    print("OK test_place_market_bracket_success")


def test_place_market_bracket_adapter_native_keys_regression():
    """
    BUG 8 regression: adapter returns {"entry_id","stop_id","target_id"}
    (V15 TopstepXAdapter riga 1223-1232). The wrapper MUST read these
    adapter-native keys, not the historical "*_order_id" alternates.
    Pre-fix: wrapper looked for "stop_order_id"/"target_order_id" only,
    so a successful bracket landed in OrderResult with stop_id=None and
    target_id=None — and MOVE_SL silently skipped at orchestrator level
    (active.entry.stop_order_id was None).
    """
    fake = FakeAdapter()

    # Explicit adapter-native shape — matches the real V15 adapter.
    async def native(**kw):
        return {
            "success": True,
            "entry_id": "ID-ENTRY-7",
            "stop_id": "ID-STOP-7",
            "target_id": "ID-TARGET-7",
            "entry_price": 4582.7,
            "sl_price": 4591.2,
            "tp_price": 4514.5,
        }
    fake.place_market_bracket = native

    b = _make_broker(fake)
    r = _run(b.place_market_bracket("MGC", "SELL", 1, 4591.2, 4514.5))
    assert r.success is True
    assert r.entry_id == "ID-ENTRY-7"
    assert r.stop_id == "ID-STOP-7"        # Pre-fix: was None
    assert r.target_id == "ID-TARGET-7"    # Pre-fix: was None
    assert r.entry_price == 4582.7
    assert r.sl_price == 4591.2
    assert r.tp_price == 4514.5
    print("OK test_place_market_bracket_adapter_native_keys_regression")


def test_place_market_bracket_failure_dict():
    fake = FakeAdapter()

    async def fail(**kw):
        return {"success": False, "error": "no contract cached for FOO"}
    fake.place_market_bracket = fail

    b = _make_broker(fake)
    r = _run(b.place_market_bracket("FOO", "BUY", 1, 1.0, 2.0))
    assert r.success is False
    assert "no contract" in (r.error or "")
    print("OK test_place_market_bracket_failure_dict")


def test_place_market_bracket_exception():
    fake = FakeAdapter()

    async def boom(**kw):
        raise RuntimeError("network down")
    fake.place_market_bracket = boom

    b = _make_broker(fake)
    r = _run(b.place_market_bracket("MES", "BUY", 1, 1.0, 2.0))
    assert r.success is False
    assert "network down" in (r.error or "")
    print("OK test_place_market_bracket_exception")


def test_cancel_order_success_and_failure():
    fake = FakeAdapter()
    b = _make_broker(fake)
    r = _run(b.cancel_order("MES", "12345"))
    assert isinstance(r, CancelResult)
    assert r.success is True
    assert r.order_id == "12345"

    async def fail(symbol, order_id):
        return False
    fake.cancel_order = fail
    r = _run(b.cancel_order("MES", "12345"))
    assert r.success is False
    assert r.error
    print("OK test_cancel_order_success_and_failure")


def test_cancel_all_for_symbol_returns_count():
    b = _make_broker(FakeAdapter())
    n = _run(b.cancel_all_for_symbol("MES"))
    assert n == 3
    print("OK test_cancel_all_for_symbol_returns_count")


def test_close_position_success():
    b = _make_broker(FakeAdapter())
    r = _run(b.close_position("MES"))
    assert r.success is True
    assert r.entry_id == "CLOSE-1"
    print("OK test_close_position_success")


def test_close_position_failure():
    fake = FakeAdapter()

    async def fail(symbol, size=None, bracket_order_ids=None):
        return {"success": False, "errorMessage": "position not found"}
    fake.close_position = fail

    b = _make_broker(fake)
    r = _run(b.close_position("MES"))
    assert r.success is False
    assert "position not found" in (r.error or "")
    print("OK test_close_position_failure")


def test_modify_stop_success():
    fake = FakeAdapter()
    b = _make_broker(fake)
    r = _run(b.modify_stop("MES", "12345", 5009.25))
    assert r.success is True
    assert r.sl_price == 5009.25
    assert r.stop_id == "12345"
    assert any(c[0] == "modify_sl" and c[1] == "MES" for c in fake.calls)
    print("OK test_modify_stop_success")


# ============================================================
# main
# ============================================================

if __name__ == "__main__":
    test_connect_disconnect_is_connected()
    test_get_last_price()
    test_positions_get_translates_dict_to_position()
    test_place_market_bracket_success()
    test_place_market_bracket_adapter_native_keys_regression()
    test_place_market_bracket_failure_dict()
    test_place_market_bracket_exception()
    test_cancel_order_success_and_failure()
    test_cancel_all_for_symbol_returns_count()
    test_close_position_success()
    test_close_position_failure()
    test_modify_stop_success()
    print("\n=== all topstepx_v16 wrapper smoke tests passed ===")
