"""Trading-cost + funding model for the paper book (toggleable, default OFF = gross).

Research-grounded per-side cost rates (Binance USDT-M perps; square-root impact law tiers the
spread/slippage by sleeve liquidity):
  crypto_tstrend       0.0008  (8 bp  — 5 bp taker fee + ~3 bp spread/slippage; liquid majors)
  stablecoin_rotation  0.0006  (6 bp  — liquid stablecoins)
  unlock               0.0035  (35 bp — 5 bp fee + ~30 bp spread/slippage; illiquid small-cap shorts)
  default              0.0015  (any other sleeve)
"""
from __future__ import annotations

import datetime as dt

from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.application.paper_book import (
    _COST_RATE_BY_SLEEVE,
    _DEFAULT_COST_RATE,
    funding_pnl,
    persist_paper_book,
    trading_cost,
)
from fxhnt.domain.paper import Position


def _pos(sleeve, symbol, qty, px, date="2026-01-01"):
    return Position(sleeve=sleeve, symbol=symbol, qty=qty, entry_price=px, entry_date=date)


# --------------------------------------------------------------------------- trading_cost

def test_trading_cost_single_sleeve_rebalance():
    """BTC 1 -> 2 units @ 50000, crypto_tstrend: traded 1 * 50000 * 0.0008 = 40."""
    prev = [_pos("crypto_tstrend", "BTC", 1.0, 50_000.0)]
    target = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    prices = {"BTC": 50_000.0}
    assert trading_cost(prev, target, prices) == 40.0


def test_trading_cost_zero_turnover_is_zero():
    prev = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    target = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    assert trading_cost(prev, target, {"BTC": 50_000.0}) == 0.0


def test_trading_cost_open_from_flat_uses_target_qty():
    """No prior position -> traded = |target_qty| (prev side = 0)."""
    target = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    # 2 * 50000 * 0.0008 = 80
    assert trading_cost([], target, {"BTC": 50_000.0}) == 80.0


def test_trading_cost_full_exit_uses_prev_qty():
    """Position closed to flat -> traded = |prev_qty|, priced at the day's close."""
    prev = [_pos("crypto_tstrend", "BTC", 3.0, 40_000.0)]
    # priced at today's close 50000: 3 * 50000 * 0.0008 = 120
    assert trading_cost(prev, [], {"BTC": 50_000.0}) == 120.0


def test_trading_cost_multi_sleeve_with_unlock_rate():
    """crypto_tstrend (8bp) + unlock (35bp) summed; unlock short opens from flat."""
    prev = [_pos("crypto_tstrend", "BTC", 1.0, 50_000.0)]
    target = [
        _pos("crypto_tstrend", "BTC", 2.0, 50_000.0),     # traded 1*50000*0.0008 = 40
        _pos("unlock", "ALT", -100.0, 10.0),              # traded 100*10*0.0035 = 3.5
    ]
    prices = {"BTC": 50_000.0, "ALT": 10.0}
    assert trading_cost(prev, target, prices) == 40.0 + 3.5


def test_trading_cost_default_rate_for_unknown_sleeve():
    target = [_pos("mystery_sleeve", "FOO", 10.0, 100.0)]
    # 10 * 100 * 0.0015 = 1.5
    assert trading_cost([], target, {"FOO": 100.0}) == 10.0 * 100.0 * _DEFAULT_COST_RATE


def test_trading_cost_missing_price_falls_back_to_entry():
    target = [_pos("crypto_tstrend", "BTC", 1.0, 50_000.0)]
    # no price -> fallback to entry_price 50000: 1*50000*0.0008 = 40
    assert trading_cost([], target, {}) == 40.0


def test_trading_cost_rates_dict_values():
    assert _COST_RATE_BY_SLEEVE["crypto_tstrend"] == 0.0008
    assert _COST_RATE_BY_SLEEVE["stablecoin_rotation"] == 0.0006
    assert _COST_RATE_BY_SLEEVE["unlock"] == 0.0035
    assert _DEFAULT_COST_RATE == 0.0015


# --------------------------------------------------------------------------- funding_pnl

def test_funding_pnl_long_pays_positive_funding():
    """Long (qty>0) with funding +0.0001 -> negative cash flow (holder pays)."""
    pos = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    # notional = 2*50000 = 100000; flow = -100000 * 0.0001 = -10
    assert funding_pnl(pos, {"BTC": 0.0001}, {"BTC": 50_000.0}) == -10.0


def test_funding_pnl_short_receives_positive_funding():
    """Short (qty<0) with funding +0.0001 -> positive cash flow (holder receives)."""
    pos = [_pos("crypto_tstrend", "BTC", -2.0, 50_000.0)]
    # notional = -2*50000 = -100000; flow = -(-100000)*0.0001 = +10
    assert funding_pnl(pos, {"BTC": 0.0001}, {"BTC": 50_000.0}) == 10.0


def test_funding_pnl_unknown_symbol_zero():
    pos = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    assert funding_pnl(pos, {}, {"BTC": 50_000.0}) == 0.0


def test_funding_pnl_missing_price_uses_entry():
    pos = [_pos("crypto_tstrend", "BTC", 2.0, 50_000.0)]
    # notional from entry 50000 -> -2*50000*0.0001 = -10
    assert funding_pnl(pos, {"BTC": 0.0001}, {}) == -10.0


def test_funding_pnl_empty_positions_zero():
    assert funding_pnl([], {"BTC": 0.0001}, {"BTC": 50_000.0}) == 0.0


# --------------------------------------------------------------------------- persist backward-compat

def _seq(repo, lev=1.0, **extra):
    """Two-day book: open BTC long day 1, rebalance day 2."""
    at = dt.datetime(2026, 1, 1, tzinfo=dt.UTC)
    common = dict(capital=100_000.0, sleeve_weights={"crypto_tstrend": 1.0},
                  symbol_weights_by_sleeve={"crypto_tstrend": {"BTC": 1.0}}, at=at, leverage=lev)
    persist_paper_book(repo, run_date="2026-01-01", prices={"BTC": 100.0}, **common, **extra)
    persist_paper_book(repo, run_date="2026-01-02", prices={"BTC": 110.0}, **common, **extra)


def test_persist_gross_when_both_none_is_identical():
    """cost_rates=None AND funding_by_symbol=None -> identical nav to the legacy path."""
    base = PaperRepo("sqlite://"); base.migrate()
    _seq(base)                                          # legacy default
    explicit = PaperRepo("sqlite://"); explicit.migrate()
    _seq(explicit, cost_rates=None, funding_by_symbol=None)   # explicit-off
    assert [(n.run_date, n.equity) for n in base.nav_history()] == \
           [(n.run_date, n.equity) for n in explicit.nav_history()]


def test_persist_with_costs_reduces_equity_by_trading_cost():
    gross = PaperRepo("sqlite://"); gross.migrate()
    _seq(gross)
    net = PaperRepo("sqlite://"); net.migrate()
    _seq(net, cost_rates=_COST_RATE_BY_SLEEVE)

    g = gross.nav_history()
    nt = net.nav_history()
    # day 0: opens BTC from flat. equity sized off 100000, qty=1000 @100 -> notional 100000.
    # tcost = 100000 * 0.0008 = 80. net equity = 100000 - 80 = 99920.
    assert abs(g[0].equity - 100_000.0) < 1e-6
    assert abs(nt[0].equity - (100_000.0 - 80.0)) < 1e-6
    # net equity is strictly below gross on every date.
    for ge, ne in zip(g, nt):
        assert ne.equity < ge.equity


def test_persist_with_funding_only_adjusts_equity():
    """funding_by_symbol set (cost_rates None) -> equity moves by funding, no trading cost."""
    r = PaperRepo("sqlite://"); r.migrate()
    at = dt.datetime(2026, 1, 1, tzinfo=dt.UTC)
    common = dict(capital=100_000.0, sleeve_weights={"crypto_tstrend": 1.0},
                  symbol_weights_by_sleeve={"crypto_tstrend": {"BTC": 1.0}}, at=at)
    persist_paper_book(r, run_date="2026-01-01", prices={"BTC": 100.0}, **common)
    # day 2: prior long BTC qty=1000 @100; funding +0.0001 on BTC at price 110 -> notional 1000*110=110000
    # fund = -110000 * 0.0001 = -11. gain = 1000*(110-100)=+10000. equity = 100000+10000-11 = 109989.
    persist_paper_book(r, run_date="2026-01-02", prices={"BTC": 110.0},
                       funding_by_symbol={"BTC": 0.0001}, **common)
    nav = r.nav_history()[-1]
    assert abs(nav.equity - (100_000.0 + 10_000.0 - 11.0)) < 1e-6
