"""BybitAccountRatio adapter — the long/short ACCOUNT ratio (retail positioning) 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:
buy_ratio} mapping, backward pagination via endTime/startTime to a floor, the floor stop, and rate-limit
backoff.
"""
from __future__ import annotations

from fxhnt.adapters.data.bybit_account_ratio import BYBIT_RATIO_FLOOR_MS, BybitAccountRatio

_DAY_MS = 86_400_000


def _ratio_row(ts_ms: int, buy: float) -> dict:
    # account-ratio rows carry buyRatio/sellRatio (fractions of accounts long/short) + a ms timestamp.
    return {"symbol": "X", "buyRatio": str(buy), "sellRatio": str(round(1.0 - buy, 6)),
            "timestamp": str(ts_ms)}


class _PagedRatio:
    """Mock of /v5/market/account-ratio: 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_long_ratio_history_maps_epoch_day_to_buy_ratio_and_paginates_backward():
    # period=1d → one row per day. Force paging with limit=2 across 4 days.
    rows = [
        _ratio_row(100 * _DAY_MS, 0.60),
        _ratio_row(99 * _DAY_MS, 0.55),
        _ratio_row(98 * _DAY_MS, 0.48),
        _ratio_row(97 * _DAY_MS, 0.52),
    ]
    paged = _PagedRatio(rows, limit=2)
    bb = BybitAccountRatio(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.long_ratio_history("BTCUSDT", start_ms=0)  # tiny synthetic epoch-days < the floor
    assert daily == {100: 0.60, 99: 0.55, 98: 0.48, 97: 0.52}
    # It paginated (>1 fetch) and walked endTime backward.
    assert len(paged.calls) >= 2


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

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

    bb = BybitAccountRatio(fetch=fetch, sleep=lambda _: None)
    bb.long_ratio_history("BTCUSDT")
    assert "account-ratio" in seen[0]
    assert "category=linear" in seen[0]
    assert "period=1d" in seen[0]
    assert "symbol=BTCUSDT" in seen[0]


def test_long_ratio_history_empty_page_stops_loop():
    paged = _PagedRatio([], limit=200)
    bb = BybitAccountRatio(fetch=paged.fetch, sleep=lambda _: None)
    assert bb.long_ratio_history("NEWUSDT") == {}
    assert len(paged.calls) == 1


def test_long_ratio_history_respects_start_ms_floor():
    rows = [_ratio_row(10 * _DAY_MS, 0.5), _ratio_row(20 * _DAY_MS, 0.6)]
    paged = _PagedRatio(rows, limit=200)
    bb = BybitAccountRatio(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.long_ratio_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_RATIO_FLOOR_MS


def test_long_ratio_history_retries_on_rate_limit_retcode_then_succeeds():
    d = 30 * _DAY_MS
    real = _PagedRatio([_ratio_row(d, 0.7)], 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 = BybitAccountRatio(fetch=flaky, sleep=waited.append)
    daily = bb.long_ratio_history("BTCUSDT", start_ms=0)
    assert abs(daily[30] - 0.7) < 1e-12
    assert state["n"] >= 3
    assert waited[:2] == [0.5, 1.0]   # exponential backoff (no real sleep)


def test_long_ratio_history_dedups_across_page_boundaries():
    # A row that re-appears at the page boundary (ts == endTime) must not be double-counted/looped forever.
    rows = [_ratio_row(d * _DAY_MS, 0.5 + d / 1000.0) for d in range(50, 55)]
    paged = _PagedRatio(rows, limit=2)
    bb = BybitAccountRatio(fetch=paged.fetch, sleep=lambda _: None)
    daily = bb.long_ratio_history("BTCUSDT", start_ms=0)
    assert set(daily) == {50, 51, 52, 53, 54}
