"""Tests for DeribitClient."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from pytest_httpx import HTTPXMock

from cerbero_bite.clients._base import HttpToolClient
from cerbero_bite.clients._exceptions import McpDataAnomalyError
from cerbero_bite.clients.deribit import (
    ComboLegOrder,
    DeribitClient,
)


def _client() -> DeribitClient:
    http = HttpToolClient(
        service="deribit",
        base_url="http://mcp-deribit:9011",
        token="t",
        retry_max=1,
    )
    return DeribitClient(http)


def _request_body(httpx_mock: HTTPXMock, *, index: int = -1) -> dict:
    requests = httpx_mock.get_requests()
    assert requests, "expected at least one request"
    return json.loads(requests[index].read())


# ---------------------------------------------------------------------------
# environment_info
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_environment_info_parses_payload(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/environment_info",
        json={
            "exchange": "deribit",
            "environment": "testnet",
            "source": "env",
            "env_value": "true",
            "base_url": "https://test.deribit.com/api/v2",
            "max_leverage": 3,
        },
    )
    info = await _client().environment_info()
    assert info.environment == "testnet"
    assert info.source == "env"
    assert info.max_leverage == 3


# ---------------------------------------------------------------------------
# index_price_eth
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_index_price_eth_uses_perpetual_mark(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_ticker",
        json={"instrument_name": "ETH-PERPETUAL", "mark_price": 3024.5, "bid": 3024.3},
    )
    out = await _client().index_price_eth()
    assert out == Decimal("3024.5")
    body = _request_body(httpx_mock)
    assert body == {"instrument_name": "ETH-PERPETUAL"}


@pytest.mark.asyncio
async def test_index_price_eth_anomaly_when_mark_missing(
    httpx_mock: HTTPXMock,
) -> None:
    httpx_mock.add_response(json={"instrument_name": "ETH-PERPETUAL"})
    with pytest.raises(McpDataAnomalyError, match="mark_price"):
        await _client().index_price_eth()


# ---------------------------------------------------------------------------
# latest_dvol
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_latest_dvol_returns_latest_field(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_dvol",
        json={"currency": "ETH", "latest": 52.4, "candles": []},
    )
    out = await _client().latest_dvol(now=datetime(2026, 4, 27, 14, 0, tzinfo=UTC))
    assert out == Decimal("52.4")


@pytest.mark.asyncio
async def test_latest_dvol_falls_back_to_candle_close(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        json={
            "currency": "ETH",
            "candles": [
                {"close": 50.0},
                {"close": 51.7},
            ],
        }
    )
    out = await _client().latest_dvol(now=datetime(2026, 4, 27, 14, 0, tzinfo=UTC))
    assert out == Decimal("51.7")


@pytest.mark.asyncio
async def test_latest_dvol_anomaly_when_empty(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(json={"currency": "ETH", "candles": []})
    with pytest.raises(McpDataAnomalyError):
        await _client().latest_dvol(now=datetime(2026, 4, 27, 14, 0, tzinfo=UTC))


# ---------------------------------------------------------------------------
# options_chain
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_options_chain_parses_instrument_names(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_instruments",
        json={
            "instruments": [
                {
                    "name": "ETH-15MAY26-2475-P",
                    "strike": 2475,
                    "open_interest": 200,
                    "tick_size": 0.0005,
                    "min_trade_amount": 1,
                },
                {
                    "name": "ETH-15MAY26-2350-P",
                    "strike": 2350,
                    "open_interest": 150,
                },
                {"name": "MALFORMED-INSTRUMENT"},
            ]
        },
    )
    chain = await _client().options_chain(currency="ETH")
    names = [m.name for m in chain]
    assert names == ["ETH-15MAY26-2475-P", "ETH-15MAY26-2350-P"]
    assert chain[0].strike == Decimal("2475")
    assert chain[0].expiry == datetime(2026, 5, 15, 8, 0, tzinfo=UTC)
    assert chain[0].option_type == "P"
    assert chain[0].open_interest == Decimal("200")


# ---------------------------------------------------------------------------
# get_tickers / orderbook
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_tickers_returns_list(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_ticker_batch",
        json={"tickers": [{"instrument_name": "X"}], "errors": []},
    )
    tickers = await _client().get_tickers(["X"])
    assert tickers == [{"instrument_name": "X"}]


@pytest.mark.asyncio
async def test_get_tickers_empty_short_circuits(httpx_mock: HTTPXMock) -> None:
    out = await _client().get_tickers([])
    assert out == []
    assert httpx_mock.get_requests() == []


@pytest.mark.asyncio
async def test_get_tickers_rejects_more_than_twenty() -> None:
    with pytest.raises(ValueError, match="max 20"):
        await _client().get_tickers(["x"] * 21)


@pytest.mark.asyncio
async def test_get_tickers_anomaly_on_error_envelope(
    httpx_mock: HTTPXMock,
) -> None:
    httpx_mock.add_response(json={"error": "max 20 instruments per batch"})
    with pytest.raises(McpDataAnomalyError):
        await _client().get_tickers(["x"])


@pytest.mark.asyncio
async def test_orderbook_depth_top3_sums_levels(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_orderbook",
        json={
            "bids": [[3000, 5], [2999, 7], [2998, 3], [2997, 100]],
            "asks": [[3001, 4], [3002, 6], [3003, 2]],
        },
    )
    depth = await _client().orderbook_depth_top3("ETH-15MAY26-2475-P")
    # bids 5+7+3=15; asks 4+6+2=12 → 27
    assert depth == 27


# ---------------------------------------------------------------------------
# place_combo_order
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_place_combo_order_submits_correct_body(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/place_combo_order",
        json={
            "combo_instrument": "ETH-15MAY26-2475P_2350P",
            "order_id": "ord-1",
            "state": "open",
            "average_price": None,
            "filled_amount": 0,
        },
    )
    legs = [
        ComboLegOrder(instrument_name="ETH-15MAY26-2475-P", direction="sell"),
        ComboLegOrder(instrument_name="ETH-15MAY26-2350-P", direction="buy"),
    ]
    res = await _client().place_combo_order(
        legs=legs,
        side="sell",
        n_contracts=2,
        limit_price_eth=Decimal("0.005"),
        label="weekly-open",
    )
    body = _request_body(httpx_mock)
    assert body["side"] == "sell"
    assert body["amount"] == 2
    assert body["price"] == 0.005
    assert body["label"] == "weekly-open"
    assert body["legs"] == [
        {"instrument_name": "ETH-15MAY26-2475-P", "direction": "sell", "ratio": 1},
        {"instrument_name": "ETH-15MAY26-2350-P", "direction": "buy", "ratio": 1},
    ]
    assert res.combo_instrument == "ETH-15MAY26-2475P_2350P"
    assert res.order_id == "ord-1"
    assert res.state == "open"


@pytest.mark.asyncio
async def test_place_combo_rejects_single_leg() -> None:
    with pytest.raises(ValueError, match="at least 2 legs"):
        await _client().place_combo_order(
            legs=[ComboLegOrder(instrument_name="X", direction="sell")],
            side="sell",
            n_contracts=1,
            limit_price_eth=Decimal("0.001"),
        )


@pytest.mark.asyncio
async def test_place_combo_rejects_zero_contracts() -> None:
    legs = [
        ComboLegOrder(instrument_name="A", direction="sell"),
        ComboLegOrder(instrument_name="B", direction="buy"),
    ]
    with pytest.raises(ValueError, match="n_contracts"):
        await _client().place_combo_order(
            legs=legs, side="sell", n_contracts=0, limit_price_eth=Decimal("0.001")
        )


@pytest.mark.asyncio
async def test_place_combo_rejects_limit_without_price() -> None:
    legs = [
        ComboLegOrder(instrument_name="A", direction="sell"),
        ComboLegOrder(instrument_name="B", direction="buy"),
    ]
    with pytest.raises(ValueError, match="limit price"):
        await _client().place_combo_order(
            legs=legs, side="sell", n_contracts=1, limit_price_eth=None
        )


@pytest.mark.asyncio
async def test_place_combo_anomaly_when_combo_instrument_missing(
    httpx_mock: HTTPXMock,
) -> None:
    httpx_mock.add_response(json={"order_id": "x"})
    legs = [
        ComboLegOrder(instrument_name="A", direction="sell"),
        ComboLegOrder(instrument_name="B", direction="buy"),
    ]
    with pytest.raises(McpDataAnomalyError, match="combo_instrument"):
        await _client().place_combo_order(
            legs=legs, side="sell", n_contracts=1, limit_price_eth=Decimal("0.001")
        )


# ---------------------------------------------------------------------------
# cancel + accounts
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cancel_order_passes_id(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/cancel_order",
        json={"order_id": "ord-1", "state": "cancelled"},
    )
    out = await _client().cancel_order("ord-1")
    assert out["state"] == "cancelled"


@pytest.mark.asyncio
async def test_get_account_summary_passes_currency(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(json={"equity": 1000, "balance": 1000})
    out = await _client().get_account_summary("USDC")
    assert out["equity"] == 1000


@pytest.mark.asyncio
async def test_get_positions_returns_list(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(json=[{"instrument": "ETH-PERPETUAL", "size": 1}])
    out = await _client().get_positions()
    assert out == [{"instrument": "ETH-PERPETUAL", "size": 1}]


# ---------------------------------------------------------------------------
# misc
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_dealer_gamma_profile_eth_parses_payload(httpx_mock: HTTPXMock) -> None:
    httpx_mock.add_response(
        url="http://mcp-deribit:9011/tools/get_dealer_gamma_profile",
        json={
            "currency": "ETH",
            "spot_price": 3000.0,
            "by_strike": [],
            "total_net_dealer_gamma": 12345.6,
            "gamma_flip_level": 2950.5,
            "strikes_analyzed": 18,
        },
    )
    snap = await _client().dealer_gamma_profile_eth()
    assert snap.spot_price == Decimal("3000.0")
    assert snap.total_net_dealer_gamma == Decimal("12345.6")
    assert snap.gamma_flip_level == Decimal("2950.5")
    assert snap.strikes_analyzed == 18


@pytest.mark.asyncio
async def test_dealer_gamma_profile_anomaly_when_total_missing(
    httpx_mock: HTTPXMock,
) -> None:
    httpx_mock.add_response(json={"spot_price": 3000.0})
    with pytest.raises(McpDataAnomalyError, match="missing spot_price or total"):
        await _client().dealer_gamma_profile_eth()


def test_deribit_client_rejects_wrong_service() -> None:
    bad = HttpToolClient(
        service="macro", base_url="http://x:1", token="t", retry_max=1
    )
    with pytest.raises(ValueError, match="requires service 'deribit'"):
        DeribitClient(bad)
