"""BybitOpenInterest adapter — the OPEN INTEREST (deleveraging-flow vehicle) from Bybit v5 market data.

ALL HTTP is injected/mocked — no network in these tests. The adapter takes a `fetch(url)->dict` callable
(the JSON endpoint), so the tests drive it with canned paginated responses and assert: the {epoch_day:
open_interest} mapping, backward pagination via endTime to a floor, the floor stop, dedup, and rate-limit
backoff.
"""
from __future__ import annotations

from fxhnt.adapters.data.bybit_open_interest import BYBIT_OI_FLOOR_MS, BybitOpenInterest

_DAY_MS = 86_400_000


def _oi_row(ts_ms: int, oi: float) -> dict:
    # open-interest rows carry openInterest + a ms timestamp.
    return {"openInterest": str(oi), "timestamp": str(ts_ms)}


class _PagedOI:
    """Mock of /v5/market/open-interest: serves rows newest-first, honoring the `endTime` cursor exactly like
    Bybit (returns up to `limit` rows with timestamp <= endTime, descending). When the caller walks endTime
    back past the oldest row, it returns an empty list (stops the loop)."""

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

    def fetch(self, url: str) -> dict:
        end = _parse_qs_int(url, "endTime")
        self.calls.append(end)
        cand = self._rows if end is None else [r for r in self._rows
                                               if int(r["timestamp"]) <= end]
        page = cand[: self._limit]
        return {"retCode": 0, "result": {"list": 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_oi_history_maps_epoch_day_to_open_interest_and_paginates_backward():
    # intervalTime=1d → one row per day. Force paging with limit=2 across 4 days.
    rows = [
        _oi_row(100 * _DAY_MS, 1_000_000.0),
        _oi_row(99 * _DAY_MS, 900_000.0),
        _oi_row(98 * _DAY_MS, 1_100_000.0),
        _oi_row(97 * _DAY_MS, 1_050_000.0),
    ]
    paged = _PagedOI(rows, limit=2)
    bb = BybitOpenInterest(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.oi_history("BTCUSDT", start_ms=0)  # tiny synthetic epoch-days < the floor
    assert daily == {100: 1_000_000.0, 99: 900_000.0, 98: 1_100_000.0, 97: 1_050_000.0}
    assert len(paged.calls) >= 2   # it paginated, walking endTime backward


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

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

    bb = BybitOpenInterest(fetch=fetch, sleep=lambda _: None)
    bb.oi_history("BTCUSDT")
    assert "open-interest" in seen[0]
    assert "category=linear" in seen[0]
    assert "intervalTime=1d" in seen[0]
    assert "symbol=BTCUSDT" in seen[0]


def test_oi_history_empty_page_stops_loop():
    paged = _PagedOI([], limit=200)
    bb = BybitOpenInterest(fetch=paged.fetch, sleep=lambda _: None)
    assert bb.oi_history("NEWUSDT") == {}
    assert len(paged.calls) == 1


def test_oi_history_respects_start_ms_floor():
    rows = [_oi_row(10 * _DAY_MS, 5.0), _oi_row(20 * _DAY_MS, 6.0)]
    paged = _PagedOI(rows, limit=200)
    bb = BybitOpenInterest(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.oi_history("BTCUSDT", start_ms=15 * _DAY_MS)
    assert 20 in daily and 10 not in daily   # only the row at/after the floor survives


def test_default_floor_is_2023_01():
    import datetime as dt
    assert int(dt.datetime(2023, 1, 1, tzinfo=dt.UTC).timestamp() * 1000) == BYBIT_OI_FLOOR_MS


def test_oi_history_retries_on_rate_limit_retcode_then_succeeds():
    d = 30 * _DAY_MS
    real = _PagedOI([_oi_row(d, 7.0)], limit=200)
    state = {"n": 0}

    def flaky(url: str) -> dict:
        state["n"] += 1
        if state["n"] <= 2:
            return {"retCode": 10006, "retMsg": "too many visits", "result": {}}
        return real.fetch(url)

    waited: list[float] = []
    bb = BybitOpenInterest(fetch=flaky, sleep=waited.append)
    daily = bb.oi_history("BTCUSDT", start_ms=0)
    assert abs(daily[30] - 7.0) < 1e-12
    assert state["n"] >= 3
    assert waited[:2] == [0.5, 1.0]   # exponential backoff (no real sleep)


def test_oi_history_dedups_across_page_boundaries():
    rows = [_oi_row(d * _DAY_MS, 0.5 + d) for d in range(50, 55)]
    paged = _PagedOI(rows, limit=2)
    bb = BybitOpenInterest(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.oi_history("BTCUSDT", start_ms=0)
    assert set(daily) == {50, 51, 52, 53, 54}
