from decimal import Decimal

from binance_quant.futures_execution import FuturesExecutionEngine, build_futures_client_order_id, round_down
from binance_quant.futures_models import (
    FuturesAction,
    FuturesOrderSide,
    FuturesPrediction,
    FuturesRiskDecision,
    FuturesSymbolMetadata,
)
from binance_quant.models import TradingMode
from binance_quant.storage import Storage


class FakeFuturesExchange:
    def __init__(self, protection_fails=False, rollback_fails=False, open_fails=False, open_status="FILLED"):
        self.calls = []
        self.protection_fails = protection_fails
        self.rollback_fails = rollback_fails
        self.open_fails = open_fails
        self.open_status = open_status

    def change_margin_type(self, symbol, margin_type="ISOLATED"):
        self.calls.append(("change_margin_type", symbol, margin_type))
        return {"code": 200}

    def change_leverage(self, symbol, leverage):
        self.calls.append(("change_leverage", symbol, leverage))
        return {"leverage": leverage}

    def create_market_order(self, symbol, side, quantity, client_order_id, *, reduce_only):
        self.calls.append(("create_market_order", symbol, side, quantity, client_order_id, reduce_only))
        if not reduce_only and self.open_fails:
            raise RuntimeError("open timeout signedUrl=https://example.test?signature=SECRET")
        if reduce_only and self.rollback_fails:
            raise RuntimeError("rollback rejected signedUrl=https://example.test?signature=SECRET")
        executed_qty = str(quantity) if self.open_status == "FILLED" else "0"
        return {"status": self.open_status, "clientOrderId": client_order_id, "executedQty": executed_qty}

    def query_order(self, symbol, client_order_id):
        self.calls.append(("query_order", symbol, client_order_id))
        return {"status": "FILLED", "clientOrderId": client_order_id, "executedQty": "0.002"}

    def create_conditional_algo_order(self, symbol, side, order_type, trigger_price, quantity, client_algo_id):
        self.calls.append(("create_conditional_algo_order", symbol, side, order_type, trigger_price, quantity, client_algo_id))
        if self.protection_fails:
            raise RuntimeError("algo rejected")
        return {"algoId": 1, "clientAlgoId": client_algo_id, "status": "NEW"}


def metadata():
    return FuturesSymbolMetadata("BTCUSDT", "BTC", "USDT", Decimal("0.001"), Decimal("0.001"), Decimal("0.1"), Decimal("100"))


def approved_open_prediction():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.88"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    return FuturesRiskDecision(True, "approved", prediction, leverage=2, margin_amount=Decimal("60"), notional=Decimal("120"))


def approved_short_prediction():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_SHORT,
        Decimal("0.88"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("63000"),
        take_profit_price=Decimal("59000"),
    )
    return FuturesRiskDecision(True, "approved", prediction, leverage=2, margin_amount=Decimal("60"), notional=Decimal("120"))


def test_round_down_uses_exchange_step_size():
    assert round_down(Decimal("0.0029"), Decimal("0.001")) == Decimal("0.002")


def test_futures_execution_dry_run_records_no_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.DRY_RUN, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "dry_run"
    assert exchange.calls == []
    [event] = storage.list_futures_order_events(1)
    assert event["event_type"] == "dry_run_open"


def test_futures_execution_live_open_places_market_and_two_algo_orders(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "protected"
    assert exchange.calls[0] == ("change_margin_type", "BTCUSDT", "ISOLATED")
    assert exchange.calls[1] == ("change_leverage", "BTCUSDT", 2)
    assert exchange.calls[2][0] == "create_market_order"
    assert exchange.calls[2][2] is FuturesOrderSide.BUY
    assert exchange.calls[3][2] is FuturesOrderSide.SELL
    assert exchange.calls[3][3] == "STOP_MARKET"
    assert exchange.calls[4][3] == "TAKE_PROFIT_MARKET"


def test_futures_execution_queries_order_when_open_market_order_is_not_filled_yet(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(open_status="NEW")
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "protected"
    assert any(call[0] == "query_order" for call in exchange.calls)
    events = storage.list_futures_order_events(1)
    assert [event["event_type"] for event in events[:2]] == ["open", "open_query"]
    assert events[1]["status"] == "FILLED"


def test_futures_execution_live_short_open_maps_entry_and_protection_sides(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_SHORT", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_short_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "protected"
    assert exchange.calls[2][0] == "create_market_order"
    assert exchange.calls[2][2] is FuturesOrderSide.SELL
    assert exchange.calls[3][2] is FuturesOrderSide.BUY
    assert exchange.calls[4][2] is FuturesOrderSide.BUY


def test_futures_execution_rejects_unapproved_decision_without_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "rejected", "risk rejected", raw={})
    exchange = FakeFuturesExchange()
    decision = FuturesRiskDecision(
        False,
        "risk rejected",
        approved_open_prediction().prediction,
        leverage=2,
        margin_amount=Decimal("60"),
        notional=Decimal("120"),
    )
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, decision, metadata(), reference_price=Decimal("60000"))

    assert result.status == "rejected"
    assert result.reason == "risk rejected"
    assert exchange.calls == []


def test_futures_execution_rejects_non_open_action_without_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "HOLD", Decimal("0.88"), "predicted", "hold", raw={})
    exchange = FakeFuturesExchange()
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.HOLD, Decimal("0.88"), "hold")
    decision = FuturesRiskDecision(True, "approved", prediction, leverage=2, margin_amount=Decimal("60"), notional=Decimal("120"))
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, decision, metadata(), reference_price=Decimal("60000"))

    assert result.status == "rejected"
    assert result.reason == "unsupported futures open action"
    assert exchange.calls == []


def test_futures_execution_rejects_zero_quantity_without_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    prediction = approved_open_prediction().prediction
    decision = FuturesRiskDecision(True, "approved", prediction, leverage=2, margin_amount=Decimal("5"), notional=Decimal("1"))
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, decision, metadata(), reference_price=Decimal("60000"))

    assert result.status == "rejected"
    assert result.reason == "invalid execution quantity"
    assert exchange.calls == []


def test_futures_execution_rejects_invalid_reference_price_without_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("0"))

    assert result.status == "rejected"
    assert result.reason == "invalid reference price"
    assert exchange.calls == []


def test_futures_execution_records_open_unknown_when_market_order_raises(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(open_fails=True)
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "open_unknown"
    assert result.reason == "open order status unknown"
    events = storage.list_futures_order_events(1)
    [event] = events
    assert event["event_type"] == "open_unknown"
    assert event["client_order_id"]
    assert event["status"] == "UNKNOWN"
    assert "open order status unknown" in event["raw_json"]
    assert "RuntimeError" in event["raw_json"]
    assert "SECRET" not in event["raw_json"]
    assert "signature" not in event["raw_json"]
    assert "signedUrl" not in event["raw_json"]


def test_futures_execution_rolls_back_when_protection_fails(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(protection_fails=True)
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "rollback_completed"
    rollback_call = exchange.calls[-1]
    assert rollback_call[0] == "create_market_order"
    assert rollback_call[2] is FuturesOrderSide.SELL
    assert rollback_call[5] is True


def test_futures_execution_returns_rollback_failed_when_rollback_order_fails(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(protection_fails=True, rollback_fails=True)
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "rollback_failed"
    assert result.reason == "protection failed; rollback failed"
    rollback_call = exchange.calls[-1]
    assert rollback_call[0] == "create_market_order"
    assert rollback_call[2] is FuturesOrderSide.SELL
    assert rollback_call[5] is True
    events = storage.list_futures_order_events(1)
    assert events[-1]["event_type"] == "rollback_failed"
    assert events[-1]["status"] == "CRITICAL"


def test_futures_execution_sanitizes_rollback_failure_audit_payload(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(protection_fails=True, rollback_fails=True)
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "rollback_failed"
    events = storage.list_futures_order_events(1)
    rollback_raw = events[-1]["raw_json"]
    assert "rollback request failed" in rollback_raw
    assert "RuntimeError" in rollback_raw
    assert "SECRET" not in rollback_raw
    assert "signature" not in rollback_raw
    assert "signedUrl" not in rollback_raw


def test_futures_execution_partial_close_without_protection_prices_reports_failure(tmp_path):
    class CloseExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CloseExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.88"), "risk", close_percent=50)

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop", "bqf-tp"),
        existing_stop_loss_price=None,
        existing_take_profit_price=None,
    )

    assert result.status == "close_protection_failed"
    assert ("cancel_algo_order", "bqf-stop") in exchange.calls
    assert ("cancel_algo_order", "bqf-tp") in exchange.calls
    close_call = next(call for call in exchange.calls if call[0] == "create_market_order")
    assert close_call[2] is FuturesOrderSide.SELL
    assert close_call[3] == Decimal("0.002")
    assert close_call[5] is True
    events = storage.list_futures_order_events(1)
    assert events[-1]["event_type"] == "close_protection_failed"


def test_futures_execution_partial_close_recreates_remaining_protection(tmp_path):
    class CloseExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CloseExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.CLOSE,
        Decimal("0.88"),
        "risk",
        close_percent=50,
    )

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop", "bqf-tp"),
        existing_stop_loss_price=Decimal("59000"),
        existing_take_profit_price=Decimal("63000"),
    )

    assert result.status == "partially_closed"
    protection_calls = [call for call in exchange.calls if call[0] == "create_conditional_algo_order"]
    assert [call[3] for call in protection_calls] == ["STOP_MARKET", "TAKE_PROFIT_MARKET"]
    assert all(call[5] == Decimal("0.002") for call in protection_calls)


def test_futures_execution_close_stops_when_cancel_algo_fails(tmp_path):
    class CancelFailingExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            raise RuntimeError("cancel failed signedUrl=https://example.test?signature=SECRET")

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CancelFailingExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.88"), "risk", close_percent=50)

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop",),
        existing_stop_loss_price=Decimal("59000"),
        existing_take_profit_price=Decimal("63000"),
    )

    assert result.status == "close_failed"
    assert not any(call[0] == "create_market_order" for call in exchange.calls)
    [event] = storage.list_futures_order_events(1)
    assert event["event_type"] == "cancel_algo_failed"
    assert "protection cancel failed" in event["raw_json"]
    assert "SECRET" not in event["raw_json"]
    assert "signature" not in event["raw_json"]
    assert "signedUrl" not in event["raw_json"]


def test_futures_execution_records_close_unknown_when_market_order_raises(tmp_path):
    class CloseUnknownExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

        def create_market_order(self, symbol, side, quantity, client_order_id, *, reduce_only):
            self.calls.append(("create_market_order", symbol, side, quantity, client_order_id, reduce_only))
            raise RuntimeError("close timeout signedUrl=https://example.test?signature=SECRET")

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CloseUnknownExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.88"), "risk", close_percent=100)

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=(),
        existing_stop_loss_price=None,
        existing_take_profit_price=None,
    )

    assert result.status == "close_unknown"
    [event] = storage.list_futures_order_events(1)
    assert event["event_type"] == "close_unknown"
    assert event["client_order_id"]
    assert event["status"] == "UNKNOWN"
    assert "close order status unknown" in event["raw_json"]
    assert "SECRET" not in event["raw_json"]
    assert "signature" not in event["raw_json"]
    assert "signedUrl" not in event["raw_json"]


def test_futures_execution_partial_close_reports_remaining_protection_failure(tmp_path):
    class ProtectionFailingExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

        def create_conditional_algo_order(self, symbol, side, order_type, trigger_price, quantity, client_algo_id):
            self.calls.append(
                ("create_conditional_algo_order", symbol, side, order_type, trigger_price, quantity, client_algo_id)
            )
            raise RuntimeError("algo failed signedUrl=https://example.test?signature=SECRET")

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = ProtectionFailingExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.88"), "risk", close_percent=50)

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop", "bqf-tp"),
        existing_stop_loss_price=Decimal("59000"),
        existing_take_profit_price=Decimal("63000"),
    )

    assert result.status == "close_protection_failed"
    events = storage.list_futures_order_events(1)
    assert events[-1]["event_type"] == "close_protection_failed"
    assert "remaining protection request failed" in events[-1]["raw_json"]
    assert "SECRET" not in events[-1]["raw_json"]
    assert "signature" not in events[-1]["raw_json"]
    assert "signedUrl" not in events[-1]["raw_json"]
