"""Tests for in-process PortfolioClient (composes deribit + hyperliquid + macro)."""

from __future__ import annotations

from decimal import Decimal
from typing import Any

import pytest

from cerbero_bite.clients._exceptions import McpDataAnomalyError
from cerbero_bite.clients.portfolio import PortfolioClient

# ---------------------------------------------------------------------------
# Test doubles
# ---------------------------------------------------------------------------


class _FakeDeribit:
    SERVICE = "deribit"

    def __init__(
        self,
        *,
        equity_usd: Decimal | float = Decimal("0"),
        positions: list[dict[str, Any]] | None = None,
    ) -> None:
        self._equity = Decimal(str(equity_usd))
        self._positions = positions or []

    async def get_account_summary(self, currency: str = "USDC") -> dict[str, Any]:
        assert currency == "USDC"
        return {"equity": float(self._equity), "currency": "USDC"}

    async def get_positions(self, currency: str = "USDC") -> list[dict[str, Any]]:
        assert currency == "USDC"
        return list(self._positions)


class _FakeHyperliquid:
    SERVICE = "hyperliquid"

    def __init__(
        self,
        *,
        equity_usd: Decimal | float = Decimal("0"),
        positions: list[dict[str, Any]] | None = None,
    ) -> None:
        self._equity = Decimal(str(equity_usd))
        self._positions = positions or []

    async def get_account_summary(self) -> dict[str, Any]:
        return {"equity": float(self._equity)}

    async def get_positions(self) -> list[dict[str, Any]]:
        return list(self._positions)


class _FakeMacro:
    SERVICE = "macro"

    def __init__(self, *, eur_usd: Decimal | float | None = Decimal("1.10")) -> None:
        self._eur_usd = eur_usd

    async def eur_usd_rate(self) -> Decimal:
        if self._eur_usd is None:
            raise McpDataAnomalyError(
                "missing", service="macro", tool="get_asset_price"
            )
        return Decimal(str(self._eur_usd))


def _make(
    *,
    deribit_eq: Decimal | float = 0,
    hl_eq: Decimal | float = 0,
    deribit_pos: list[dict[str, Any]] | None = None,
    hl_pos: list[dict[str, Any]] | None = None,
    eur_usd: Decimal | float | None = Decimal("1.10"),
) -> PortfolioClient:
    return PortfolioClient(
        deribit=_FakeDeribit(equity_usd=deribit_eq, positions=deribit_pos),
        hyperliquid=_FakeHyperliquid(equity_usd=hl_eq, positions=hl_pos),
        macro=_FakeMacro(eur_usd=eur_usd),
    )


# ---------------------------------------------------------------------------
# total_equity_usd / total_equity_eur
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_total_equity_usd_sums_both_exchanges() -> None:
    p = _make(deribit_eq="1500.50", hl_eq="982.50")
    assert await p.total_equity_usd() == Decimal("2483.00")


@pytest.mark.asyncio
async def test_total_equity_eur_converts_with_fx() -> None:
    p = _make(deribit_eq="1100", hl_eq="0", eur_usd="1.10")
    # 1100 USD / 1.10 = 1000 EUR
    assert await p.total_equity_eur() == Decimal("1000")


@pytest.mark.asyncio
async def test_total_equity_eur_zero_when_no_balance() -> None:
    p = _make(deribit_eq=0, hl_eq=0, eur_usd="1.20")
    assert await p.total_equity_eur() == Decimal("0")


@pytest.mark.asyncio
async def test_total_equity_eur_raises_on_non_positive_fx() -> None:
    p = _make(deribit_eq="100", hl_eq="0", eur_usd="0")
    with pytest.raises(McpDataAnomalyError, match="non-positive EURUSD"):
        await p.total_equity_eur()


@pytest.mark.asyncio
async def test_total_equity_eur_propagates_macro_anomaly() -> None:
    p = _make(deribit_eq="100", hl_eq="0", eur_usd=None)
    with pytest.raises(McpDataAnomalyError):
        await p.total_equity_eur()


# ---------------------------------------------------------------------------
# asset_pct_of_portfolio
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_asset_pct_aggregates_eth_across_both_exchanges() -> None:
    p = _make(
        deribit_eq="5000",
        hl_eq="5000",
        deribit_pos=[
            {
                "instrument_name": "ETH-15MAY26-2475-P",
                "size": 10,
                "mark_price": 100,
            },
            # BTC position should be ignored when asking for ETH
            {
                "instrument_name": "BTC-PERPETUAL",
                "size": 1,
                "mark_price": 75000,
            },
        ],
        hl_pos=[
            {"coin": "ETH", "notional_usd": 1000},
        ],
    )
    # ETH exposure: 10×100 (deribit) + 1000 (hl) = 2000
    # total equity: 10000
    pct = await p.asset_pct_of_portfolio("ETH")
    assert pct == Decimal("0.2")


@pytest.mark.asyncio
async def test_asset_pct_returns_zero_when_no_positions() -> None:
    p = _make(deribit_eq="1000", hl_eq="0")
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0")


@pytest.mark.asyncio
async def test_asset_pct_returns_zero_when_no_equity() -> None:
    p = _make(
        deribit_eq=0,
        hl_eq=0,
        deribit_pos=[
            {"instrument_name": "ETH-PERP", "notional_usd": 100},
        ],
    )
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0")


@pytest.mark.asyncio
async def test_asset_pct_uses_explicit_notional_when_present() -> None:
    p = _make(
        deribit_eq="1000",
        hl_eq=0,
        deribit_pos=[
            # explicit notional_usd takes precedence over size×mark
            {
                "instrument_name": "ETH-XYZ",
                "notional_usd": 250,
                "size": 999,
                "mark_price": 999,
            },
        ],
    )
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0.25")


@pytest.mark.asyncio
async def test_asset_pct_falls_back_to_size_times_mark() -> None:
    p = _make(
        deribit_eq="1000",
        hl_eq=0,
        deribit_pos=[
            {"instrument_name": "ETH-XYZ", "size": 5, "mark_price": 40},
        ],
    )
    # 5×40 / 1000 = 0.2
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0.2")


@pytest.mark.asyncio
async def test_asset_pct_takes_absolute_value_for_short_positions() -> None:
    p = _make(
        deribit_eq="1000",
        hl_eq=0,
        hl_pos=[{"coin": "ETH", "size": -10, "mark_price": 50}],
    )
    # |-10×50| / 1000 = 0.5
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0.5")


@pytest.mark.asyncio
async def test_asset_pct_case_insensitive_match() -> None:
    p = _make(
        deribit_eq="1000",
        hl_eq=0,
        deribit_pos=[
            {"instrument_name": "eth-perpetual", "notional_usd": 300},
        ],
    )
    assert await p.asset_pct_of_portfolio("eth") == Decimal("0.3")


@pytest.mark.asyncio
async def test_asset_pct_skips_non_dict_entries() -> None:
    p = _make(
        deribit_eq="1000",
        hl_eq=0,
        deribit_pos=[
            "not a dict",  # type: ignore[list-item]
            {"instrument_name": "ETH", "notional_usd": 100},
        ],
    )
    assert await p.asset_pct_of_portfolio("ETH") == Decimal("0.1")
