"""Unit tests for equity_honest.py — the equity analog of the crypto honest_reference cost model.

Covers: dollar_adv trailing-median + missing-volume exclusion; the cost invariant (net Sharpe <= gross
Sharpe, zero-turnover == no cost); participation-cap capacity scaling; capacity-curve AUM monotonicity."""
from __future__ import annotations

import math

import numpy as np
import pytest

from fxhnt.application.equity_honest import (
    ETF_HALF_SPREAD_BPS,
    _spread_bps,
    dollar_adv,
    equity_capacity_curve,
    equity_honest_returns,
)
from fxhnt.domain.gauntlet import per_period_sharpe


def test_spread_constants_match_global_constraints():
    assert ETF_HALF_SPREAD_BPS == {"SPY": 0.3, "IEF": 1.0, "GLD": 1.0, "PDBC": 4.0, "DBMF": 8.0}
    assert _spread_bps("SPY") == 0.3
    assert _spread_bps("DBMF") == 8.0
    assert _spread_bps("UNLISTED_ETF") == 5.0  # documented default


def test_dollar_adv_carries_median_across_missing_and_pre_history_days():
    # A close date with no volume entry (2026-01-04) is CARRIED from the nearest preceding volume date, and a
    # close date BEFORE the first volume date (2025-12-31) is back-filled with the earliest median. Capacity is
    # a current-liquidity concept -- old return days must be sized at the nearest liquidity, not zeroed.
    days = ["2025-12-31", "2026-01-01", "2026-01-02", "2026-01-03", "2026-01-04", "2026-01-05"]
    closes = {"SPY": {d: 100.0 for d in days}}
    volumes = {"SPY": {"2026-01-01": 10.0, "2026-01-02": 20.0, "2026-01-03": 30.0, "2026-01-05": 50.0}}

    adv = dollar_adv(closes, volumes, window=3)

    # trailing window=3 medians AT the volume dates: 1000, 1500, 2000, (3000 @ 01-05)
    assert adv["SPY"]["2026-01-01"] == 1000.0                 # median(1000)
    assert adv["SPY"]["2026-01-02"] == 1500.0                 # median(1000,2000)
    assert adv["SPY"]["2026-01-03"] == 2000.0                 # median(1000,2000,3000)
    assert adv["SPY"]["2026-01-05"] == 3000.0                 # median(2000,3000,5000)
    # carry-fill: 01-04 (no volume) inherits the nearest preceding volume-date median (01-03 = 2000)
    assert adv["SPY"]["2026-01-04"] == 2000.0
    # back-fill: 2025-12-31 (before the first volume date) inherits the earliest median (01-01 = 1000)
    assert adv["SPY"]["2025-12-31"] == 1000.0


def test_dollar_adv_no_volume_at_all_is_empty():
    closes = {"SPY": {"2026-01-01": 100.0, "2026-01-02": 100.0}}
    assert dollar_adv(closes, {"SPY": {}}, window=3)["SPY"] == {}   # coverage guard trips downstream


def test_zero_turnover_no_cost_after_entry():
    days = ["2026-01-01", "2026-01-02", "2026-01-03"]
    book = {d: 0.001 for d in days}
    w = {d: {"SPY": 0.5} for d in days}                       # constant weights -> no turnover after day 1
    adv = {"SPY": {d: 1e12 for d in days}}                    # effectively infinite liquidity -> no impact/cap

    net = equity_honest_returns(book, w, adv, aum=100_000.0)

    # day 1 enters the position (turnover 0 -> 0.5) -> a small spread-only cost is charged.
    entry_cost = 0.5 * (ETF_HALF_SPREAD_BPS["SPY"] / 1e4)
    assert net["2026-01-01"] == pytest.approx(book["2026-01-01"] - entry_cost, abs=1e-12)
    assert net["2026-01-01"] < book["2026-01-01"]
    # days 2-3: zero turnover, zero capacity scaling -> net == gross exactly.
    assert net["2026-01-02"] == pytest.approx(book["2026-01-02"], abs=1e-12)
    assert net["2026-01-03"] == pytest.approx(book["2026-01-03"], abs=1e-12)


def test_costs_reduce_sharpe_over_turnover_series():
    days = ["2026-02-02", "2026-02-03", "2026-02-04", "2026-02-05", "2026-02-06", "2026-02-09"]
    book = dict(zip(days, [0.004, -0.002, 0.003, -0.001, 0.005, 0.002], strict=True))
    w = {d: {"SPY": weight} for d, weight in zip(days, [0.20, 0.50, 0.15, 0.60, 0.25, 0.55], strict=True)}
    adv = {"SPY": {d: 5_000_000.0 for d in days}}             # finite but ample -> no capacity cap triggered
    aum = 200_000.0                                           # notional max 0.6*200k=120k < 0.03*5M=150k

    net = equity_honest_returns(book, w, adv, aum=aum)

    gross_sh = per_period_sharpe(np.array(list(book.values())))
    net_sh = per_period_sharpe(np.array(list(net.values())))

    # every day has nonzero turnover -> a real cost is charged every day -> net strictly below gross.
    for d in days:
        assert net[d] < book[d]
    assert net_sh <= gross_sh
    assert net_sh < gross_sh


def test_participation_cap_scales_down_oversized_position():
    day = "2026-03-02"
    book = {day: 0.01}
    w = {day: {"SPY": 1.0}}
    adv_val = 1_000_000.0
    adv = {"SPY": {day: adv_val}}
    aum = 1_000_000_000.0                                     # deliberately huge vs ADV -> forces the cap

    net = equity_honest_returns(book, w, adv, aum=aum)

    cap_notional = 0.03 * adv_val                             # participation_cap default = 0.03
    scale = cap_notional / (1.0 * aum)
    scale_hit = 1.0 - scale
    traded_w = scale                                          # prev={} -> full capped weight is "traded"
    traded_notional = traded_w * aum
    impact_bps = 1.0 * 400.0 * math.sqrt(traded_notional / adv_val)
    slip_bps = max(impact_bps, ETF_HALF_SPREAD_BPS["SPY"])
    expected_cost = traded_w * (slip_bps / 1e4)
    expected_net = book[day] * (1.0 - scale_hit) - expected_cost

    assert net[day] == pytest.approx(expected_net, abs=1e-12)
    # the capped position earns only a tiny fraction of the day's gross return.
    assert net[day] < book[day] * 0.01
    assert net[day] > 0.0


def test_capacity_curve_monotone_higher_aum_lower_or_equal_sharpe():
    # Weight is sized UP on the good (positive) days and DOWN on the bad (negative) days -- mimicking a
    # genuinely skilled strategy, so that capping the large positions on the best days is what erodes
    # Sharpe (not an artifact of an unrelated weight/return correlation).
    days = ["2026-04-01", "2026-04-02", "2026-04-03", "2026-04-06", "2026-04-07", "2026-04-08"]
    book = dict(zip(days, [0.004, -0.001, 0.005, -0.0015, 0.006, -0.002], strict=True))
    w = {d: {"SPY": weight} for d, weight in zip(days, [0.6, 0.2, 0.65, 0.15, 0.7, 0.1], strict=True)}
    adv = {"SPY": {d: 5_000_000.0 for d in days}}
    # Grid stays within the "capacity progressively engaging" regime (below full saturation, where the
    # book-level scale_hit approximation's cost term would eventually shrink back toward zero along with
    # the position itself -- see the approximation note on equity_honest_returns).
    aums = [30_000.0, 60_000.0, 100_000.0, 150_000.0, 200_000.0, 260_000.0]

    curve = equity_capacity_curve(book, w, adv, aums=aums)
    sharpes = [per_period_sharpe(np.array(list(curve[aum].values()))) for aum in aums]

    for lo, hi in zip(sharpes, sharpes[1:], strict=False):
        assert hi <= lo + 1e-9
    # the largest AUM should be materially worse than the smallest (capacity clearly binds).
    assert sharpes[-1] < sharpes[0]
