"""Task 4 — `honest_allocation_inputs`: the deploy-strategy -> honest-reference series + capacity ceiling
provider. Uses an in-memory sqlite `TimescaleFeatureStore` seeded like the fidelity test's fixture (flat
cross-sectional funding for xsfunding + a per-coin long_ratio/price wiggle for positioning), so BOTH crypto
sleeves are computable WITHOUT an unlock calendar. Asserts the crypto deploy mapping per the 2026-07-16 plan
(`docs/superpowers/plans/2026-07-16-book-representation-and-venue-cap.md`, Task A): `bybit_4edge` is back as
the deploy target and gets the risk-parity BOOK series (`_book_curve`); `positioning`/`xsfunding`/`unlock`
remain available as their own SLEEVE series (`_deploy_curve`) if ever deployed standalone — the int->str key
seam, and that positioning and xsfunding series share a common string key (so `deploy_book_returns` can
align them). Also covers the load-once/reprice refactor (`2026-07-16-honest-inputs-load-once-reprice.md`
Task 1): `read_panel` must be called ONCE per distinct sleeve regardless of the AUM grid size, not once per
(sleeve x AUM)."""
from __future__ import annotations

import datetime as dt
import math

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.allocation_honest_inputs import (
    _CRYPTO_DEPLOY_SIDS,
    honest_allocation_inputs,
)

_DAY = 86_400
# Base the fixture at a REAL calendar date (2026-01-01) so the epoch-day keys convert to ISO dates in the
# 2020-2027 window the key-seam assertion checks (a fixture anchored at epoch day 0 would land in 1970).
_BASE_DAY = (dt.date(2026, 1, 1) - dt.date(1970, 1, 1)).days


def _seed(store: TimescaleFeatureStore, *, days: int = 120) -> None:
    """Seed a self-contained crypto fixture:
      * xsfunding: FLAT-basis (spot == close each day) so carry return == the cross-sectional funding
        (3 coins +, 3 coins -) — the proven `_seed_flat_funding` shape;
      * positioning: a per-coin price WIGGLE (so next-day returns are non-trivial + cross-sectionally
        dispersed) with a per-coin long_ratio signal;
      * turnover: huge ADV so the participation cap is non-binding at the tested AUMs.
    Prices stay positive; spot tracks close each day so the xsfunding basis change is zero (return == funding).
    """
    funding = {"AAAUSDT": 0.002, "BBBUSDT": 0.002, "CCCUSDT": 0.002,
               "XXXUSDT": -0.002, "YYYUSDT": -0.002, "ZZZUSDT": -0.002}
    ratios = {"AAAUSDT": 0.70, "BBBUSDT": 0.60, "CCCUSDT": 0.55,
              "XXXUSDT": 0.45, "YYYUSDT": 0.40, "ZZZUSDT": 0.30}
    for i, (sym, f) in enumerate(funding.items()):
        rows = []
        for d in range(days):
            # deterministic per-coin wiggle -> real, cross-sectionally dispersed close-to-close returns.
            price = 100.0 + 5.0 * math.sin(0.3 * d + i)
            ratio = ratios[sym] + 0.05 * math.sin(0.2 * d + i)
            rows.append(((_BASE_DAY + d) * _DAY, {
                "close": price, "spot_close": price, "funding": f,
                "high": price, "low": price, "long_ratio": ratio,
                "turnover": 1.0e12,
            }))
        store.write_features(sym, rows)


def _seed_with_unlock(store: TimescaleFeatureStore, *, days: int = 120) -> list[dict]:
    """As `_seed`, plus a BTCUSDT hedge instrument (flat/neutral: zero funding, flat basis, so it barely
    perturbs the xsfunding cross-section) so the `unlock` sleeve is ALSO computable — `UnlockShortRunner`
    needs a BTCUSDT hedge leg (`market="BTCUSDT"`, the default) — and one supply-shock-eligible unlock-cliff
    event on AAA (mirrors the shape used by `tests/integration/test_bybit_paper_reconciliation.py`). Returns
    the `unlock_events` list the caller passes to `honest_allocation_inputs`."""
    _seed(store, days=days)
    price = 50_000.0
    rows = []
    for d in range(days):
        price *= 1.0 + 0.0005 * math.sin(0.1 * d)
        rows.append(((_BASE_DAY + d) * _DAY, {
            "close": price, "spot_close": price, "funding": 0.0,
            "high": price, "low": price, "turnover": 1.0e12,
        }))
    store.write_features("BTCUSDT", rows)
    # Cliff near the end of the sample so the 10-day pre-window is inside it; n large enough that the
    # supply-shock (n*price / warehouse-qvol-fallback 1e7) clears the runner's min_shock (0.1).
    cliff = _BASE_DAY + days - 5
    return [{"sym": "AAA", "cliff_day": cliff, "n": 5_000_000.0, "cat": "insiders"}]


def test_honest_allocation_inputs_crypto_only_mapping_and_key_seam() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)

    returns, ceilings = honest_allocation_inputs(
        store, spread_bps={}, unlock_events=[],
        deploy_sids={"positioning", "xsfunding", "bybit_4edge", "multistrat"},
        target_aum=35_000,
        aums=[35_000, 100_000, 350_000, 1_000_000, 10_000_000],
    )
    store.close()

    # 1. crypto series for the sleeves AND the book: `bybit_4edge` (the deploy target, Task A) gets its
    #    risk-parity BOOK series; `multistrat` (equity) gets NO series in the allocation path.
    assert set(returns) == {"positioning", "xsfunding", "bybit_4edge"}

    # 2. every series is non-empty and ISO-DATE-STRING-keyed (the epoch-day int -> ISO date seam): each key
    #    contains "-", parses via date.fromisoformat, and falls in the 2020-2027 window (proves the epoch base
    #    is 1970-01-01 and NOT, e.g., epoch SECONDS or a wrong ordinal base).
    for sid, series in returns.items():
        assert series, f"{sid} series is empty"
        for k in series:
            assert isinstance(k, str) and "-" in k, f"{sid} has a non-ISO key: {k!r}"
            parsed = dt.date.fromisoformat(k)
            assert dt.date(2020, 1, 1) <= parsed <= dt.date(2027, 12, 31), f"{sid} key out of window: {k}"

    # 3. the non-crypto sid gets the equity default ceiling (no honest series to size a real ceiling from).
    assert ceilings["multistrat"] == 10_000_000.0

    # 4. each crypto sid (the sleeves AND the book) has a ceiling (a numeric AUM-or-None; the ceiling
    #    inherits the AUM key's type, so with the int aums passed here it is an int — with non-binding
    #    capacity the honest Sharpe stays above min_sharpe at every AUM, so the ceiling is the top of the
    #    range).
    for sid in ("positioning", "xsfunding", "bybit_4edge"):
        assert sid in ceilings
        assert ceilings[sid] is None or isinstance(ceilings[sid], (int, float))

    # 5. KEY ALIGNMENT: positioning and xsfunding share >= 1 common string key, so deploy_book_returns
    #    will cross-align them (the consistent epoch-day -> ISO conversion across strategies).
    common = set(returns["positioning"]) & set(returns["xsfunding"])
    assert common, "positioning and xsfunding share no common key -> deploy_book_returns cannot align them"
    assert all(isinstance(k, str) and dt.date.fromisoformat(k) for k in common)


def test_crypto_deploy_sids_are_the_three_sleeves_plus_the_book() -> None:
    assert _CRYPTO_DEPLOY_SIDS == frozenset({"positioning", "xsfunding", "unlock", "bybit_4edge"})


def test_bybit_4edge_gets_the_book_series_not_a_sleeve_series() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)

    returns, ceilings = honest_allocation_inputs(
        store, spread_bps={}, unlock_events=[],
        deploy_sids={"bybit_4edge"}, target_aum=35_000.0, aums=[35_000.0, 350_000.0],
    )
    store.close()

    # non-empty, ISO-DATE-STRING-keyed BOOK series (risk-parity of xsfunding + positioning; unlock drops
    # out with no events) — bybit_4edge is the deploy target again (Task A), not a fall-through sid.
    assert "bybit_4edge" in returns and returns["bybit_4edge"]
    for k in returns["bybit_4edge"]:
        assert isinstance(k, str) and dt.date.fromisoformat(k)
    assert ceilings["bybit_4edge"] != 10_000_000.0


class _CountingStore:
    """Wraps a real `TimescaleFeatureStore`, counting `read_panel` calls; everything else (including
    `crypto_close_panel`/`crypto_funding_panel`/`crypto_spot_panel`, which do NOT go through `read_panel`)
    delegates unchanged to the wrapped store via `__getattr__`. This is the LOAD-ONCE invariant's guard: the
    heavy panel read must happen once per distinct sleeve regardless of how many AUMs are repriced, NOT once
    per (sleeve x AUM)."""

    def __init__(self, inner) -> None:
        self._inner = inner
        self.read_panel_calls = 0

    def read_panel(self, symbols, feature):
        self.read_panel_calls += 1
        return self._inner.read_panel(symbols, feature)

    def __getattr__(self, name: str):
        return getattr(self._inner, name)


_FIVE_AUMS = [35_000, 100_000, 350_000, 1_000_000, 10_000_000]


def test_read_panel_load_once_for_the_book_across_the_aum_grid() -> None:
    """`bybit_4edge` books xsfunding + unlock + positioning. With `unlock_events=[]` the unlock sleeve is
    uncomputable and short-circuits BEFORE any `read_panel` call. xsfunding's weight engine reads its panels
    via `crypto_close_panel`/`crypto_funding_panel`/`crypto_spot_panel` (NOT `read_panel`), so its ONLY
    `read_panel` call is the turnover load. positioning's weight engine reads `long_ratio` via `read_panel`
    (1 call) PLUS the turnover load (1 call) = 2. Total per load-once cycle = 1 (xsfunding) + 2 (positioning)
    + 0 (unlock) = 3 — and that total must NOT grow with the 5-AUM grid (old, per-AUM-reload code hits 18:
    (1 base + 5 curve) x 3 sleeves-worth of calls)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    counting = _CountingStore(store)

    honest_allocation_inputs(
        counting, spread_bps={}, unlock_events=[],
        deploy_sids={"bybit_4edge"}, target_aum=100_000, aums=_FIVE_AUMS,
    )
    store.close()

    assert counting.read_panel_calls == 3, (
        f"expected read_panel called exactly 3 times (load-once across 5 AUMs), got {counting.read_panel_calls}"
    )


def test_read_panel_load_once_for_a_single_sleeve_across_the_aum_grid() -> None:
    """xsfunding's weight engine (`carry_weights_and_contributions`) reads its panels via
    `crypto_close_panel`/`crypto_funding_panel`/`crypto_spot_panel` (NOT `read_panel`), so loading xsfunding
    ONCE makes exactly 1 `read_panel` call (the turnover load) — and that count must stay 1 across the whole
    5-AUM grid, NOT scale with it (old, per-AUM-reload code hits 6: 1 base + 5 curve points)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    counting = _CountingStore(store)

    honest_allocation_inputs(
        counting, spread_bps={}, unlock_events=[],
        deploy_sids={"xsfunding"}, target_aum=100_000, aums=_FIVE_AUMS,
    )
    store.close()

    assert counting.read_panel_calls == 1, (
        f"expected read_panel called exactly 1 time (load-once across 5 AUMs), got {counting.read_panel_calls}"
    )


def test_xsfunding_and_unlock_get_a_series_not_equity_default() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed_with_unlock(store)

    returns, ceilings = honest_allocation_inputs(
        store, spread_bps={}, unlock_events=events,
        deploy_sids={"xsfunding", "unlock"}, target_aum=35_000.0, aums=[35_000.0, 350_000.0],
    )
    store.close()

    assert "xsfunding" in returns and returns["xsfunding"]       # sleeve-sized, ISO-keyed
    assert "unlock" in returns and returns["unlock"]
    # ceilings present (not the equity default 10M sentinel for these crypto sleeves)
    assert ceilings["xsfunding"] != 10_000_000.0
    assert ceilings["unlock"] != 10_000_000.0
