"""Correctness tests for the backtest engine, focused on timing / no look-ahead."""
import numpy as np
import pandas as pd

from src.config import BacktestParams
from src.backtest import performance_stats, run_backtest


def _build_controlled_prices():
    """Six tickers with known January MAX ordering and known February returns.

    January: every day is flat (0) except one spike per ticker, so MAX(1) equals
    that spike.  Spikes ascend T0 < T1 < ... < T5, fixing the quintile sort.
    February: every day flat except one known move, fixing the held return.
    """
    jan = pd.bdate_range("2020-01-01", "2020-01-31")
    feb = pd.bdate_range("2020-02-01", "2020-02-29")
    dates = jan.append(feb)
    tickers = [f"T{i}" for i in range(6)]

    jan_spike = {f"T{i}": 0.01 * (i + 1) for i in range(6)}      # ascending MAX
    feb_move = {"T0": 0.05, "T1": 0.04, "T2": 0.03,             # low-MAX winners
                "T3": -0.01, "T4": -0.02, "T5": -0.03}          # high-MAX losers

    rets = pd.DataFrame(0.0, index=dates, columns=tickers)
    for t in tickers:
        rets.loc[jan[5], t] = jan_spike[t]   # the only non-zero Jan day
        rets.loc[feb[5], t] = feb_move[t]    # the only non-zero Feb day

    prices = 100.0 * (1.0 + rets).cumprod()
    return prices, feb_move


def test_quintile_returns_use_next_month_only():
    prices, feb_move = _build_controlled_prices()
    params = BacktestParams(max_n=1, n_quintiles=2, cost_bps=0.0, min_days_in_month=15)
    res = run_backtest(prices, params)

    # First (and only) holding month is February 2020.
    assert res.quintile_returns.index[0] == pd.Timestamp("2020-02-29")

    feb = pd.Timestamp("2020-02-29")
    q1 = res.quintile_returns.loc[feb, "Q1"]   # low-MAX: T0,T1,T2
    q2 = res.quintile_returns.loc[feb, "Q2"]   # high-MAX: T3,T4,T5
    assert np.isclose(q1, np.mean([feb_move[t] for t in ("T0", "T1", "T2")]))
    assert np.isclose(q2, np.mean([feb_move[t] for t in ("T3", "T4", "T5")]))

    # Long-short = low-MAX minus high-MAX, and it should be positive here.
    ls = res.long_short_gross.loc[feb]
    assert np.isclose(ls, q1 - q2)
    assert ls > 0


def test_costs_reduce_returns_and_turnover_nonneg():
    prices, _ = _build_controlled_prices()
    free = run_backtest(prices, BacktestParams(max_n=1, n_quintiles=2, cost_bps=0.0))
    costly = run_backtest(prices, BacktestParams(max_n=1, n_quintiles=2, cost_bps=50.0))

    # With costs, net return is never above the gross/free-cost return.
    aligned = (costly.long_short_net - free.long_short_net).dropna()
    assert (aligned <= 1e-12).all()
    assert (free.turnover >= -1e-12).all()


def test_no_lookahead_signal_precedes_holding():
    prices, _ = _build_controlled_prices()
    res = run_backtest(prices, BacktestParams(max_n=1, n_quintiles=2, cost_bps=0.0))
    # Every holding timestamp must be strictly after the formation month it used:
    # the earliest possible holding is February (Jan signal), never January itself.
    assert res.quintile_returns.index.min() >= pd.Timestamp("2020-02-01")


def test_performance_stats_basic_identities():
    r = pd.Series([0.01, -0.02, 0.03, 0.00, 0.015],
                  index=pd.period_range("2020-01", periods=5, freq="M").to_timestamp("M"))
    stats = performance_stats(r)
    assert np.isclose(stats["ann_return"], r.mean() * 12)
    assert stats["n_months"] == 5
    assert 0.0 <= stats["hit_rate"] <= 1.0
    assert stats["max_drawdown"] <= 0.0
