"""TDD per :mod:`cerbero_bite.runtime.option_chain_snapshot_cycle`."""

from __future__ import annotations

from datetime import UTC, datetime
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock

import pytest

from cerbero_bite.clients.deribit import InstrumentMeta
from cerbero_bite.runtime.option_chain_snapshot_cycle import (
    collect_option_chain_snapshot,
)
from cerbero_bite.state.models import OptionChainQuoteRecord


_NOW = datetime(2026, 5, 4, 13, 55, tzinfo=UTC)


def _meta(name: str, strike: int, expiry_dte: int = 18) -> InstrumentMeta:
    expiry = _NOW.replace(hour=8, minute=0, second=0)
    expiry = expiry.replace(day=expiry.day) + (
        # add days
        __import__("datetime").timedelta(days=expiry_dte)
    )
    return InstrumentMeta(
        name=name,
        strike=Decimal(str(strike)),
        expiry=expiry,
        option_type="P",
        open_interest=Decimal("100"),
        tick_size=Decimal("0.0005"),
        min_trade_amount=Decimal("1"),
    )


def _ticker(name: str, *, mark: float = 0.020, bid: float = 0.018,
            ask: float = 0.022, delta: float = -0.12) -> dict:
    return {
        "instrument_name": name,
        "bid": bid,
        "ask": ask,
        "mark_price": mark,
        "mark_iv": 60.0,
        "volume_24h": 50,
        "greeks": {
            "delta": delta,
            "gamma": 0.001,
            "theta": -0.0005,
            "vega": 0.10,
        },
    }


@pytest.fixture
def cfg() -> object:
    from cerbero_bite.config import golden_config
    return golden_config()


@pytest.fixture
def fake_ctx(cfg: object) -> MagicMock:
    """Mock minimal RuntimeContext."""
    ctx = MagicMock()
    ctx.cfg = cfg
    ctx.db_path = ":memory:"
    return ctx


@pytest.mark.asyncio
async def test_collector_persists_one_quote_per_instrument(
    fake_ctx: MagicMock,
) -> None:
    metas = [_meta("ETH-21MAY26-2475-P", 2475), _meta("ETH-21MAY26-2400-P", 2400)]
    fake_ctx.deribit.options_chain = AsyncMock(return_value=metas)
    fake_ctx.deribit.get_tickers = AsyncMock(
        return_value=[_ticker(m.name) for m in metas]
    )
    persisted: list[list[OptionChainQuoteRecord]] = []

    def _record(_conn: object, qs: list[OptionChainQuoteRecord]) -> int:
        persisted.append(qs)
        return len(qs)

    fake_ctx.repository.record_option_chain_snapshot = _record

    n = await collect_option_chain_snapshot(fake_ctx, asset="ETH", now=_NOW)
    assert n == 2
    assert len(persisted) == 1
    assert {q.instrument_name for q in persisted[0]} == {
        "ETH-21MAY26-2475-P", "ETH-21MAY26-2400-P",
    }
    # Tutti i quote condividono il timestamp del cron tick.
    assert all(q.timestamp == _NOW for q in persisted[0])


@pytest.mark.asyncio
async def test_collector_handles_missing_tickers_with_null_fields(
    fake_ctx: MagicMock,
) -> None:
    metas = [_meta("ETH-21MAY26-2475-P", 2475)]
    fake_ctx.deribit.options_chain = AsyncMock(return_value=metas)
    fake_ctx.deribit.get_tickers = AsyncMock(return_value=[])  # vuoto
    persisted: list[list[OptionChainQuoteRecord]] = []

    def _record(_conn: object, qs: list[OptionChainQuoteRecord]) -> int:
        persisted.append(qs)
        return len(qs)

    fake_ctx.repository.record_option_chain_snapshot = _record

    n = await collect_option_chain_snapshot(fake_ctx, now=_NOW)
    assert n == 1
    assert persisted[0][0].mid is None  # ticker mancante ⇒ campi NULL
    assert persisted[0][0].instrument_name == "ETH-21MAY26-2475-P"


@pytest.mark.asyncio
async def test_collector_returns_zero_when_chain_empty(
    fake_ctx: MagicMock,
) -> None:
    fake_ctx.deribit.options_chain = AsyncMock(return_value=[])
    n = await collect_option_chain_snapshot(fake_ctx, now=_NOW)
    assert n == 0


@pytest.mark.asyncio
async def test_collector_swallows_chain_fetch_failure(
    fake_ctx: MagicMock,
) -> None:
    fake_ctx.deribit.options_chain = AsyncMock(side_effect=RuntimeError("boom"))
    n = await collect_option_chain_snapshot(fake_ctx, now=_NOW)
    assert n == 0  # best-effort: non rilancia
