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 sign_query
from binance_quant.futures_exchange import BinanceUsdmFuturesClient
from binance_quant.futures_models import FuturesOrderSide, FuturesSymbolMetadata


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_futures_client_uses_testnet_base_url_by_default():
    client = BinanceUsdmFuturesClient(api_key="key", api_secret="secret")

    assert client.base_url == "https://testnet.binancefuture.com"


def test_futures_exchange_info_parses_usdt_perpetual_metadata():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "BTCUSDT",
                        "contractType": "PERPETUAL",
                        "status": "TRADING",
                        "baseAsset": "BTC",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.001", "stepSize": "0.001"},
                            {"filterType": "PRICE_FILTER", "tickSize": "0.10"},
                            {"filterType": "MIN_NOTIONAL", "notional": "100"},
                        ],
                    }
                ]
            },
        )

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

    metadata = client.exchange_info("btcusdt")

    assert isinstance(metadata, FuturesSymbolMetadata)
    assert metadata.symbol == "BTCUSDT"
    assert metadata.base_asset == "BTC"
    assert metadata.quote_asset == "USDT"
    assert metadata.min_qty == Decimal("0.001")
    assert metadata.step_size == Decimal("0.001")
    assert metadata.tick_size == Decimal("0.10")
    assert metadata.min_notional == Decimal("100")


def test_futures_exchange_info_selects_requested_symbol_when_response_has_multiple_symbols():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "BTCUSDT",
                        "contractType": "PERPETUAL",
                        "status": "TRADING",
                        "baseAsset": "BTC",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.001", "stepSize": "0.001"},
                            {"filterType": "PRICE_FILTER", "tickSize": "0.10"},
                            {"filterType": "MIN_NOTIONAL", "notional": "100"},
                        ],
                    },
                    {
                        "symbol": "BNBUSDT",
                        "contractType": "PERPETUAL",
                        "status": "TRADING",
                        "baseAsset": "BNB",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.01", "stepSize": "0.01"},
                            {"filterType": "PRICE_FILTER", "tickSize": "0.001"},
                            {"filterType": "MIN_NOTIONAL", "notional": "5"},
                        ],
                    },
                ]
            },
        )

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

    metadata = client.exchange_info("bnbusdt")

    assert metadata.symbol == "BNBUSDT"
    assert metadata.base_asset == "BNB"
    assert metadata.min_qty == Decimal("0.01")
    assert metadata.step_size == Decimal("0.01")
    assert metadata.tick_size == Decimal("0.001")
    assert metadata.min_notional == Decimal("5")


def test_futures_create_market_order_sends_signed_params():
    requests = []

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

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

    payload = client.create_market_order(
        "btcusdt",
        side=FuturesOrderSide.BUY,
        quantity=Decimal("0.002"),
        client_order_id="bqf-open",
        reduce_only=False,
        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 payload["status"] == "FILLED"
    assert requests[0].method == "POST"
    assert requests[0].url.path == "/fapi/v1/order"
    assert "symbol=BTCUSDT" in body
    assert "side=BUY" in body
    assert "type=MARKET" in body
    assert "quantity=0.002" in body
    assert "newClientOrderId=bqf-open" in body
    assert "reduceOnly=true" not in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_futures_query_order_uses_symbol_and_client_order_id():
    requests = []

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

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

    payload = client.query_order("btcusdt", "bqf-open", timestamp_ms=1)

    query = requests[0].url.query.decode()
    assert payload["status"] == "FILLED"
    assert requests[0].method == "GET"
    assert requests[0].url.path == "/fapi/v1/order"
    assert "symbol=BTCUSDT" in query
    assert "origClientOrderId=bqf-open" in query


def test_futures_signed_get_retries_next_base_url_after_transport_error():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.path == "/fapi/v1/time":
            return httpx.Response(200, json={"serverTime": 1_000})
        if request.url.host == "fapi.binance.com":
            raise httpx.ConnectTimeout("handshake timeout")
        return httpx.Response(200, json={"availableBalance": "10"})

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://fapi.binance.com", "https://fapi1.binance.com"),
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    payload = client.account()

    account_requests = [request for request in requests if request.url.path == "/fapi/v3/account"]
    assert [request.url.host for request in account_requests] == ["fapi.binance.com", "fapi1.binance.com"]
    assert client.base_url == "https://fapi1.binance.com"
    assert payload == {"availableBalance": "10"}


def test_futures_public_get_retries_next_base_url_after_transport_error():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.host == "fapi.binance.com":
            raise httpx.ConnectError("ssl eof")
        return httpx.Response(200, json={"symbol": "BTCUSDT", "markPrice": "60000"})

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://fapi.binance.com", "https://fapi1.binance.com"),
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    payload = client.mark_price("btcusdt")

    mark_price_requests = [request for request in requests if request.url.path == "/fapi/v1/premiumIndex"]
    assert [request.url.host for request in mark_price_requests] == ["fapi.binance.com", "fapi1.binance.com"]
    assert client.base_url == "https://fapi1.binance.com"
    assert payload == {"symbol": "BTCUSDT", "markPrice": "60000"}


def test_futures_public_get_retries_next_base_url_after_redirect_status():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.host == "fapi.binance.com":
            return httpx.Response(
                302,
                headers={"location": "https://www.binance.com/en"},
                request=request,
            )
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "ETHUSDT",
                        "baseAsset": "ETH",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.001", "stepSize": "0.001"},
                            {"filterType": "PRICE_FILTER", "tickSize": "0.01"},
                            {"filterType": "MIN_NOTIONAL", "notional": "20"},
                        ],
                    }
                ]
            },
        )

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://fapi.binance.com", "https://fapi1.binance.com"),
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    metadata = client.exchange_info("ethusdt")

    exchange_info_requests = [request for request in requests if request.url.path == "/fapi/v1/exchangeInfo"]
    assert [request.url.host for request in exchange_info_requests] == ["fapi.binance.com", "fapi1.binance.com"]
    assert client.base_url == "https://fapi1.binance.com"
    assert metadata.symbol == "ETHUSDT"


def test_futures_public_get_raises_last_transport_error_after_all_base_urls_fail():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        raise httpx.ConnectTimeout(f"timeout {request.url.host}")

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        base_urls=("https://fapi.binance.com", "https://fapi1.binance.com"),
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    with pytest.raises(httpx.ConnectTimeout, match="timeout fapi1.binance.com"):
        client.mark_price("btcusdt")

    mark_price_requests = [request for request in requests if request.url.path == "/fapi/v1/premiumIndex"]
    assert [request.url.host for request in mark_price_requests] == ["fapi.binance.com", "fapi1.binance.com"]


def test_futures_context_data_endpoints_use_expected_paths_and_params():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.path == "/fapi/v1/time":
            return httpx.Response(200, json={"serverTime": 1})
        if request.url.path == "/fapi/v3/account":
            return httpx.Response(200, json={"availableBalance": "10"})
        if request.url.path == "/fapi/v1/leverageBracket":
            return httpx.Response(200, json=[{"symbol": "BTCUSDT", "brackets": []}])
        if request.url.path == "/fapi/v1/fundingRate":
            return httpx.Response(200, json=[{"fundingRate": "0.0001"}])
        if request.url.path == "/futures/data/openInterestHist":
            return httpx.Response(200, json=[{"sumOpenInterest": "100"}])
        raise AssertionError(f"unexpected path {request.url.path}")

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

    assert client.account() == {"availableBalance": "10"}
    assert client.leverage_bracket("btcusdt") == [{"symbol": "BTCUSDT", "brackets": []}]
    assert client.funding_rate_history("btcusdt", limit=8) == [{"fundingRate": "0.0001"}]
    assert client.open_interest_hist("btcusdt", period="15m", limit=12) == [{"sumOpenInterest": "100"}]

    paths = [request.url.path for request in requests]
    assert paths == [
        "/fapi/v1/time",
        "/fapi/v3/account",
        "/fapi/v1/leverageBracket",
        "/fapi/v1/fundingRate",
        "/futures/data/openInterestHist",
    ]
    queries = [dict(parse_qsl(request.url.query.decode(), keep_blank_values=True)) for request in requests]
    assert queries[2]["symbol"] == "BTCUSDT"
    assert queries[3] == {"symbol": "BTCUSDT", "limit": "8"}
    assert queries[4] == {"symbol": "BTCUSDT", "period": "15m", "limit": "12"}


def test_futures_create_algo_order_sends_conditional_algo_params():
    requests = []

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

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

    payload = client.create_conditional_algo_order(
        "btcusdt",
        side=FuturesOrderSide.SELL,
        order_type="STOP_MARKET",
        trigger_price=Decimal("59000"),
        quantity=Decimal("0.002"),
        client_algo_id="bqf-stop",
        timestamp_ms=1,
    )

    body = requests[0].content.decode()
    assert payload["algoId"] == 123
    assert requests[0].method == "POST"
    assert requests[0].url.path == "/fapi/v1/algoOrder"
    assert "algoType=CONDITIONAL" in body
    assert "type=STOP_MARKET" in body
    assert "triggerPrice=59000" in body
    assert "reduceOnly=true" in body
    assert "workingType=MARK_PRICE" in body
    assert "clientAlgoId=bqf-stop" in body


def test_futures_cancel_algo_order_uses_client_algo_id():
    requests = []

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

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

    result = client.cancel_algo_order("bqf-stop", timestamp_ms=1)

    assert result["status"] == "CANCELED"
    assert requests[0].method == "DELETE"
    assert requests[0].url.path == "/fapi/v1/algoOrder"
    assert "clientAlgoId=bqf-stop" in requests[0].content.decode()


def test_futures_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", "markPrice": "60000"})

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

    client.mark_price("btcusdt")

    [event] = read_audit_events(tmp_path / "logs" / "audit.log")
    assert event["event"] == "binance.api"
    assert event["params"] == {
        "method": "GET",
        "path": "/fapi/v1/premiumIndex",
        "signed": False,
        "params": {"symbol": "BTCUSDT"},
    }
    assert event["result"] == {
        "status_code": 200,
        "payload": {"symbol": "BTCUSDT", "markPrice": "60000"},
    }


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

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

    client.open_orders("btcusdt", 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"] == "/fapi/v1/openOrders"
    assert event["params"]["signed"] is True
    assert event["params"]["params"]["symbol"] == "BTCUSDT"
    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, "payload": []}


def test_futures_signed_api_uses_server_time_offset_when_local_clock_is_ahead():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        if request.url.path == "/fapi/v1/time":
            return httpx.Response(200, json={"serverTime": 1_000})
        return httpx.Response(
            200,
            json=[
                {
                    "symbol": "BTCUSDT",
                    "positionAmt": "0",
                    "entryPrice": "0",
                    "markPrice": "60000",
                    "liquidationPrice": "0",
                    "isolatedMargin": "0",
                    "unRealizedProfit": "0",
                    "leverage": "2",
                    "marginType": "isolated",
                }
            ],
        )

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
        clock=lambda: 2.5,
    )

    client.position_risk("btcusdt")

    signed_request = requests[-1]
    signed_query = dict(parse_qsl(signed_request.url.query.decode(), keep_blank_values=True))
    assert [request.url.path for request in requests] == ["/fapi/v1/time", "/fapi/v2/positionRisk"]
    assert signed_query["timestamp"] == "1000"


def test_futures_signed_api_does_not_cache_slow_time_probe_latency_as_clock_offset():
    requests = []
    current_time = 1.0

    def clock() -> float:
        return current_time

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal current_time
        requests.append(request)
        if request.url.path == "/fapi/v1/time":
            current_time = 2.5
            return httpx.Response(200, json={"serverTime": 2_500})
        return httpx.Response(
            200,
            json=[
                {
                    "symbol": "BTCUSDT",
                    "positionAmt": "0",
                    "entryPrice": "0",
                    "markPrice": "60000",
                    "liquidationPrice": "0",
                    "isolatedMargin": "0",
                    "unRealizedProfit": "0",
                    "leverage": "2",
                    "marginType": "isolated",
                }
            ],
        )

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

    client.position_risk("btcusdt")
    current_time = 3.0
    client.position_risk("btcusdt")

    signed_requests = [request for request in requests if request.url.path == "/fapi/v2/positionRisk"]
    signed_timestamps = [
        dict(parse_qsl(request.url.query.decode(), keep_blank_values=True))["timestamp"]
        for request in signed_requests
    ]
    assert signed_timestamps == ["2500", "3000"]


def test_futures_signed_get_error_audit_does_not_log_signature_or_api_key(tmp_path):
    captured_signature = None

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal captured_signature
        query = dict(parse_qsl(request.url.query.decode(), keep_blank_values=True))
        captured_signature = query["signature"]
        return httpx.Response(400, json={"code": -1022}, request=request)

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

    with pytest.raises(httpx.HTTPStatusError):
        client.open_orders("btcusdt", timestamp_ms=1)

    log_text = (tmp_path / "logs" / "audit.log").read_text(encoding="utf-8")
    [event] = read_audit_events(tmp_path / "logs" / "audit.log")
    assert captured_signature is not None
    assert captured_signature not in log_text
    assert "raw-key" not in log_text
    assert event["params"]["params"]["signature"] == "***REDACTED***"
    assert event["params"]["headers"]["X-MBX-APIKEY"] == "***REDACTED***"
    assert event["error"]["type"] == "HTTPStatusError"
