"""ingest_bybit_funding writes Bybit daily funding into a bybit_features TimescaleFeatureStore, isolated
from the default features table, idempotent on re-run. The Bybit adapter is a fake (no HTTP)."""
from __future__ import annotations

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_funding_ingest import ingest_bybit_funding


class _FakeBybit:
    """Stand-in for BybitFunding: returns canned {epoch_day: daily_funding} per symbol."""

    def __init__(self, panels: dict[str, dict[int, float]]) -> None:
        self._panels = panels
        self.calls: list[tuple[str, int | None]] = []

    def funding_history(self, symbol: str, *, start_ms=None) -> dict[int, float]:
        self.calls.append((symbol, start_ms))
        return self._panels.get(symbol, {})


def test_ingest_writes_daily_funding_into_bybit_features():
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    bybit = _FakeBybit({
        "BTCUSDT": {19000: 0.0003, 19001: -0.0001},
        "ETHUSDT": {19000: 0.0005},
        "DEADUSDT": {},  # delisted with no funding rows in window -> contributes nothing
    })
    summary = ingest_bybit_funding(store, bybit, ["BTCUSDT", "ETHUSDT", "DEADUSDT"])

    panel = store.crypto_funding_panel()
    assert panel == {"BTCUSDT": {19000: 0.0003, 19001: -0.0001}, "ETHUSDT": {19000: 0.0005}}
    # read_panel(...,'funding') is the canonical reader too.
    assert store.read_panel(["BTCUSDT"], "funding") == {"BTCUSDT": {19000: 0.0003, 19001: -0.0001}}
    assert summary["symbols"] == 2          # only symbols that produced >=1 day-row
    assert summary["day_rows"] == 3
    assert summary["oldest_day"] == 19000
    store.close()


def test_ingest_is_idempotent():
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    bybit = _FakeBybit({"BTCUSDT": {19000: 0.0003, 19001: -0.0001}})
    ingest_bybit_funding(store, bybit, ["BTCUSDT"])
    first = store.crypto_funding_panel()
    ingest_bybit_funding(store, bybit, ["BTCUSDT"])  # re-run
    assert store.crypto_funding_panel() == first      # upsert overwrites in place, no duplicates
    store.close()


def test_ingest_isolated_from_default_features_table():
    # In-memory engines aren't shared across stores, so use a file to prove isolation by table name.
    import os
    import tempfile
    with tempfile.TemporaryDirectory() as td:
        fdsn = f"sqlite:///{os.path.join(td, 'shared.db')}"
        default = TimescaleFeatureStore(fdsn)
        bybit_store = TimescaleFeatureStore(fdsn, table="bybit_features")
        default.write_features("BTCUSDT", [(19000 * 86400, {"funding": 0.99})])
        ingest_bybit_funding(bybit_store, _FakeBybit({"BTCUSDT": {19000: 0.0003}}), ["BTCUSDT"])
        # The Bybit ingest did NOT touch the default features table.
        assert default.crypto_funding_panel() == {"BTCUSDT": {19000: 0.99}}
        assert bybit_store.crypto_funding_panel() == {"BTCUSDT": {19000: 0.0003}}
        default.close()
        bybit_store.close()


def test_ingest_progress_callback_invoked():
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    bybit = _FakeBybit({"BTCUSDT": {19000: 0.1}, "ETHUSDT": {19000: 0.2}})
    seen: list[str] = []
    ingest_bybit_funding(store, bybit, ["BTCUSDT", "ETHUSDT"], progress=lambda m: seen.append(m))
    assert any("BTCUSDT" in m for m in seen) and any("ETHUSDT" in m for m in seen)
    store.close()


def test_resume_advances_done_symbols_from_last_date_and_completes_the_rest():
    """DATE-RESUME (replaces symbol-skip): A,B already carry OLD funding -> re-fetched FROM THEIR LAST
    PERSISTED DAY so NEW days land (advance), while C,D are back-filled from the default floor. Nothing is
    skipped, so funding advances every run (the freeze fix)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # First (partial) run seeds A,B at day 19000 only.
    ingest_bybit_funding(store, _FakeBybit({"AUSDT": {19000: 0.1}, "BUSDT": {19000: 0.2}}),
                         ["AUSDT", "BUSDT"])

    # Resume run: the adapter now serves an EXTRA day (19001) for A,B and full history for C,D.
    panels = {
        "AUSDT": {19000: 0.1, 19001: 0.15}, "BUSDT": {19000: 0.2, 19001: 0.25},
        "CUSDT": {19000: 0.3}, "DUSDT": {19000: 0.4},
    }
    second = _FakeBybit(panels)
    seen: list[str] = []
    ingest_bybit_funding(
        store, second, ["AUSDT", "BUSDT", "CUSDT", "DUSDT"], resume=True,
        progress=lambda m: seen.append(m))

    # ALL four fetched (no skip); A,B resume from their last persisted day, C,D from the default floor (None).
    fetched = dict(second.calls)
    assert set(fetched) == {"AUSDT", "BUSDT", "CUSDT", "DUSDT"}
    assert fetched["AUSDT"] == 19000 * 86_400_000 and fetched["BUSDT"] == 19000 * 86_400_000
    assert fetched["CUSDT"] is None and fetched["DUSDT"] is None
    assert any("resuming from last persisted date" in m for m in seen)

    # The done symbols ADVANCED to the new day; the rest were back-filled.
    assert store.crypto_funding_panel() == {
        "AUSDT": {19000: 0.1, 19001: 0.15}, "BUSDT": {19000: 0.2, 19001: 0.25},
        "CUSDT": {19000: 0.3}, "DUSDT": {19000: 0.4},
    }
    store.close()


def test_no_resume_refetches_everything():
    """resume=False re-fetches every symbol (no skip), landing the identical final state (idempotent)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    panels = {"AUSDT": {19000: 0.1}, "BUSDT": {19000: 0.2}}
    ingest_bybit_funding(store, _FakeBybit(panels), ["AUSDT", "BUSDT"])

    second = _FakeBybit(panels)
    ingest_bybit_funding(store, second, ["AUSDT", "BUSDT"], resume=False)
    assert {c[0] for c in second.calls} == {"AUSDT", "BUSDT"}  # everything re-fetched
    assert store.crypto_funding_panel() == {"AUSDT": {19000: 0.1}, "BUSDT": {19000: 0.2}}
    store.close()


def test_resume_equals_single_full_run():
    """A partial run + a resume run must yield the SAME final state as one single full run (complete on the
    union, idempotent on the overlap)."""
    panels = {f"{c}USDT": {19000: float(i)} for i, c in enumerate("ABCDE", start=1)}
    syms = list(panels)

    partial = TimescaleFeatureStore("sqlite://", table="bybit_features")
    ingest_bybit_funding(partial, _FakeBybit(panels), syms[:2])          # crash after 2
    ingest_bybit_funding(partial, _FakeBybit(panels), syms, resume=True)  # resume the rest
    resumed_state = partial.crypto_funding_panel()
    partial.close()

    full = TimescaleFeatureStore("sqlite://", table="bybit_features")
    ingest_bybit_funding(full, _FakeBybit(panels), syms)
    full_state = full.crypto_funding_panel()
    full.close()

    assert resumed_state == full_state


def test_concurrent_ingest_writes_all_symbols_exactly_once():
    """With workers>1 every symbol is still fetched + written exactly once (no drops/dups under the pool),
    and the final panel matches the sequential result."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    panels = {f"S{i}USDT": {19000 + i: float(i) / 1000} for i in range(40)}
    bybit = _FakeBybit(panels)
    summary = ingest_bybit_funding(store, bybit, list(panels), workers=8)
    assert summary["symbols"] == 40 and summary["day_rows"] == 40
    assert store.crypto_funding_panel() == {s: d for s, d in panels.items()}
    assert sorted(c[0] for c in bybit.calls) == sorted(panels)  # each symbol fetched once
    store.close()


def test_concurrent_resume_refetches_all_from_last_date():
    """Under concurrency, date-resume re-fetches ALL symbols (the done half from their last persisted day),
    landing the full panel — no symbol is skipped (the per-symbol last-day map is queried once up front)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    panels = {f"S{i}USDT": {19000: float(i)} for i in range(20)}
    done_half = list(panels)[:10]
    ingest_bybit_funding(store, _FakeBybit(panels), done_half, workers=8)

    second = _FakeBybit(panels)
    ingest_bybit_funding(store, second, list(panels), resume=True, workers=8)
    fetched = dict(second.calls)
    assert set(fetched) == set(panels)                                  # ALL fetched, none skipped
    assert all(fetched[s] == 19000 * 86_400_000 for s in done_half)     # done half resumes from last date
    assert all(fetched[s] is None for s in list(panels)[10:])           # fresh half from the default floor
    assert store.crypto_funding_panel() == {s: d for s, d in panels.items()}
    store.close()
