"""TDD per i nuovi helper repository del gate IV-RV adattivo.

Spec: distinct-days policy — il caller (entry_cycle) interroga il
numero di giorni coperti separatamente dai valori della finestra,
così che cadenze miste (tick live 15min + backfill daily) restino
statisticamente coerenti.

Helpers:
    * ``count_iv_rv_distinct_days(asset, max_days, as_of) -> int``
    * ``iv_rv_values_for_window(asset, window_days, as_of) -> list[Decimal]``
    * ``dvol_lookback`` (invariato, riusato dal Vol-of-Vol guard)
"""

from __future__ import annotations

import sqlite3
from datetime import UTC, datetime, timedelta
from decimal import Decimal

import pytest

from cerbero_bite.state.db import connect, run_migrations
from cerbero_bite.state.models import MarketSnapshotRecord
from cerbero_bite.state.repository import Repository


def _snap(
    *,
    ts: datetime,
    asset: str = "ETH",
    iv_minus_rv: Decimal | None = Decimal("2"),
    fetch_ok: bool = True,
    dvol: Decimal = Decimal("50"),
) -> MarketSnapshotRecord:
    return MarketSnapshotRecord(
        timestamp=ts,
        asset=asset,
        spot=Decimal("2000"),
        dvol=dvol,
        realized_vol_30d=Decimal("48"),
        iv_minus_rv=iv_minus_rv,
        funding_perp_annualized=Decimal("0"),
        funding_cross_annualized=Decimal("0"),
        dealer_net_gamma=Decimal("0"),
        gamma_flip_level=None,
        oi_delta_pct_4h=None,
        liquidation_long_risk="low",
        liquidation_short_risk="low",
        macro_days_to_event=None,
        fetch_ok=fetch_ok,
        fetch_errors_json=None,
    )


@pytest.fixture
def db_one_day(tmp_path) -> sqlite3.Connection:
    """SQLite temp con 96 tick ETH a 15min (1 giorno) e fetch_ok=1."""
    conn = connect(str(tmp_path / "test.sqlite"))
    run_migrations(conn)
    repo = Repository()
    base = datetime(2026, 5, 1, 0, 0, tzinfo=UTC)
    for i in range(96):
        repo.record_market_snapshot(
            conn,
            _snap(
                ts=base + timedelta(minutes=15 * i),
                iv_minus_rv=Decimal("2") + Decimal(i) / Decimal("100"),
                dvol=Decimal("50") + Decimal(i) / Decimal("10"),
            ),
        )
    conn.commit()
    return conn


@pytest.fixture
def db_three_days_mixed(tmp_path) -> sqlite3.Connection:
    """SQLite temp con 3 giorni ETH:
    - day1 (2026-05-01): 96 tick @ 15min, valori 1..96
    - day2 (2026-05-02): 1 record daily a 12:00, valore 100 (backfill style)
    - day3 (2026-05-03): 4 tick orari, valori 200, 201, 202, 203
    Più 1 giorno BTC isolato (per cross-asset isolation).
    """
    conn = connect(str(tmp_path / "test.sqlite"))
    run_migrations(conn)
    repo = Repository()

    day1 = datetime(2026, 5, 1, 0, 0, tzinfo=UTC)
    for i in range(96):
        repo.record_market_snapshot(
            conn,
            _snap(
                ts=day1 + timedelta(minutes=15 * i),
                iv_minus_rv=Decimal(i + 1),
            ),
        )
    repo.record_market_snapshot(
        conn,
        _snap(ts=datetime(2026, 5, 2, 12, 0, tzinfo=UTC), iv_minus_rv=Decimal("100")),
    )
    day3 = datetime(2026, 5, 3, 0, 0, tzinfo=UTC)
    for i in range(4):
        repo.record_market_snapshot(
            conn,
            _snap(
                ts=day3 + timedelta(hours=i),
                iv_minus_rv=Decimal(200 + i),
            ),
        )
    repo.record_market_snapshot(
        conn,
        _snap(
            ts=datetime(2026, 4, 30, 0, 0, tzinfo=UTC),
            asset="BTC",
            iv_minus_rv=Decimal("999"),
        ),
    )
    conn.commit()
    return conn


# ---------------------------------------------------------------------------
# count_iv_rv_distinct_days
# ---------------------------------------------------------------------------


def test_count_distinct_days_returns_one_for_single_day_history(db_one_day) -> None:
    repo = Repository()
    n = repo.count_iv_rv_distinct_days(
        db_one_day,
        asset="ETH",
        max_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert n == 1


def test_count_distinct_days_returns_zero_for_other_asset(db_one_day) -> None:
    repo = Repository()
    n = repo.count_iv_rv_distinct_days(
        db_one_day,
        asset="BTC",
        max_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert n == 0


def test_count_distinct_days_counts_unique_calendar_days(
    db_three_days_mixed,
) -> None:
    repo = Repository()
    n = repo.count_iv_rv_distinct_days(
        db_three_days_mixed,
        asset="ETH",
        max_days=60,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert n == 3


def test_count_distinct_days_excludes_other_assets(
    db_three_days_mixed,
) -> None:
    repo = Repository()
    n_btc = repo.count_iv_rv_distinct_days(
        db_three_days_mixed,
        asset="BTC",
        max_days=60,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert n_btc == 1


def test_count_distinct_days_respects_window_cutoff(
    db_three_days_mixed,
) -> None:
    """max_days=1 da as_of=2026-05-04 → cutoff=2026-05-03 → solo day3."""
    repo = Repository()
    n = repo.count_iv_rv_distinct_days(
        db_three_days_mixed,
        asset="ETH",
        max_days=1,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert n == 1


def test_count_distinct_days_excludes_null_iv_rv(tmp_path) -> None:
    conn = connect(str(tmp_path / "test.sqlite"))
    run_migrations(conn)
    repo = Repository()
    repo.record_market_snapshot(
        conn,
        _snap(ts=datetime(2026, 5, 1, 12, 0, tzinfo=UTC), iv_minus_rv=None),
    )
    conn.commit()
    n = repo.count_iv_rv_distinct_days(
        conn,
        asset="ETH",
        max_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert n == 0


def test_count_distinct_days_excludes_fetch_failed(tmp_path) -> None:
    conn = connect(str(tmp_path / "test.sqlite"))
    run_migrations(conn)
    repo = Repository()
    repo.record_market_snapshot(
        conn,
        _snap(
            ts=datetime(2026, 5, 1, 12, 0, tzinfo=UTC),
            iv_minus_rv=Decimal("99"),
            fetch_ok=False,
        ),
    )
    conn.commit()
    n = repo.count_iv_rv_distinct_days(
        conn,
        asset="ETH",
        max_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert n == 0


def test_count_distinct_days_rejects_naive_as_of(db_one_day) -> None:
    repo = Repository()
    with pytest.raises(ValueError, match="timezone-aware"):
        repo.count_iv_rv_distinct_days(
            db_one_day,
            asset="ETH",
            max_days=60,
            as_of=datetime(2026, 5, 2, 0, 0),  # naive
        )


def test_count_distinct_days_rejects_non_positive_max_days(db_one_day) -> None:
    repo = Repository()
    with pytest.raises(ValueError, match="max_days must be positive"):
        repo.count_iv_rv_distinct_days(
            db_one_day,
            asset="ETH",
            max_days=0,
            as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
        )


# ---------------------------------------------------------------------------
# iv_rv_values_for_window
# ---------------------------------------------------------------------------


def test_values_for_window_returns_ordered_asc(db_one_day) -> None:
    repo = Repository()
    values = repo.iv_rv_values_for_window(
        db_one_day,
        asset="ETH",
        window_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert len(values) == 96
    assert values == sorted(values)
    assert values[0] == Decimal("2.00")


def test_values_for_window_filters_other_asset(db_one_day) -> None:
    repo = Repository()
    values = repo.iv_rv_values_for_window(
        db_one_day,
        asset="BTC",
        window_days=60,
        as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
    )
    assert values == []


def test_values_for_window_skips_null(db_one_day) -> None:
    repo = Repository()
    repo.record_market_snapshot(
        db_one_day,
        _snap(ts=datetime(2026, 5, 2, 0, 0, tzinfo=UTC), iv_minus_rv=None),
    )
    db_one_day.commit()
    values = repo.iv_rv_values_for_window(
        db_one_day,
        asset="ETH",
        window_days=60,
        as_of=datetime(2026, 5, 3, 0, 0, tzinfo=UTC),
    )
    assert len(values) == 96


def test_values_for_window_skips_fetch_failed(db_one_day) -> None:
    repo = Repository()
    repo.record_market_snapshot(
        db_one_day,
        _snap(
            ts=datetime(2026, 5, 3, 0, 0, tzinfo=UTC),
            iv_minus_rv=Decimal("99"),
            fetch_ok=False,
        ),
    )
    db_one_day.commit()
    values = repo.iv_rv_values_for_window(
        db_one_day,
        asset="ETH",
        window_days=60,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert Decimal("99") not in values


def test_values_for_window_respects_window_cutoff(
    db_three_days_mixed,
) -> None:
    """window_days=1 da as_of=2026-05-04 → solo day3 (4 valori 200..203)."""
    repo = Repository()
    values = repo.iv_rv_values_for_window(
        db_three_days_mixed,
        asset="ETH",
        window_days=1,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert values == [Decimal(200 + i) for i in range(4)]


def test_values_for_window_full_window(db_three_days_mixed) -> None:
    """window_days=60: tutti i valori dei 3 giorni (96 + 1 + 4 = 101)."""
    repo = Repository()
    values = repo.iv_rv_values_for_window(
        db_three_days_mixed,
        asset="ETH",
        window_days=60,
        as_of=datetime(2026, 5, 4, 0, 0, tzinfo=UTC),
    )
    assert len(values) == 101


def test_values_for_window_rejects_naive_as_of(db_one_day) -> None:
    repo = Repository()
    with pytest.raises(ValueError, match="timezone-aware"):
        repo.iv_rv_values_for_window(
            db_one_day,
            asset="ETH",
            window_days=60,
            as_of=datetime(2026, 5, 2, 0, 0),
        )


def test_values_for_window_rejects_non_positive_window(db_one_day) -> None:
    repo = Repository()
    with pytest.raises(ValueError, match="window_days must be positive"):
        repo.iv_rv_values_for_window(
            db_one_day,
            asset="ETH",
            window_days=0,
            as_of=datetime(2026, 5, 2, 0, 0, tzinfo=UTC),
        )


# ---------------------------------------------------------------------------
# dvol_lookback (regression — invariato dopo refactor)
# ---------------------------------------------------------------------------


def test_dvol_lookback_returns_closest_tick(db_one_day) -> None:
    repo = Repository()
    base = datetime(2026, 5, 1, 0, 0, tzinfo=UTC)
    target = base + timedelta(hours=12)
    out = repo.dvol_lookback(
        db_one_day, asset="ETH", reference=target, tolerance_minutes=15
    )
    # i=48 → dvol = 50 + 4.8 = 54.8
    assert out == Decimal("54.8")


def test_dvol_lookback_returns_none_when_gap(db_one_day) -> None:
    repo = Repository()
    target = datetime(2025, 1, 1, 0, 0, tzinfo=UTC)
    out = repo.dvol_lookback(
        db_one_day, asset="ETH", reference=target, tolerance_minutes=15
    )
    assert out is None


def test_dvol_lookback_rejects_naive_reference(db_one_day) -> None:
    repo = Repository()
    with pytest.raises(ValueError, match="timezone-aware"):
        repo.dvol_lookback(
            db_one_day,
            asset="ETH",
            reference=datetime(2026, 5, 1, 12, 0),
        )
