"""Unit tests for libs/backtest/metrics.py."""
from __future__ import annotations

import datetime as dt

import pytest

from libs.backtest.domain import DailyPortfolioState, ExitReason, FilledTrade


def _make_trade(
    net_pnl: float,
    exit_reason: ExitReason = ExitReason.TARGET,
    r_multiple: float = 1.0,
    holding_days: int = 5,
    exit_date: dt.date = dt.date(2026, 1, 10),
    symbol: str = "AAPL",
) -> FilledTrade:
    entry_price = 100.0
    return FilledTrade(
        trade_id=f"t_{symbol}_{exit_date}_{net_pnl}",
        position_id="p1",
        event_id="EVT::TEST",
        symbol=symbol,
        entry_date=dt.date(2026, 1, 5),
        exit_date=exit_date,
        entry_price=entry_price,
        exit_price=entry_price + (net_pnl / max(1, 10)),
        exit_reason=exit_reason,
        shares=10,
        commission=0.1,
        slippage_bps=10.0,
        gross_pnl=net_pnl + 0.1,
        net_pnl=net_pnl,
        pnl_pct=net_pnl / (entry_price * 10),
        r_multiple=r_multiple,
        holding_days=holding_days,
    )


def _make_equity_state(
    date: dt.date,
    equity: float,
    n_positions: int = 0,
    gross_exposure: float = 0.0,
    net_exposure: float = 0.0,
) -> DailyPortfolioState:
    return DailyPortfolioState(
        date=date,
        equity=equity,
        cash_available=equity,
        gross_exposure=gross_exposure,
        net_exposure=net_exposure,
        reserved_risk_budget=0.0,
        unrealized_pnl=0.0,
        realized_pnl=0.0,
        open_positions=[f"p{i}" for i in range(n_positions)],
        daily_new_risk_used=0.0,
        peak_equity=equity,
        current_drawdown_pct=0.0,
    )


class TestWinRate:
    def test_all_wins(self):
        from libs.backtest.metrics import compute_win_rate

        trades = [_make_trade(100), _make_trade(200)]
        assert compute_win_rate(trades) == 1.0

    def test_mixed(self):
        from libs.backtest.metrics import compute_win_rate

        trades = [_make_trade(100), _make_trade(-50)]
        assert compute_win_rate(trades) == pytest.approx(0.5)

    def test_empty_returns_none(self):
        from libs.backtest.metrics import compute_win_rate

        assert compute_win_rate([]) is None


class TestProfitFactor:
    def test_basic(self):
        from libs.backtest.metrics import compute_profit_factor

        trades = [_make_trade(200), _make_trade(100), _make_trade(-100)]
        pf = compute_profit_factor(trades)
        assert pf == pytest.approx(3.0)

    def test_no_losses_returns_none(self):
        from libs.backtest.metrics import compute_profit_factor

        trades = [_make_trade(100), _make_trade(200)]
        assert compute_profit_factor(trades) is None

    def test_empty_returns_none(self):
        from libs.backtest.metrics import compute_profit_factor

        assert compute_profit_factor([]) is None


class TestExpectancyR:
    def test_positive(self):
        from libs.backtest.metrics import compute_expectancy_r

        trades = [_make_trade(100, r_multiple=2.0), _make_trade(-50, r_multiple=-1.0)]
        er = compute_expectancy_r(trades)
        assert er == pytest.approx(0.5)

    def test_empty_returns_none(self):
        from libs.backtest.metrics import compute_expectancy_r

        assert compute_expectancy_r([]) is None


class TestTotalReturnPct:
    def test_basic(self):
        from libs.backtest.metrics import compute_total_return_pct

        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000),
            _make_equity_state(dt.date(2026, 1, 6), 110_000),
        ]
        assert compute_total_return_pct(curve) == pytest.approx(10.0)

    def test_single_point_returns_none(self):
        from libs.backtest.metrics import compute_total_return_pct

        assert compute_total_return_pct([_make_equity_state(dt.date(2026, 1, 5), 100_000)]) is None


class TestMaxDrawdown:
    def test_basic_drawdown(self):
        from libs.backtest.metrics import compute_max_drawdown_pct

        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000),
            _make_equity_state(dt.date(2026, 1, 6), 120_000),  # peak
            _make_equity_state(dt.date(2026, 1, 7), 90_000),   # drawdown from 120k
            _make_equity_state(dt.date(2026, 1, 8), 100_000),
        ]
        dd = compute_max_drawdown_pct(curve)
        # Max drawdown = (120k - 90k) / 120k = 25%
        assert dd == pytest.approx(25.0)

    def test_no_drawdown(self):
        from libs.backtest.metrics import compute_max_drawdown_pct

        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000),
            _make_equity_state(dt.date(2026, 1, 6), 110_000),
        ]
        assert compute_max_drawdown_pct(curve) == pytest.approx(0.0)


class TestSharpeRatio:
    def test_positive_sharpe(self):
        from libs.backtest.metrics import compute_sharpe_ratio

        # Steady returns → positive Sharpe
        curve = [
            _make_equity_state(dt.date(2026, 1, 2) + dt.timedelta(days=i), 100_000 + i * 100)
            for i in range(50)
        ]
        sharpe = compute_sharpe_ratio(curve)
        assert sharpe is not None
        assert sharpe > 0

    def test_not_enough_data_returns_none(self):
        from libs.backtest.metrics import compute_sharpe_ratio

        curve = [_make_equity_state(dt.date(2026, 1, 5), 100_000)]
        assert compute_sharpe_ratio(curve) is None


class TestExposureMetrics:
    def test_avg_gross_and_net_exposure_pct(self):
        from libs.backtest.metrics import (
            compute_avg_gross_exposure_pct,
            compute_avg_net_exposure_pct,
            compute_days_in_market_pct,
        )

        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000, gross_exposure=0.0, net_exposure=0.0),
            _make_equity_state(dt.date(2026, 1, 6), 100_000, gross_exposure=20_000.0, net_exposure=-20_000.0),
            _make_equity_state(dt.date(2026, 1, 7), 100_000, gross_exposure=10_000.0, net_exposure=5_000.0),
        ]

        assert compute_avg_gross_exposure_pct(curve) == pytest.approx(10.0)
        assert compute_avg_net_exposure_pct(curve) == pytest.approx(-5.0)
        assert compute_days_in_market_pct(curve) == pytest.approx(66.6666666667)


class TestStopExitRate:
    def test_basic(self):
        from libs.backtest.metrics import compute_stop_exit_rate

        trades = [
            _make_trade(100, ExitReason.TARGET),
            _make_trade(-50, ExitReason.STOP),
            _make_trade(-30, ExitReason.STOP),
        ]
        assert compute_stop_exit_rate(trades) == pytest.approx(2 / 3)

    def test_trailing_counts_as_stop(self):
        from libs.backtest.metrics import compute_stop_exit_rate

        trades = [
            _make_trade(50, ExitReason.TRAILING),
        ]
        assert compute_stop_exit_rate(trades) == pytest.approx(1.0)


class TestMonthlyWinRate:
    def test_basic(self):
        from libs.backtest.metrics import compute_monthly_win_rate

        trades = [
            _make_trade(100, exit_date=dt.date(2026, 1, 15)),
            _make_trade(200, exit_date=dt.date(2026, 1, 20)),
            _make_trade(-50, exit_date=dt.date(2026, 2, 10)),
        ]
        # Jan: +300 = win, Feb: -50 = loss → 1/2 = 0.5
        mwr = compute_monthly_win_rate(trades)
        assert mwr == pytest.approx(0.5)


class TestBuildMetricsBundle:
    def test_builds_with_trades_and_curve(self):
        from libs.backtest.metrics import build_metrics_bundle

        trades = [
            _make_trade(100, ExitReason.TARGET, r_multiple=2.0),
            _make_trade(-50, ExitReason.STOP, r_multiple=-1.0),
        ]
        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000),
            _make_equity_state(dt.date(2026, 1, 10), 105_000),
        ]
        m = build_metrics_bundle(trades, curve)
        assert m.trade_count == 2
        assert m.win_rate == pytest.approx(0.5)
        assert m.total_return_pct == pytest.approx(5.0)
        assert m.avg_gross_exposure_pct == pytest.approx(0.0)
        assert m.avg_net_exposure_pct == pytest.approx(0.0)
        assert m.days_in_market_pct == pytest.approx(0.0)

    def test_builds_exposure_metrics(self):
        from libs.backtest.metrics import build_metrics_bundle

        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000, gross_exposure=0.0, net_exposure=0.0),
            _make_equity_state(dt.date(2026, 1, 6), 100_000, gross_exposure=30_000.0, net_exposure=-10_000.0),
            _make_equity_state(dt.date(2026, 1, 7), 100_000, gross_exposure=10_000.0, net_exposure=10_000.0),
        ]

        m = build_metrics_bundle([], curve)
        assert m.avg_gross_exposure_pct == pytest.approx(13.3333333333)
        assert m.avg_net_exposure_pct == pytest.approx(0.0)
        assert m.days_in_market_pct == pytest.approx(66.6666666667)

    def test_empty_trades(self):
        from libs.backtest.metrics import build_metrics_bundle

        m = build_metrics_bundle([], [])
        assert m.trade_count == 0
        assert m.win_rate is None

    def test_bootstrap_cis_included(self):
        from libs.backtest.metrics import build_metrics_bundle

        # Need ≥5 trades for bootstrap
        trades = [
            _make_trade(100, ExitReason.TARGET, r_multiple=2.0, exit_date=dt.date(2026, 1, 10 + i))
            for i in range(3)
        ] + [
            _make_trade(-50, ExitReason.STOP, r_multiple=-1.0, exit_date=dt.date(2026, 1, 20 + i))
            for i in range(3)
        ]
        curve = [
            _make_equity_state(dt.date(2026, 1, 5), 100_000),
            _make_equity_state(dt.date(2026, 1, 30), 105_000),
        ]
        m = build_metrics_bundle(trades, curve)
        assert "win_rate_ci_95" in m.bootstrap_cis
        ci = m.bootstrap_cis["win_rate_ci_95"]
        assert ci is not None
        assert ci[0] <= ci[1]  # lower ≤ upper


class TestBootstrapCI:
    def test_basic_ci(self):
        from libs.backtest.metrics import bootstrap_ci, compute_win_rate

        trades = [_make_trade(100)] * 4 + [_make_trade(-50)] * 4
        ci = bootstrap_ci(trades, compute_win_rate, n_iterations=500, seed=42)
        assert ci is not None
        lo, hi = ci
        assert 0.0 <= lo <= hi <= 1.0

    def test_too_few_trades(self):
        from libs.backtest.metrics import bootstrap_ci, compute_win_rate

        trades = [_make_trade(100)] * 3
        ci = bootstrap_ci(trades, compute_win_rate)
        assert ci is None

    def test_deterministic_with_seed(self):
        from libs.backtest.metrics import bootstrap_ci, compute_win_rate

        trades = [_make_trade(100)] * 5 + [_make_trade(-50)] * 5
        ci1 = bootstrap_ci(trades, compute_win_rate, seed=42)
        ci2 = bootstrap_ci(trades, compute_win_rate, seed=42)
        assert ci1 == ci2
