"""DeribitFunding adapter — Deribit perp funding-rate history (the Deribit leg of the cross-venue carry).

ALL HTTP is injected/mocked — no network in these tests. The adapter takes a `fetch(url)->dict` callable
(the get_funding_rate_history JSON endpoint), so the tests drive it with canned paginated responses and
assert: the {epoch_day: daily_funding} mapping (DAILY = SUM of the UTC day's hourly interest_1h rows,
matching the Bybit daily-sum convention), backward pagination via end_timestamp to a floor, the floor stop,
the empty-page stop, dedupe across page boundaries, and rate-limit/transient backoff.
"""
from __future__ import annotations

import math

from fxhnt.adapters.data.deribit_funding import DERIBIT_FUNDING_FLOOR_MS, DeribitFunding

_HOUR_MS = 3_600_000
_DAY_MS = 86_400_000

# The adapter anchors the first page at the REAL `now` and walks end_timestamp BACKWARD in bounded ~30d
# windows toward the floor (this windowing is the fix for Deribit's HTTP 400 on >~1yr windows). Synthetic
# rows must therefore sit at REALISTIC recent timestamps (just below `now`, above the 2021 floor) so the
# backward walk actually reaches them. We anchor on a fixed recent UTC day so the tests are deterministic.
import datetime as _dt  # noqa: E402

_NOW_DAY = int(_dt.datetime(2026, 6, 1, tzinfo=_dt.UTC).timestamp() // 86_400)  # epoch-day of a recent date

# DETERMINISM SEAM: pin the adapter's wall-clock to noon of `_NOW_DAY` so the backward-windowed walk anchors
# at a fixed `now` regardless of the calendar date the suite runs on. The adapter's first page sets
# end_timestamp = now and walks ~30d windows back to the floor; if `now` were the real wall clock, a floor
# placed a few days below `_NOW_DAY` drifts out of the first 30d window as real time advances (the bug this
# fixes: the empty-page test took 2 pages instead of 1 once today was >30d past the floor). All construction
# sites pass `clock=_FIXED_CLOCK`, and the guard below pins the anchor so this can never regress.
_NOW_MS = _NOW_DAY * _DAY_MS + _DAY_MS // 2          # noon of `_NOW_DAY`, in ms
def _FIXED_CLOCK() -> float:                          # noqa: N802 — constant-like injectable clock
    return _NOW_MS / 1000.0


def test_fixed_clock_anchor_is_pinned():
    # Regression guard: the injected clock is a CONSTANT (date-independent), not the wall clock. If this ever
    # reverts to time.time(), the windowed-pagination tests become calendar-dependent again.
    assert _FIXED_CLOCK() == _NOW_MS / 1000.0
    assert int(_FIXED_CLOCK()) == 1780315200  # noon 2026-06-01 UTC — frozen, never tracks today


def _row(ts_ms: int, interest_1h: float) -> dict:
    # Deribit get_funding_rate_history rows: {timestamp, interest_1h, interest_8h, ...}.
    return {"timestamp": ts_ms, "interest_1h": interest_1h, "interest_8h": interest_1h * 8.0}


class _PagedFunding:
    """Mock of /public/get_funding_rate_history: serves rows oldest->newest within the requested
    [start_timestamp, end_timestamp] window, honoring the end_timestamp cursor like Deribit (returns up to
    `limit` rows ending at end_timestamp, oldest-first). Walking end_timestamp back past the oldest row
    returns an empty list (stops the loop)."""

    def __init__(self, rows: list[dict], *, limit: int = 1000) -> None:
        self._rows = sorted(rows, key=lambda r: r["timestamp"])
        self._limit = limit
        self.calls: list[tuple[int | None, int | None]] = []

    def fetch(self, url: str) -> dict:
        start = _parse_qs_int(url, "start_timestamp")
        end = _parse_qs_int(url, "end_timestamp")
        self.calls.append((start, end))
        cand = [r for r in self._rows
                if (start is None or r["timestamp"] >= start) and (end is None or r["timestamp"] <= end)]
        page = cand[-self._limit:]   # newest `limit` rows in the window
        return {"result": page}


def _parse_qs_int(url: str, key: str) -> int | None:
    from urllib.parse import parse_qs, urlparse
    q = parse_qs(urlparse(url).query)
    return int(q[key][0]) if key in q else None


def test_funding_history_aggregates_hourly_into_daily_sum():
    # Recent day d0: three hourly rows -> daily = sum. Day d0-1: one row. (Anchored near `now` so the
    # backward-windowed walk reaches them; floor set just below so a single ~30d window covers both days.)
    d0 = _NOW_DAY - 2
    rows = [
        _row(d0 * _DAY_MS + 0 * _HOUR_MS, 0.00001),
        _row(d0 * _DAY_MS + 8 * _HOUR_MS, 0.00002),
        _row(d0 * _DAY_MS + 16 * _HOUR_MS, 0.00003),
        _row((d0 - 1) * _DAY_MS, 0.00005),
    ]
    paged = _PagedFunding(rows, limit=1000)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=(d0 - 1) * _DAY_MS)
    assert math.isclose(daily[d0], 0.00001 + 0.00002 + 0.00003, abs_tol=1e-15)
    assert math.isclose(daily[d0 - 1], 0.00005, abs_tol=1e-15)


def test_funding_history_paginates_backward():
    # Six consecutive recent days, tiny page limit so the adapter must walk end_timestamp back across pages.
    days = list(range(_NOW_DAY - 5, _NOW_DAY + 1))
    rows = [_row(d * _DAY_MS, 0.0001 * d) for d in days]
    paged = _PagedFunding(rows, limit=2)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=days[0] * _DAY_MS)
    assert set(daily) == set(days)
    assert len(paged.calls) >= 3   # walked end_timestamp backward across pages


def test_funding_history_url_carries_instrument():
    seen: list[str] = []

    def fetch(url: str) -> dict:
        seen.append(url)
        return {"result": []}

    der = DeribitFunding(fetch=fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    der.funding_history("ETH-PERPETUAL")
    assert "get_funding_rate_history" in seen[0]
    assert "instrument_name=ETH-PERPETUAL" in seen[0]


def test_funding_history_empty_page_stops_loop():
    # An empty page whose window already starts AT the floor (inception reached) ends the walk immediately:
    # one call, empty result. (start_ms within one ~30d window of `now` so the first page_start hits floor.)
    paged = _PagedFunding([], limit=1000)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    floor = (_NOW_DAY - 5) * _DAY_MS  # < 30d below now → first page_start == floor
    assert der.funding_history("BTC-PERPETUAL", start_ms=floor) == {}
    assert len(paged.calls) == 1


def test_funding_history_respects_start_ms_floor():
    # Two recent days straddling the floor: the day below start_ms is dropped, the one above is kept.
    below, above = _NOW_DAY - 10, _NOW_DAY - 3
    rows = [_row(below * _DAY_MS, 0.0001), _row(above * _DAY_MS, 0.0002)]
    paged = _PagedFunding(rows, limit=1000)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=(_NOW_DAY - 5) * _DAY_MS)
    assert above in daily and below not in daily


def test_default_floor_is_2021_01():
    import datetime as dt
    assert int(dt.datetime(2021, 1, 1, tzinfo=dt.UTC).timestamp() * 1000) == DERIBIT_FUNDING_FLOOR_MS


def test_funding_history_dedups_across_page_boundaries():
    # Many single-row recent days; with a small page the backward walk could otherwise re-count a boundary row.
    days = list(range(_NOW_DAY - 5, _NOW_DAY + 1))
    rows = [_row(d * _DAY_MS, 0.0001) for d in days]
    paged = _PagedFunding(rows, limit=2)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=days[0] * _DAY_MS)
    assert set(daily) == set(days)
    for d in daily.values():
        assert math.isclose(d, 0.0001, abs_tol=1e-15)   # each day counted EXACTLY once


def test_funding_history_retries_on_transient_error_then_succeeds():
    day = _NOW_DAY - 3
    real = _PagedFunding([_row(day * _DAY_MS, 0.0002)], limit=1000)
    state = {"n": 0}

    def flaky(url: str) -> dict:
        state["n"] += 1
        if state["n"] <= 2:
            raise RuntimeError("transient 502")
        return real.fetch(url)

    waited: list[float] = []
    der = DeribitFunding(fetch=flaky, sleep=waited.append, clock=_FIXED_CLOCK, backoff_base=0.5, backoff_tries=5)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=(_NOW_DAY - 5) * _DAY_MS)
    assert math.isclose(daily[day], 0.0002, abs_tol=1e-15)
    assert state["n"] >= 3
    assert waited[:2] == [0.5, 1.0]   # exponential backoff (no real sleep)


def test_resume_path_builds_valid_windows_never_start_ge_end():
    """REGRESSION (resume path the nightly hits in steady state): with a RECENT start_ms (4 days ago) the
    adapter must build VALID requests — integer start_timestamp STRICTLY < end_timestamp, within [floor, now]
    — and return the new days. (The actual prod 400 was an invalid INSTRUMENT name, not the window — Deribit
    tolerates any window — but this guards the request construction so a degenerate start>=end can never be
    emitted on the recent-floor path.)"""
    floor_day = _NOW_DAY - 4
    days = list(range(floor_day, _NOW_DAY + 1))
    rows = [_row(d * _DAY_MS, 0.0001 * d) for d in days]
    paged = _PagedFunding(rows, limit=1000)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    floor_ms = floor_day * _DAY_MS
    daily = der.funding_history("BTC-PERPETUAL", start_ms=floor_ms)

    assert set(daily) == set(days)                       # advanced over the new (recent) days
    assert paged.calls                                   # at least one request was issued
    for start, end in paged.calls:
        assert start is not None and end is not None
        assert isinstance(start, int) and isinstance(end, int)   # integer ms, no scientific notation
        assert start < end, f"degenerate window emitted (start>=end): {start} >= {end}"
        assert start >= floor_ms                          # never below the requested floor
        assert end <= _NOW_MS                             # never above now


def test_every_page_window_is_bounded_to_30d():
    # REGRESSION GUARD for the Deribit HTTP-400 bug: Deribit 400s on windows wider than ~1yr (caps ~744
    # rows/request), so EVERY page MUST request a bounded ~30d window. The OLD code anchored the first page
    # at start_timestamp=floor (≈2021) + end_timestamp=now → a ~5y window → 400. Here we drive a continuous
    # daily series spanning a wide range (well over a year) and assert every requested page window is ≤ ~31d.
    days = list(range(_NOW_DAY - 500, _NOW_DAY + 1))  # ~1.4y of continuous daily funding rows
    rows = [_row(d * _DAY_MS, 0.0001) for d in days]
    paged = _PagedFunding(rows, limit=1000)
    der = DeribitFunding(fetch=paged.fetch, sleep=lambda _: None, clock=_FIXED_CLOCK)
    daily = der.funding_history("BTC-PERPETUAL", start_ms=days[0] * _DAY_MS)
    assert set(daily) == set(days)                      # full backward walk over >1y of continuous data
    assert len(paged.calls) >= 2                        # required multiple bounded pages (not one wide window)
    max_window_ms = 31 * _DAY_MS
    for start, end in paged.calls:
        assert start is not None and end is not None
        assert end - start <= max_window_ms            # every page bounded → never trips Deribit's >1y 400
