import json
from decimal import Decimal
from urllib.parse import parse_qsl, urlencode

import httpx
import pytest

from binance_quant.audit_log import AuditLogger
from binance_quant.exchange import BinanceSpotClient, sign_query
from binance_quant.models import AccountSnapshot, OrderSide, SymbolMetadata


def read_audit_events(path):
    events = []
    for block in path.read_text(encoding="utf-8").split("=" * 80):
        if "\nJSON:\n" not in block:
            continue
        raw_json = block.split("\nJSON:\n", 1)[1].splitlines()[0]
        events.append(json.loads(raw_json))
    return events


def test_sign_query_uses_hmac_sha256():
    signature = sign_query(
        "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559",
        "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
    )

    assert signature == "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"


def test_client_uses_testnet_base_url():
    client = BinanceSpotClient(api_key="key", api_secret="secret")

    assert client.base_url == "https://testnet.binance.vision/api"


def test_client_selects_fastest_successful_base_url_before_requests():
    requests = []
    durations = iter([0.30, 0.05, 0.10])
    clock_value = 0.0

    def clock():
        return clock_value

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal clock_value
        requests.append(request)
        clock_value += next(durations)
        if request.url.host == "api.binance.com":
            return httpx.Response(200, json={"serverTime": 1})
        if request.url.host == "api1.binance.com":
            return httpx.Response(200, json={"serverTime": 2})
        return httpx.Response(200, json={"serverTime": 3})

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=(
            "https://api.binance.com/api",
            "https://api1.binance.com/api",
            "https://api2.binance.com/api",
        ),
        http_client=http_client,
        endpoint_probe_clock=clock,
    )

    assert client.base_url == "https://api1.binance.com/api"
    assert [request.url.host for request in requests] == ["api.binance.com", "api1.binance.com", "api2.binance.com"]
    assert all(request.url.path.endswith("/api/v3/time") for request in requests)


def test_client_skips_failed_endpoint_probe_and_uses_successful_endpoint():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.host == "api.binance.com":
            raise httpx.ConnectTimeout("timeout")
        return httpx.Response(200, json={"serverTime": 2})

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
    )

    assert client.base_url == "https://api1.binance.com/api"
    assert [request.url.host for request in requests] == ["api.binance.com", "api1.binance.com"]


def test_client_falls_back_to_first_base_url_when_all_endpoint_probes_fail():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        raise httpx.ConnectTimeout("timeout")

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
    )

    assert client.base_url == "https://api.binance.com/api"
    assert [request.url.host for request in requests] == ["api.binance.com", "api1.binance.com"]


def test_signed_request_refreshes_server_time_when_timestamp_omitted():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.path.endswith("/v3/time"):
            return httpx.Response(200, json={"serverTime": 98500 + len(requests)})
        return httpx.Response(200, json=[])

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
        endpoint_probe_clock=lambda: 100.0,
    )

    result = client.open_orders("BTCUSDT")

    query_pairs = parse_qsl(requests[-1].url.query.decode(), keep_blank_values=True)
    query = dict(query_pairs)
    assert result == []
    assert query["timestamp"] == "98503"
    assert query["symbol"] == "BTCUSDT"


def test_signed_get_retries_next_base_url_after_transport_error():
    requests = []
    durations = iter([0.01, 0.02])
    clock_value = 0.0

    def clock():
        return clock_value

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal clock_value
        requests.append(request)
        if request.url.path.endswith("/v3/time"):
            clock_value += next(durations)
            return httpx.Response(200, json={"serverTime": 1000})
        if request.url.host == "api.binance.com":
            raise httpx.ConnectTimeout("handshake timeout")
        return httpx.Response(
            200,
            json={"balances": [{"asset": "USDT", "free": "42", "locked": "0.0"}]},
        )

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)
    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
        endpoint_probe_clock=clock,
    )

    snapshot = client.account(timestamp_ms=1)

    account_requests = [request for request in requests if request.url.path.endswith("/v3/account")]
    assert [request.url.host for request in account_requests] == ["api.binance.com", "api1.binance.com"]
    assert client.base_url == "https://api1.binance.com/api"
    assert snapshot.free("USDT") == Decimal("42")


def test_public_get_retries_next_base_url_after_transport_error():
    requests = []
    durations = iter([0.01, 0.02])
    clock_value = 0.0

    def clock():
        return clock_value

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal clock_value
        requests.append(request)
        if request.url.path.endswith("/v3/time"):
            clock_value += next(durations)
            return httpx.Response(200, json={"serverTime": 1000})
        if request.url.host == "api.binance.com":
            raise httpx.ConnectError("ssl eof")
        return httpx.Response(200, json={"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"})

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)
    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
        endpoint_probe_clock=clock,
    )

    result = client.symbol_order_book_ticker("BTCUSDT")

    ticker_requests = [request for request in requests if request.url.path.endswith("/v3/ticker/bookTicker")]
    assert [request.url.host for request in ticker_requests] == ["api.binance.com", "api1.binance.com"]
    assert client.base_url == "https://api1.binance.com/api"
    assert result == {"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"}


def test_public_get_raises_last_transport_error_after_all_base_urls_fail():
    requests = []
    durations = iter([0.01, 0.02])
    clock_value = 0.0

    def clock():
        return clock_value

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal clock_value
        requests.append(request)
        if request.url.path.endswith("/v3/time"):
            clock_value += next(durations)
            return httpx.Response(200, json={"serverTime": 1000})
        raise httpx.ConnectTimeout(f"timeout {request.url.host}")

    transport = httpx.MockTransport(handler)
    http_client = httpx.Client(transport=transport)
    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://api.binance.com/api", "https://api1.binance.com/api"),
        http_client=http_client,
        endpoint_probe_clock=clock,
    )

    with pytest.raises(httpx.ConnectTimeout, match="timeout api1.binance.com"):
        client.symbol_order_book_ticker("BTCUSDT")

    ticker_requests = [request for request in requests if request.url.path.endswith("/v3/ticker/bookTicker")]
    assert [request.url.host for request in ticker_requests] == ["api.binance.com", "api1.binance.com"]


def test_parse_account_snapshot():
    snapshot = BinanceSpotClient.parse_account(
        {
            "balances": [
                {"asset": "BTC", "free": "0.1", "locked": "0.0"},
                {"asset": "USDT", "free": "100", "locked": "0.0"},
            ]
        }
    )

    assert isinstance(snapshot, AccountSnapshot)
    assert snapshot.free("USDT") == Decimal("100")


def test_account_sends_signed_params_in_get_query():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(
            200,
            json={"balances": [{"asset": "USDT", "free": "42", "locked": "0.0"}]},
        )

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    snapshot = client.account(timestamp_ms=1)

    query = requests[0].url.query.decode()
    body = requests[0].content.decode()
    query_pairs = parse_qsl(query, keep_blank_values=True)
    signature = dict(query_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in query_pairs if key != "signature"])
    assert requests[0].method == "GET"
    assert "timestamp=1" in query
    assert "recvWindow=5000" in query
    assert "signature=" in query
    assert sign_query(sent_without_signature, "secret") == signature
    assert "timestamp=1" not in body
    assert "recvWindow=5000" not in body
    assert "signature=" not in body
    assert isinstance(snapshot, AccountSnapshot)
    assert snapshot.free("USDT") == Decimal("42")


def test_exchange_info_parses_symbol_metadata():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "BTCUSDT",
                        "baseAsset": "BTC",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.00001000", "stepSize": "0.00001000"},
                            {"filterType": "MIN_NOTIONAL", "minNotional": "5.00000000"},
                        ],
                    }
                ]
            },
        )

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    metadata = client.exchange_info("btcusdt")

    assert isinstance(metadata, SymbolMetadata)
    assert metadata.symbol == "BTCUSDT"
    assert metadata.base_asset == "BTC"
    assert metadata.quote_asset == "USDT"
    assert metadata.min_qty == Decimal("0.00001000")
    assert metadata.step_size == Decimal("0.00001000")
    assert metadata.min_notional == Decimal("5.00000000")
    assert "symbol=BTCUSDT" in requests[0].url.query.decode()


def test_exchange_info_parses_notional_filter_min_notional():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "ETHUSDT",
                        "baseAsset": "ETH",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.00010000", "stepSize": "0.00010000"},
                            {"filterType": "NOTIONAL", "minNotional": "10"},
                        ],
                    }
                ]
            },
        )

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    metadata = client.exchange_info("ethusdt")

    assert metadata.min_notional == Decimal("10")


def test_exchange_info_parses_price_filter_metadata():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "SOLUSDT",
                        "baseAsset": "SOL",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.01000000", "stepSize": "0.01000000"},
                            {"filterType": "PRICE_FILTER", "minPrice": "0.01000000", "tickSize": "0.01000000"},
                            {"filterType": "MIN_NOTIONAL", "minNotional": "5.00000000"},
                        ],
                    }
                ]
            },
        )

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    metadata = client.exchange_info("solusdt")

    assert metadata.tick_size == Decimal("0.01000000")
    assert metadata.min_price == Decimal("0.01000000")


def test_symbol_order_book_ticker_returns_json_with_uppercase_symbol():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(
            200,
            json={"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"},
        )

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.symbol_order_book_ticker("btcusdt")

    assert result == {"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"}
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/ticker/bookTicker")
    assert "symbol=BTCUSDT" in requests[0].url.query.decode()


def test_klines_returns_json_with_symbol_interval_and_limit_params():
    requests = []
    payload = [[1, "100", "101", "99", "100.5", "12"]]

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.klines("BTCUSDT", interval="1h", limit=6)

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/klines")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {
        "symbol": "BTCUSDT",
        "interval": "1h",
        "limit": "6",
    }


def test_klines_uses_default_interval_and_limit():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=[])

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.klines("btcusdt")

    assert result == []
    assert dict(parse_qsl(requests[0].url.query.decode())) == {
        "symbol": "BTCUSDT",
        "interval": "1h",
        "limit": "6",
    }


def test_depth_returns_json_with_symbol_and_limit_params():
    requests = []
    payload = {"lastUpdateId": 1, "bids": [["100", "2"]], "asks": [["101", "3"]]}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.depth("btcusdt", limit=20)

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/depth")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT", "limit": "20"}


def test_aggregate_trades_returns_json_with_symbol_and_limit_params():
    requests = []
    payload = [{"a": 1, "p": "100", "q": "2", "T": 1000, "m": False}]

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.aggregate_trades("btcusdt", limit=123)

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/aggTrades")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT", "limit": "123"}


def test_ticker_24hr_returns_json_with_symbol_param():
    requests = []
    payload = {"symbol": "BTCUSDT", "priceChangePercent": "1.23", "quoteVolume": "1000"}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.ticker_24hr("btcusdt")

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/ticker/24hr")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT"}


def test_avg_price_returns_json_with_symbol_param():
    requests = []
    payload = {"mins": 5, "price": "100.5"}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.avg_price("btcusdt")

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/avgPrice")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT"}


def test_open_orders_sends_signed_symbol_query_and_returns_list_json():
    requests = []
    payload = [{"symbol": "BTCUSDT", "orderId": 123}]

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.open_orders("btcusdt", timestamp_ms=1)

    query = requests[0].url.query.decode()
    query_pairs = parse_qsl(query, keep_blank_values=True)
    signature = dict(query_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in query_pairs if key != "signature"])
    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/openOrders")
    assert "symbol=BTCUSDT" in query
    assert "signature=" in query
    assert sign_query(sent_without_signature, "secret") == signature


def test_open_orders_rejects_non_list_payload():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"code": 0})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    with pytest.raises(ValueError, match="open orders response must be a list"):
        client.open_orders("BTCUSDT", timestamp_ms=1)


def test_my_trades_sends_signed_symbol_query_and_returns_list_json():
    requests = []
    payload = [{"id": 42, "orderId": 123, "symbol": "BTCUSDT"}]

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.my_trades("btcusdt", start_time_ms=1000, from_id=7, limit=100, timestamp_ms=1)

    query = requests[0].url.query.decode()
    query_pairs = parse_qsl(query, keep_blank_values=True)
    signature = dict(query_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in query_pairs if key != "signature"])
    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/myTrades")
    assert dict((key, value) for key, value in query_pairs if key != "signature") == {
        "symbol": "BTCUSDT",
        "startTime": "1000",
        "fromId": "7",
        "limit": "100",
        "recvWindow": "5000",
        "timestamp": "1",
    }
    assert sign_query(sent_without_signature, "secret") == signature


def test_my_trades_rejects_non_list_payload():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"code": 0})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    with pytest.raises(ValueError, match="my trades response must be a list"):
        client.my_trades("BTCUSDT", timestamp_ms=1)


def test_market_buy_order_sends_test_market_buy_with_quote_order_qty():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.test_market_buy_order("btcusdt", Decimal("25.5000"), "abc", timestamp_ms=1)

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result == {}
    assert requests[0].method == "POST"
    assert requests[0].url.path.endswith("/v3/order/test")
    assert "symbol=BTCUSDT" in body
    assert "side=BUY" in body
    assert "type=MARKET" in body
    assert "quoteOrderQty=25.5000" in body
    assert "newClientOrderId=abc" in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_create_market_sell_quantity_sends_market_sell_quantity_without_quote_order_qty():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"status": "FILLED"})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.create_market_sell_quantity("btcusdt", Decimal("0.0100"), "sell-1", timestamp_ms=1)

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result == {"status": "FILLED"}
    assert requests[0].method == "POST"
    assert requests[0].url.path.endswith("/v3/order")
    assert "symbol=BTCUSDT" in body
    assert "side=SELL" in body
    assert "type=MARKET" in body
    assert "quantity=0.0100" in body
    assert "newClientOrderId=sell-1" in body
    assert "quoteOrderQty" not in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_create_stop_loss_limit_sell_sends_stop_loss_limit_sell_params():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"orderId": 123})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.create_stop_loss_limit_sell(
        "btcusdt",
        Decimal("0.0100"),
        Decimal("95.5000"),
        Decimal("95.0000"),
        "stop-1",
        timestamp_ms=1,
    )

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result == {"orderId": 123}
    assert requests[0].method == "POST"
    assert requests[0].url.path.endswith("/v3/order")
    assert "symbol=BTCUSDT" in body
    assert "side=SELL" in body
    assert "type=STOP_LOSS_LIMIT" in body
    assert "timeInForce=GTC" in body
    assert "quantity=0.0100" in body
    assert "stopPrice=95.5000" in body
    assert "price=95.0000" in body
    assert "newOrderRespType=RESULT" in body
    assert "newClientOrderId=stop-1" in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_create_oco_sell_sends_order_list_oco_params():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"orderListId": 123, "listStatusType": "EXEC_STARTED"})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.create_oco_sell(
        "btcusdt",
        Decimal("0.0100"),
        Decimal("103.0000"),
        Decimal("98.0000"),
        Decimal("97.8000"),
        "oco-1",
        "tp-1",
        "stop-1",
        timestamp_ms=1,
    )

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result == {"orderListId": 123, "listStatusType": "EXEC_STARTED"}
    assert requests[0].method == "POST"
    assert requests[0].url.path.endswith("/v3/orderList/oco")
    assert "symbol=BTCUSDT" in body
    assert "side=SELL" in body
    assert "quantity=0.0100" in body
    assert "aboveType=LIMIT_MAKER" in body
    assert "abovePrice=103.0000" in body
    assert "aboveClientOrderId=tp-1" in body
    assert "belowType=STOP_LOSS_LIMIT" in body
    assert "belowStopPrice=98.0000" in body
    assert "belowPrice=97.8000" in body
    assert "belowTimeInForce=GTC" in body
    assert "belowClientOrderId=stop-1" in body
    assert "listClientOrderId=oco-1" in body
    assert "newOrderRespType=RESULT" in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_cancel_order_sends_signed_delete_with_orig_client_order_id():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"status": "CANCELED"})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.cancel_order("btcusdt", "stop-1", timestamp_ms=1)

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result == {"status": "CANCELED"}
    assert requests[0].method == "DELETE"
    assert requests[0].url.path.endswith("/v3/order")
    assert "symbol=BTCUSDT" in body
    assert "origClientOrderId=stop-1" in body
    assert sign_query(sent_without_signature, "secret") == signature
    assert requests[0].headers["X-MBX-APIKEY"] == "key"


def test_create_market_buy_order_sends_quote_order_qty():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"symbol": "BTCUSDT", "status": "FILLED", "clientOrderId": "abc"})

    transport = httpx.MockTransport(handler)
    client = BinanceSpotClient(api_key="key", api_secret="secret", http_client=httpx.Client(transport=transport))

    result = client.create_market_order("BTCUSDT", OrderSide.BUY, Decimal("25"), "abc", timestamp_ms=1)

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert result["status"] == "FILLED"
    assert "quoteOrderQty=25" in body
    assert "newClientOrderId=abc" in body
    assert sign_query(sent_without_signature, "secret") == signature
    assert requests[0].headers["X-MBX-APIKEY"] == "key"


def test_public_api_call_writes_audit_log_with_params_and_result(tmp_path):
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"})

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
        audit_logger=AuditLogger(tmp_path / "logs"),
    )

    client.symbol_order_book_ticker("btcusdt")

    [event] = read_audit_events(tmp_path / "logs" / "audit.log")
    assert event["event"] == "binance.api"
    assert event["params"] == {
        "method": "GET",
        "path": "/v3/ticker/bookTicker",
        "signed": False,
        "params": {"symbol": "BTCUSDT"},
    }
    assert event["result"] == {
        "status_code": 200,
        "payload": {"symbol": "BTCUSDT", "bidPrice": "100", "askPrice": "101"},
    }


def test_signed_api_call_writes_sanitized_audit_log(tmp_path):
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"balances": [{"asset": "USDT", "free": "42", "locked": "0"}]})

    client = BinanceSpotClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
        audit_logger=AuditLogger(tmp_path / "logs"),
    )

    client.account(timestamp_ms=1)

    [event] = read_audit_events(tmp_path / "logs" / "audit.log")
    assert event["event"] == "binance.api"
    assert event["params"]["method"] == "GET"
    assert event["params"]["path"] == "/v3/account"
    assert event["params"]["signed"] is True
    assert event["params"]["params"]["recvWindow"] == "5000"
    assert event["params"]["params"]["timestamp"] == "1"
    assert event["params"]["params"]["signature"] == "***REDACTED***"
    assert event["params"]["headers"]["X-MBX-APIKEY"] == "***REDACTED***"
    assert event["result"]["status_code"] == 200
    assert event["result"]["payload"]["balances"][0]["free"] == "42"
