from fxhnt.application.paper_book import (
    carry_pnl,
    carry_return_by_symbol,
    sleeve_daily_carry,
)
from fxhnt.domain.paper import Position

def _pos(sym, qty, entry):       # qty*entry = signed unit weight
    return Position("xsfunding", sym, qty, entry, "2026-01-01")

def test_carry_is_weighted_funding_over_gross():
    # long A (w=+1 via qty 0.1 * entry 10), short B (w=-1 via qty -0.2 * entry 5); gross=2
    prev = [_pos("A", 0.1, 10.0), _pos("B", -0.2, 5.0)]
    funding = {"A": 0.02, "B": -0.01}
    # carry = (1*0.02 + (-1)*(-0.01)) / (|1|+|-1|) = (0.02+0.01)/2 = 0.015
    assert abs(sleeve_daily_carry(prev, funding) - 0.015) < 1e-12

def test_short_high_funding_earns_when_weight_negative():
    prev = [_pos("A", -0.1, 10.0)]        # w = -1, short
    assert sleeve_daily_carry(prev, {"A": 0.03}) == -1.0 * 0.03 / 1.0   # = -0.03 (short loses on +funding)

def test_missing_funding_contributes_zero():
    prev = [_pos("A", 0.1, 10.0), _pos("B", 0.1, 10.0)]   # gross = 1 + 1 = 2
    # only A has funding; B -> 0. carry = (1*0.02 + 1*0.0)/2 = 0.01
    assert abs(sleeve_daily_carry(prev, {"A": 0.02}) - 0.01) < 1e-12

def test_no_prior_or_zero_notional_returns_none():
    assert sleeve_daily_carry([], {"A": 0.02}) is None
    assert sleeve_daily_carry([_pos("A", 0.0, 10.0)], {"A": 0.02}) is None


def test_carry_return_includes_basis_not_just_funding():
    # carry_return_by_symbol merges funding + basis into the FULL carry return per symbol.
    funding = {"A": 0.01, "B": -0.01}
    basis = {"A": -0.05, "B": 0.02}        # A's basis moved against the long; B's helps the short side
    cr = carry_return_by_symbol(funding, basis)
    assert abs(cr["A"] - (0.01 + (-0.05))) < 1e-12
    assert abs(cr["B"] - (-0.01 + 0.02)) < 1e-12
    # the sleeve return reflects the basis term, NOT funding-only
    prev = [_pos("A", 0.1, 10.0)]          # w = +1 (long A)
    full = sleeve_daily_carry(prev, cr)
    funding_only = sleeve_daily_carry(prev, funding)
    assert full != funding_only
    assert abs(full - (0.01 - 0.05)) < 1e-12   # 1*( funding+basis ) / gross 1


def test_basis_loss_reduces_carry_vs_flat_basis():
    # A long book; a basis that moves against it reduces the carry return vs a flat (funding-only) basis.
    prev = [_pos("A", 0.1, 10.0)]          # w = +1
    funding = {"A": 0.02}
    flat = sleeve_daily_carry(prev, carry_return_by_symbol(funding, {}))            # basis 0
    loss = sleeve_daily_carry(prev, carry_return_by_symbol(funding, {"A": -0.08}))  # basis -8%
    assert loss < flat
    assert abs((flat - loss) - 0.08) < 1e-12


def test_carry_pnl_over_gross_equals_sleeve_return():
    # The consistency invariant must hold with the basis term: carry_pnl / Σ|qty·entry| == sleeve_daily_carry.
    prev = [_pos("A", 0.1, 10.0), _pos("B", -0.2, 5.0)]   # gross = |1| + |-1| = 2
    cr = carry_return_by_symbol({"A": 0.02, "B": -0.01}, {"A": -0.03, "B": 0.04})
    gross = sum(abs(p.qty * p.entry_price) for p in prev)
    assert abs(carry_pnl(prev, cr) / gross - sleeve_daily_carry(prev, cr)) < 1e-12
