"""TDD for :mod:`cerbero_bite.core.sizing_engine`.

Spec: ``docs/01-strategy-rules.md §5`` and ``docs/03-algorithms.md §3``.
The five mandatory test cases listed in §3 are reproduced here verbatim.
"""

from __future__ import annotations

from decimal import Decimal

import pytest

from cerbero_bite.config import StrategyConfig, golden_config
from cerbero_bite.core.sizing_engine import SizingContext, compute_contracts

# Standard fixture: EUR/USD = 1.075 (cap 200 EUR ≈ 215 USD, 1000 EUR ≈ 1075 USD)
EUR_USD = Decimal("1.075")


def _ctx(
    *,
    capital_usd: str = "1500",
    max_loss_per_contract_usd: str = "93",
    dvol_now: str = "40",
    open_engagement_usd: str = "0",
    eur_to_usd: Decimal = EUR_USD,
    other_open_positions: int = 0,
) -> SizingContext:
    return SizingContext(
        capital_usd=Decimal(capital_usd),
        max_loss_per_contract_usd=Decimal(max_loss_per_contract_usd),
        dvol_now=Decimal(dvol_now),
        open_engagement_usd=Decimal(open_engagement_usd),
        eur_to_usd=eur_to_usd,
        other_open_positions=other_open_positions,
    )


@pytest.fixture
def cfg() -> StrategyConfig:
    return golden_config()


# ---------------------------------------------------------------------------
# Mandatory examples from docs/03-algorithms.md §3
# ---------------------------------------------------------------------------


def test_minimum_capital_dvol_40_yields_one_contract(cfg: StrategyConfig) -> None:
    res = compute_contracts(_ctx(capital_usd="720", dvol_now="40"), cfg)
    assert res.n_contracts == 1
    assert res.reason_if_zero is None


def test_capital_1500_dvol_50_yields_one_contract(cfg: StrategyConfig) -> None:
    # 13% × 1500 = 195; adj 0.85 → 165.75; floor(165.75/93) = 1
    res = compute_contracts(_ctx(capital_usd="1500", dvol_now="50"), cfg)
    assert res.n_contracts == 1


def test_capital_1500_dvol_40_yields_two_contracts(cfg: StrategyConfig) -> None:
    # 13% × 1500 = 195; cap 215 USD; adj 1.0 → 195; floor(195/93) = 2
    res = compute_contracts(_ctx(capital_usd="1500", dvol_now="40"), cfg)
    assert res.n_contracts == 2


def test_capital_5000_dvol_40_capped_at_two_contracts(cfg: StrategyConfig) -> None:
    # 13% × 5000 = 650; cap 215 USD wins; floor(215/93) = 2
    res = compute_contracts(_ctx(capital_usd="5000", dvol_now="40"), cfg)
    assert res.n_contracts == 2


def test_capital_100000_dvol_40_capped_at_two_contracts(cfg: StrategyConfig) -> None:
    res = compute_contracts(_ctx(capital_usd="100000", dvol_now="40"), cfg)
    assert res.n_contracts == 2


# ---------------------------------------------------------------------------
# DVOL adjustment bands and no-entry threshold
# ---------------------------------------------------------------------------


def test_dvol_in_60_to_80_band_uses_0_65_multiplier(cfg: StrategyConfig) -> None:
    # 13% × 5000 = 650; cap 215; adj 0.65 → 139.75; floor(139.75/93) = 1
    res = compute_contracts(_ctx(capital_usd="5000", dvol_now="65"), cfg)
    assert res.n_contracts == 1


def test_dvol_at_80_returns_zero(cfg: StrategyConfig) -> None:
    res = compute_contracts(_ctx(capital_usd="5000", dvol_now="80"), cfg)
    assert res.n_contracts == 0
    assert res.reason_if_zero is not None
    assert "dvol" in res.reason_if_zero.lower()


def test_dvol_above_no_entry_threshold_returns_zero(cfg: StrategyConfig) -> None:
    res = compute_contracts(_ctx(capital_usd="5000", dvol_now="85"), cfg)
    assert res.n_contracts == 0


# ---------------------------------------------------------------------------
# Aggregate engagement constraint
# ---------------------------------------------------------------------------


def test_aggregate_cap_reduces_contracts(cfg: StrategyConfig) -> None:
    # cap_aggregate ≈ 1075 USD. With 1000 USD already engaged and ML=93,
    # only floor((1075-1000)/93) = 0 new contracts should fit.
    res = compute_contracts(
        _ctx(
            capital_usd="100000",
            dvol_now="40",
            open_engagement_usd="1000",
        ),
        cfg,
    )
    assert res.n_contracts == 0
    assert res.reason_if_zero is not None


def test_aggregate_cap_partially_reduces(cfg: StrategyConfig) -> None:
    # 800 already engaged → free 275; floor(275/93)=2 but also kelly cap may bind.
    # 13% × 100000 = 13000; cap 215 → 215; floor(215/93)=2 contracts.
    # 2*93 = 186; 186+800=986 ≤ 1075 ✓ → 2.
    res = compute_contracts(
        _ctx(capital_usd="100000", dvol_now="40", open_engagement_usd="800"),
        cfg,
    )
    assert res.n_contracts == 2


def test_aggregate_cap_at_975_reduces_to_one(cfg: StrategyConfig) -> None:
    # 975 engaged + 2*93 = 1161 > 1075 → drop to 1: 975 + 93 = 1068 ≤ 1075 ✓
    res = compute_contracts(
        _ctx(capital_usd="100000", dvol_now="40", open_engagement_usd="975"),
        cfg,
    )
    assert res.n_contracts == 1


# ---------------------------------------------------------------------------
# Concurrent positions guard
# ---------------------------------------------------------------------------


def test_concurrent_positions_at_cap_returns_zero(cfg: StrategyConfig) -> None:
    # Default cap = 5 (entry daily). other_open_positions=5 ⇒ cap raggiunto.
    res = compute_contracts(_ctx(capital_usd="1500", other_open_positions=5), cfg)
    assert res.n_contracts == 0
    assert res.reason_if_zero is not None
    assert "position" in res.reason_if_zero.lower()


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


def test_below_one_contract_returns_zero(cfg: StrategyConfig) -> None:
    # capital 600 → 13% * 600 = 78; floor(78/93)=0 → undersize
    res = compute_contracts(_ctx(capital_usd="600", dvol_now="40"), cfg)
    assert res.n_contracts == 0
    assert "undersize" in (res.reason_if_zero or "").lower()


def test_max_contracts_per_trade_cap_binds(cfg: StrategyConfig) -> None:
    # very high capital + tiny ML → would compute > 4 → cap at 4.
    res = compute_contracts(
        _ctx(
            capital_usd="100000",
            max_loss_per_contract_usd="1",
            dvol_now="40",
            eur_to_usd=Decimal("100"),  # cap 20000 USD per trade → no cap
        ),
        cfg,
    )
    assert res.n_contracts == cfg.sizing.max_contracts_per_trade


def test_zero_max_loss_per_contract_returns_zero(cfg: StrategyConfig) -> None:
    res = compute_contracts(_ctx(max_loss_per_contract_usd="0"), cfg)
    assert res.n_contracts == 0
    assert res.reason_if_zero is not None
