from decimal import Decimal
import json

import pytest

import binance_quant.auto_buy as auto_buy
from binance_quant.models import OrderSide, SymbolMetadata
from binance_quant.storage import Storage


METADATA = SymbolMetadata(
    "BTCUSDT",
    "BTC",
    "USDT",
    Decimal("5"),
    Decimal("0.00001"),
    Decimal("0.00001"),
    Decimal("0.01"),
    Decimal("0.01"),
)
CANDIDATE = auto_buy.AutoBuyCandidate("BTCUSDT", Decimal("4.5"), Decimal("50000"), Decimal("5"), ("momentum", "volume"))


class FakeExchange:
    def __init__(
        self,
        buy_payload,
        oco_error: Exception | None = None,
        rollback_error: Exception | None = None,
        oco_payload=None,
        rollback_payload=None,
    ):
        self.buy_payload = buy_payload
        self.oco_error = oco_error
        self.rollback_error = rollback_error
        self.oco_payload = oco_payload
        self.rollback_payload = rollback_payload
        self.calls = []

    def create_market_order(self, symbol, side, quote_amount, client_order_id):
        self.calls.append(("buy", symbol, side, quote_amount, client_order_id))
        if not isinstance(self.buy_payload, dict):
            return self.buy_payload
        return {"clientOrderId": client_order_id} | self.buy_payload

    def create_oco_sell(
        self,
        symbol,
        quantity,
        take_profit_price,
        stop_price,
        stop_limit_price,
        list_client_order_id,
        take_profit_client_order_id,
        stop_client_order_id,
    ):
        self.calls.append(
            (
                "oco",
                symbol,
                quantity,
                take_profit_price,
                stop_price,
                stop_limit_price,
                list_client_order_id,
                take_profit_client_order_id,
                stop_client_order_id,
            )
        )
        if self.oco_error:
            raise self.oco_error
        if self.oco_payload is not None and not isinstance(self.oco_payload, dict):
            return self.oco_payload
        if self.oco_payload is not None:
            return {"listClientOrderId": list_client_order_id} | self.oco_payload
        return {
            "symbol": symbol,
            "contingencyType": "OCO",
            "listStatusType": "EXEC_STARTED",
            "listOrderStatus": "EXECUTING",
            "listClientOrderId": list_client_order_id,
            "orders": [
                {"symbol": symbol, "clientOrderId": take_profit_client_order_id},
                {"symbol": symbol, "clientOrderId": stop_client_order_id},
            ],
            "orderReports": [
                {
                    "symbol": symbol,
                    "side": "SELL",
                    "type": "LIMIT_MAKER",
                    "status": "NEW",
                    "clientOrderId": take_profit_client_order_id,
                    "origQty": str(quantity),
                    "price": str(take_profit_price),
                },
                {
                    "symbol": symbol,
                    "side": "SELL",
                    "type": "STOP_LOSS_LIMIT",
                    "status": "NEW",
                    "clientOrderId": stop_client_order_id,
                    "origQty": str(quantity),
                    "price": str(stop_limit_price),
                    "stopPrice": str(stop_price),
                },
            ],
        }

    def create_market_sell_quantity(self, symbol, quantity, client_order_id):
        self.calls.append(("rollback", symbol, quantity, client_order_id))
        if self.rollback_error:
            raise self.rollback_error
        if self.rollback_payload is not None and not isinstance(self.rollback_payload, dict):
            return self.rollback_payload
        if self.rollback_payload is not None:
            return {"clientOrderId": client_order_id} | self.rollback_payload
        return {"symbol": symbol, "status": "FILLED", "executedQty": str(quantity), "clientOrderId": client_order_id}


class FailingFirstEventStorage:
    def __init__(self, wrapped):
        self.wrapped = wrapped
        self.event_attempts = 0

    def record_auto_buy_event(self, *args, **kwargs):
        self.event_attempts += 1
        if self.event_attempts == 1:
            raise RuntimeError("audit database unavailable: https://internal.example/signature=abc123")
        return self.wrapped.record_auto_buy_event(*args, **kwargs)

    def __getattr__(self, name):
        return getattr(self.wrapped, name)


def storage(tmp_path):
    audit = Storage(tmp_path / "audit.sqlite3")
    audit.initialize()
    return audit


def filled_buy(executed_qty: str = "0.001", quote_qty: str = "50") -> dict:
    return {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": executed_qty, "cummulativeQuoteQty": quote_qty}


def engine(exchange, audit):
    return auto_buy.AutoBuyExecutionEngine(
        exchange=exchange,
        storage=audit,
        stop_loss_percent=Decimal("2"),
        stop_limit_buffer_percent=Decimal("0.2"),
        take_profit_percent=Decimal("3"),
    )


def failed_oco_then(event_type: str) -> list[str]:
    return [
        "market_buy",
        "protective_oco_failed",
        "protective_oco_failed",
        "protective_oco_failed",
        event_type,
    ]


def test_successful_execution_buys_then_places_protective_oco(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy())

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "protected"
    assert result.symbol == "BTCUSDT"
    assert result.reason == "protective oco accepted"
    assert exchange.calls[0][:4] == ("buy", "BTCUSDT", OrderSide.BUY, Decimal("50"))
    assert exchange.calls[0][4].startswith("bqab-buy-")
    assert exchange.calls[1][:6] == (
        "oco",
        "BTCUSDT",
        Decimal("0.001"),
        Decimal("51500.00"),
        Decimal("49000.00"),
        Decimal("48902.00"),
    )
    assert exchange.calls[1][6].startswith("bqab-oco-")
    assert exchange.calls[1][7].startswith("bqab-tp-")
    assert exchange.calls[1][8].startswith("bqab-stop-")

    [run] = audit.list_auto_buy_runs()
    assert run["id"] == result.run_id
    assert run["status"] == "protected"
    assert run["score"] == "4.5"
    assert run["reason"] == "protective oco accepted"
    assert run["quote_amount"] == "50"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "protective_oco"]
    assert json.loads(events[0]["raw_json"])["executedQty"] == "0.001"


def test_execution_engine_preserves_legacy_positional_stop_arguments(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy())

    result = auto_buy.AutoBuyExecutionEngine(exchange, audit, Decimal("2"), Decimal("0.2")).execute(
        CANDIDATE,
        METADATA,
        Decimal("50"),
    )

    assert result.status == "protected"
    assert exchange.calls[1][:6] == (
        "oco",
        "BTCUSDT",
        Decimal("0.001"),
        Decimal("51500.00"),
        Decimal("49000.00"),
        Decimal("48902.00"),
    )


def test_base_asset_commission_is_excluded_from_protective_oco_quantity(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(
        filled_buy(executed_qty="0.12600000", quote_qty="19.96470000")
        | {
            "fills": [
                {
                    "price": "158.45000000",
                    "qty": "0.12600000",
                    "commission": "0.00012600",
                    "commissionAsset": "BTC",
                }
            ]
        }
    )

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("20"))

    assert result.status == "protected"
    assert exchange.calls[1][:3] == ("oco", "BTCUSDT", Decimal("0.12587"))


def test_non_base_asset_commission_does_not_reduce_protective_oco_quantity(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(
        filled_buy()
        | {
            "fills": [
                {
                    "price": "50000",
                    "qty": "0.001",
                    "commission": "0.1",
                    "commissionAsset": "BNB",
                }
            ]
        }
    )

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "protected"
    assert exchange.calls[1][:3] == ("oco", "BTCUSDT", Decimal("0.001"))


@pytest.mark.parametrize(
    "fills",
    [
        "not fills",
        [{"commission": "bad", "commissionAsset": "BTC"}],
        [{"commission": "0.002", "commissionAsset": "BTC"}],
    ],
)
def test_invalid_base_commission_records_rollback_failed_without_stop_or_rollback(tmp_path, fills):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy() | {"fills": fills})

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "invalid fill quantities"
    assert [call[0] for call in exchange.calls] == ["buy"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "rollback_failed"]


def test_audit_failure_after_market_buy_still_places_protective_oco(tmp_path):
    audit = FailingFirstEventStorage(storage(tmp_path))
    exchange = FakeExchange(filled_buy())

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "protected"
    assert [call[0] for call in exchange.calls] == ["buy", "oco"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "protected"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["protective_oco"]


def test_oco_failure_is_retried_until_accepted(tmp_path):
    class FlakyOcoExchange(FakeExchange):
        def __init__(self):
            super().__init__(filled_buy())
            self.oco_attempts = 0

        def create_oco_sell(self, *args):
            self.oco_attempts += 1
            if self.oco_attempts < 3:
                self.calls.append(("oco", *args))
                raise RuntimeError("temporary oco rejection")
            return super().create_oco_sell(*args)

    audit = storage(tmp_path)
    exchange = FlakyOcoExchange()

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "protected"
    assert result.reason == "protective oco accepted"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco"]
    list_client_order_ids = [call[6] for call in exchange.calls if call[0] == "oco"]
    assert len(set(list_client_order_ids)) == 3
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == [
        "market_buy",
        "protective_oco_failed",
        "protective_oco_failed",
        "protective_oco",
    ]


def test_unaccepted_oco_payload_is_retried_before_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_payload={"symbol": "BTCUSDT", "status": "REJECTED"})

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == [
        "market_buy",
        "protective_oco_failed",
        "protective_oco_failed",
        "protective_oco_failed",
        "rollback_sell",
    ]


def test_oco_failure_triggers_market_sell_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_error=RuntimeError("oco rejected"))

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    assert exchange.calls[-1][1:3] == ("BTCUSDT", Decimal("0.001"))
    assert exchange.calls[-1][3].startswith("bqab-rb-")
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_completed"
    assert [event["event_type"] for event in audit.list_auto_buy_events(result.run_id)] == failed_oco_then("rollback_sell")


@pytest.mark.parametrize("oco_payload", [{"symbol": "BTCUSDT", "status": "REJECTED"}, {"symbol": "BTCUSDT"}])
def test_unaccepted_oco_payload_triggers_market_sell_rollback(tmp_path, oco_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_payload=oco_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_completed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_sell")


def test_oco_result_payload_with_open_order_reports_is_accepted_as_protected(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(
        filled_buy(),
        oco_payload={
            "symbol": "BTCUSDT",
            "contingencyType": "OCO",
            "listStatusType": "EXEC_STARTED",
            "listOrderStatus": "EXECUTING",
            "orderReports": [
                {
                    "symbol": "BTCUSDT",
                    "side": "SELL",
                    "type": "LIMIT_MAKER",
                    "status": "NEW",
                    "origQty": "0.001",
                    "price": "51500.00",
                },
                {
                    "symbol": "BTCUSDT",
                    "side": "SELL",
                    "type": "STOP_LOSS_LIMIT",
                    "status": "NEW",
                    "origQty": "0.001",
                    "price": "48902.00",
                    "stopPrice": "49000.00",
                },
            ],
        },
    )

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "protected"
    assert [call[0] for call in exchange.calls] == ["buy", "oco"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "protective_oco"]


@pytest.mark.parametrize(
    "oco_payload",
    [
        {"symbol": "BTCUSDT", "listStatusType": "EXEC_STARTED", "orderReports": []},
        {
            "symbol": "BTCUSDT",
            "listStatusType": "EXEC_STARTED",
            "orderReports": [
                {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT_MAKER", "status": "NEW", "origQty": "0.001", "price": "51500.01"},
                {
                    "symbol": "BTCUSDT",
                    "side": "SELL",
                    "type": "STOP_LOSS_LIMIT",
                    "status": "NEW",
                    "origQty": "0.001",
                    "price": "48902.00",
                    "stopPrice": "49000.00",
                },
            ],
        },
        {
            "symbol": "BTCUSDT",
            "listStatusType": "EXEC_STARTED",
            "orderReports": [
                {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT_MAKER", "status": "NEW", "origQty": "0.001", "price": "51500.00"},
                {
                    "symbol": "BTCUSDT",
                    "side": "SELL",
                    "type": "STOP_LOSS_LIMIT",
                    "status": "NEW",
                    "origQty": "0.001",
                    "price": "48902.01",
                    "stopPrice": "49000.00",
                },
            ],
        },
    ],
)
def test_oco_result_payload_with_wrong_reports_triggers_market_sell_rollback(tmp_path, oco_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_payload=oco_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_sell")


def test_oco_failure_and_rollback_failure_records_rollback_failed(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(
        filled_buy(),
        oco_error=RuntimeError("oco rejected"),
        rollback_error=RuntimeError("rollback rejected"),
    )

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "protective oco failed; rollback failed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_failed"
    assert [event["event_type"] for event in audit.list_auto_buy_events(result.run_id)] == failed_oco_then("rollback_failed")


@pytest.mark.parametrize(
    "rollback_payload",
    [
        {"symbol": "BTCUSDT", "status": "EXPIRED", "executedQty": "0.001"},
        {"symbol": "BTCUSDT", "status": "FILLED"},
        {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "0"},
    ],
)
def test_unfilled_rollback_payload_records_critical_rollback_failed(tmp_path, rollback_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_error=RuntimeError("oco rejected"), rollback_payload=rollback_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "protective oco failed; rollback failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_failed")
    assert events[-1]["status"] == "CRITICAL"


def test_rollback_exception_records_critical_sanitized_event(tmp_path):
    audit = storage(tmp_path)
    sensitive_text = "rollback rejected https://internal.example?signature=secret&apiKey=abc"
    exchange = FakeExchange(
        filled_buy(),
        oco_error=RuntimeError("oco rejected"),
        rollback_error=RuntimeError(sensitive_text),
    )

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert events[-1]["event_type"] == "rollback_failed"
    assert events[-1]["status"] == "CRITICAL"
    raw = json.loads(events[-1]["raw_json"])
    assert raw == {"error_type": "RuntimeError", "message": "rollback failed"}
    assert sensitive_text not in events[-1]["raw_json"]


def test_rounded_executed_quantity_below_min_qty_records_rollback_failed_without_stop_or_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(executed_qty="0.00000999", quote_qty="0.4995"))

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "executed quantity below min quantity"
    assert [call[0] for call in exchange.calls] == ["buy"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "rollback_failed"]
    assert json.loads(events[1]["raw_json"]) == {
        "reason": "executed quantity below min quantity",
        "executedQty": "0.00000",
        "minQty": "0.00001",
    }


def test_malformed_buy_fill_records_rollback_failed_without_stop_or_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange({"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "bad", "cummulativeQuoteQty": "50"})

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "invalid fill quantities"
    assert [call[0] for call in exchange.calls] == ["buy"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_failed"
    assert [event["event_type"] for event in audit.list_auto_buy_events(result.run_id)] == ["market_buy", "rollback_failed"]


def test_non_dict_buy_response_records_rollback_failed_without_stop_or_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange("not a payload")

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "invalid fill quantities"
    assert [call[0] for call in exchange.calls] == ["buy"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["rollback_failed"]
    assert json.loads(events[0]["raw_json"]) == {"reason": "invalid fill quantities"}


def test_protective_oco_price_rounding_failure_rolls_back_after_buy(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy())
    bad_tick = SymbolMetadata(
        "BTCUSDT",
        "BTC",
        "USDT",
        Decimal("5"),
        Decimal("0.00001"),
        Decimal("0.00001"),
        Decimal("0"),
        Decimal("0.01"),
    )

    result = engine(exchange, audit).execute(CANDIDATE, bad_tick, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco calculation failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "rollback"]
    assert [event["event_type"] for event in audit.list_auto_buy_events(result.run_id)] == [
        "market_buy",
        "protective_oco_failed",
        "rollback_sell",
    ]


def test_non_dict_oco_response_triggers_market_sell_rollback(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_payload=["not", "a", "dict"])

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_sell")
    assert json.loads(events[1]["raw_json"]) == {"message": "protective oco response invalid"}


@pytest.mark.parametrize(
    "oco_payload",
    [
        {"symbol": "ETHUSDT", "listStatusType": "EXEC_STARTED"},
        {"symbol": "BTCUSDT", "listStatusType": "RESPONSE"},
        {
            "symbol": "BTCUSDT",
            "listStatusType": "EXEC_STARTED",
            "orderReports": [
                {"symbol": "BTCUSDT", "side": "BUY", "type": "LIMIT_MAKER", "status": "NEW", "origQty": "0.001", "price": "51500.00"}
            ],
        },
        {
            "symbol": "BTCUSDT",
            "listStatusType": "EXEC_STARTED",
            "orderReports": [
                {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT", "status": "NEW", "origQty": "0.001", "price": "51500.00"}
            ],
        },
        {
            "symbol": "BTCUSDT",
            "listStatusType": "EXEC_STARTED",
            "orderReports": [
                {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT_MAKER", "status": "NEW", "origQty": "0.002", "price": "51500.00"}
            ],
        },
    ],
)
def test_invalid_oco_response_fields_trigger_market_sell_rollback(tmp_path, oco_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_payload=oco_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "protective oco failed; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "oco", "oco", "oco", "rollback"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_sell")
    assert json.loads(events[1]["raw_json"]) == {"message": "protective oco response invalid"}


def test_non_dict_rollback_response_records_critical_rollback_failed(tmp_path):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_error=RuntimeError("oco rejected"), rollback_payload="not a payload")

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "protective oco failed; rollback failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_failed")
    assert events[-1]["status"] == "CRITICAL"
    assert json.loads(events[-1]["raw_json"]) == {"message": "rollback response invalid"}


@pytest.mark.parametrize(
    "rollback_payload",
    [
        {"symbol": "ETHUSDT", "status": "FILLED", "executedQty": "0.001"},
        {"symbol": "BTCUSDT", "status": "FILLED", "clientOrderId": "wrong-rb-id", "executedQty": "0.001"},
        {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "0.00099"},
    ],
)
def test_invalid_rollback_completion_response_records_critical_rollback_failed(tmp_path, rollback_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(filled_buy(), oco_error=RuntimeError("oco rejected"), rollback_payload=rollback_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "protective oco failed; rollback failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == failed_oco_then("rollback_failed")
    assert events[-1]["status"] == "CRITICAL"
    assert json.loads(events[-1]["raw_json"]) == {"message": "rollback response invalid"}


@pytest.mark.parametrize(
    "buy_payload",
    [
        {"symbol": "BTCUSDT", "status": "NEW", "executedQty": "0.001", "cummulativeQuoteQty": "50"},
        {"symbol": "ETHUSDT", "status": "FILLED", "executedQty": "0.001", "cummulativeQuoteQty": "50"},
        {
            "symbol": "BTCUSDT",
            "status": "FILLED",
            "clientOrderId": "wrong-buy-id",
            "executedQty": "0.001",
            "cummulativeQuoteQty": "50",
        },
        {
            "symbol": "BTCUSDT",
            "status": "FILLED",
            "side": "SELL",
            "executedQty": "0.001",
            "cummulativeQuoteQty": "50",
        },
        {
            "symbol": "BTCUSDT",
            "status": "FILLED",
            "type": "LIMIT",
            "executedQty": "0.001",
            "cummulativeQuoteQty": "50",
        },
    ],
)
def test_invalid_buy_response_identity_or_status_fails_without_stop_or_rollback(tmp_path, buy_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(buy_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "invalid buy response"
    assert [call[0] for call in exchange.calls] == ["buy"]
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "rollback_failed"]
    raw = json.loads(events[-1]["raw_json"])
    assert raw["reason"] == "invalid buy response"
    assert raw["buyPayload"] | buy_payload == raw["buyPayload"]


@pytest.mark.parametrize(
    "buy_payload",
    [
        {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "0.001"},
        {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "0.001", "cummulativeQuoteQty": "bad"},
        {"symbol": "BTCUSDT", "status": "FILLED", "executedQty": "0.001", "cummulativeQuoteQty": "0"},
    ],
)
def test_buy_response_with_sellable_qty_and_invalid_quote_qty_rolls_back(tmp_path, buy_payload):
    audit = storage(tmp_path)
    exchange = FakeExchange(buy_payload)

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_completed"
    assert result.reason == "invalid fill quantities; rollback completed"
    assert [call[0] for call in exchange.calls] == ["buy", "rollback"]
    assert exchange.calls[1][1:3] == ("BTCUSDT", Decimal("0.001"))
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["market_buy", "protective_oco_failed", "rollback_sell"]
    raw = json.loads(events[1]["raw_json"])
    assert raw["message"] == "invalid fill quantities"
    assert raw["buyPayload"] | buy_payload == raw["buyPayload"]


def test_buy_exception_returns_rollback_failed_and_records_sanitized_event(tmp_path):
    class RaisingBuyExchange(FakeExchange):
        def create_market_order(self, symbol, side, quote_amount, client_order_id):
            self.calls.append(("buy", symbol, side, quote_amount, client_order_id))
            raise RuntimeError("buy rejected https://internal.example?signature=secret&apiKey=abc")

    audit = storage(tmp_path)
    exchange = RaisingBuyExchange(filled_buy())

    result = engine(exchange, audit).execute(CANDIDATE, METADATA, Decimal("50"))

    assert result.status == "rollback_failed"
    assert result.reason == "buy submission failed"
    assert [call[0] for call in exchange.calls] == ["buy"]
    [run] = audit.list_auto_buy_runs()
    assert run["status"] == "rollback_failed"
    assert run["reason"] == "buy submission failed"
    events = audit.list_auto_buy_events(result.run_id)
    assert [event["event_type"] for event in events] == ["rollback_failed"]
    raw_json = events[0]["raw_json"]
    assert json.loads(raw_json) == {"message": "buy submission failed"}
    assert "internal.example" not in raw_json
    assert "signature" not in raw_json
    assert "apiKey" not in raw_json


@pytest.mark.parametrize(
    ("value", "increment", "expected"),
    [
        (Decimal("1.239"), Decimal("0.01"), Decimal("1.23")),
        (Decimal("0.001009"), Decimal("0.00001"), Decimal("0.00100")),
    ],
)
def test_round_down_quantizes_to_exchange_increment(value, increment, expected):
    assert auto_buy._round_down(value, increment) == expected


def test_round_down_rejects_non_positive_increment():
    with pytest.raises(ValueError, match="increment must be positive"):
        auto_buy._round_down(Decimal("1"), Decimal("0"))


@pytest.mark.parametrize(
    "payload",
    [
        {},
        {"executedQty": "0", "cummulativeQuoteQty": "50"},
        {"executedQty": "0.001", "cummulativeQuoteQty": "0"},
        {"executedQty": "bad", "cummulativeQuoteQty": "50"},
        {"executedQty": "NaN", "cummulativeQuoteQty": "50"},
    ],
)
def test_average_fill_price_rejects_malformed_live_payloads(payload):
    with pytest.raises(ValueError, match="invalid fill quantities"):
        auto_buy._average_fill_price(payload)


def test_average_fill_price_uses_executed_and_quote_quantities():
    assert auto_buy._average_fill_price({"executedQty": "0.001", "cummulativeQuoteQty": "50"}) == Decimal("5E+4")


@pytest.mark.parametrize(
    ("stop_loss_percent", "stop_limit_buffer_percent", "message"),
    [
        (Decimal("0"), Decimal("0.2"), "stop_loss_percent must be positive"),
        (Decimal("NaN"), Decimal("0.2"), "stop_loss_percent must be finite"),
        ("2", Decimal("0.2"), "stop_loss_percent must be a Decimal"),
        (Decimal("2"), Decimal("0"), "stop_limit_buffer_percent must be positive"),
        (Decimal("2"), Decimal("Infinity"), "stop_limit_buffer_percent must be finite"),
        (Decimal("2"), "0.2", "stop_limit_buffer_percent must be a Decimal"),
        (Decimal("100"), Decimal("0.2"), "stop_loss_percent must be less than 100"),
        (Decimal("2"), Decimal("100"), "stop_limit_buffer_percent must be less than 100"),
    ],
)
def test_auto_buy_execution_engine_validates_percentages(stop_loss_percent, stop_limit_buffer_percent, message):
    with pytest.raises(ValueError, match=message):
        auto_buy.AutoBuyExecutionEngine(
            exchange=FakeExchange(filled_buy()),
            storage=None,
            stop_loss_percent=stop_loss_percent,
            stop_limit_buffer_percent=stop_limit_buffer_percent,
        )
