"""Tests for eapctf.ctf.metrics — performance metric computations."""

from __future__ import annotations

import math

import numpy as np
import pandas as pd
import pytest

from eapctf.ctf.metrics import (
    MetricsResult,
    _annual_return,
    _max_drawdown,
    _monthly_portfolio_returns,
    _sharpe,
    _volatility,
    compute_metrics,
)


class TestSharpe:
    def test_positive_sharpe(self) -> None:
        monthly = pd.Series([0.01] * 24)
        result = _sharpe(monthly)
        # mean=0.01, std=0 → nan (constant series)
        # Use non-constant series
        monthly = pd.Series([0.02, 0.01] * 12)
        result = _sharpe(monthly)
        assert result > 0

    def test_zero_std_returns_nan(self) -> None:
        # Use 1.0 (exactly representable in float64) so std() is exactly 0
        monthly = pd.Series([1.0] * 12)
        assert math.isnan(_sharpe(monthly))

    def test_single_observation_returns_nan(self) -> None:
        assert math.isnan(_sharpe(pd.Series([0.05])))

    def test_empty_returns_nan(self) -> None:
        assert math.isnan(_sharpe(pd.Series([], dtype=float)))

    def test_annualization_factor(self) -> None:
        # Known: mean=1%, std=1% → Sharpe = sqrt(12).
        # Use n=10000 and tolerance=0.3 — the sample estimate has std ~0.05 per sqrt(n).
        np.random.seed(99)
        n = 10_000
        monthly = pd.Series(np.random.normal(0.01, 0.01, n))
        result = _sharpe(monthly)
        expected = math.sqrt(12)
        assert abs(result - expected) < 0.3  # within 0.3 of sqrt(12)


class TestAnnualReturn:
    def test_zero_return(self) -> None:
        monthly = pd.Series([0.0] * 12)
        assert abs(_annual_return(monthly)) < 1e-10

    def test_positive_return(self) -> None:
        monthly = pd.Series([0.01] * 12)
        result = _annual_return(monthly)
        expected = (1.01 ** 12) - 1
        assert abs(result - expected) < 1e-8

    def test_empty_returns_nan(self) -> None:
        assert math.isnan(_annual_return(pd.Series([], dtype=float)))

    def test_negative_gross_return_returns_nan(self) -> None:
        monthly = pd.Series([-2.0, 0.01])  # 1 + (-2.0) = -1.0 < 0
        assert math.isnan(_annual_return(monthly))

    def test_log_sum_numerically_stable(self) -> None:
        # 100 months of small returns — np.prod would underflow to 0
        monthly = pd.Series([0.001] * 100)
        result = _annual_return(monthly)
        assert not math.isnan(result)
        assert result > 0


class TestVolatility:
    def test_zero_variance(self) -> None:
        # Use 1.0 (exactly representable in float64) so std() is exactly 0
        monthly = pd.Series([1.0] * 12)
        assert _volatility(monthly) == 0.0

    def test_annualization(self) -> None:
        np.random.seed(7)
        monthly = pd.Series(np.random.normal(0, 0.02, 1000))
        result = _volatility(monthly)
        expected = 0.02 * math.sqrt(12)
        assert abs(result - expected) < 0.005

    def test_single_observation_returns_nan(self) -> None:
        assert math.isnan(_volatility(pd.Series([0.01])))


class TestMaxDrawdown:
    def test_no_drawdown(self) -> None:
        monthly = pd.Series([0.01] * 12)
        assert _max_drawdown(monthly) == pytest.approx(0.0, abs=1e-10)

    def test_known_drawdown(self) -> None:
        # Peak at month 2, then a 50% drawdown
        monthly = pd.Series([0.5, -0.5, 0.0])
        # cumulative: 1.5, 0.75, 0.75
        # drawdown at month 2: (0.75 - 1.5) / 1.5 = -0.5
        result = _max_drawdown(monthly)
        assert result == pytest.approx(-0.5, abs=1e-6)

    def test_all_negative(self) -> None:
        monthly = pd.Series([-0.1, -0.1, -0.1])
        result = _max_drawdown(monthly)
        assert result < 0

    def test_empty_returns_nan(self) -> None:
        assert math.isnan(_max_drawdown(pd.Series([], dtype=float)))


class TestMonthlyPortfolioReturns:
    def test_basic_aggregation(self) -> None:
        weights = pd.DataFrame({
            "id": [1, 2],
            "eom": ["2020-01-31", "2020-01-31"],
            "w": [0.5, 0.5],
        })
        daily = pd.DataFrame({
            "id": [1, 1, 2, 2],
            "date": pd.to_datetime(["2020-02-03", "2020-02-04", "2020-02-03", "2020-02-04"]),
            "ret_exc": [0.01, 0.01, 0.02, 0.02],
        })
        result = _monthly_portfolio_returns(weights, daily)
        # portfolio return = 0.5 * (0.01+0.01) + 0.5 * (0.02+0.02) = 0.01 + 0.02 = 0.03
        assert len(result) == 1
        assert result.iloc[0] == pytest.approx(0.03, abs=1e-10)

    def test_empty_overlap_returns_empty(self) -> None:
        weights = pd.DataFrame({
            "id": [1],
            "eom": ["2020-01-31"],
            "w": [1.0],
        })
        daily = pd.DataFrame({
            "id": [1],
            "date": pd.to_datetime(["2019-12-31"]),
            "ret_exc": [0.01],
        })
        result = _monthly_portfolio_returns(weights, daily)
        assert len(result) == 0

    def test_multiple_months(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        result = _monthly_portfolio_returns(ctf_weights_df, ctf_daily_ret_df)
        assert len(result) > 0
        assert result.index.is_monotonic_increasing


class TestComputeMetrics:
    def test_returns_metrics_result(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        result = compute_metrics(ctf_weights_df, ctf_daily_ret_df)
        assert isinstance(result, MetricsResult)
        assert result.n_months > 0
        assert not math.isnan(result.sharpe)
        assert not math.isnan(result.annual_return)
        assert not math.isnan(result.volatility)
        assert result.max_drawdown <= 0.0

    def test_benchmark_computed_by_default(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        result = compute_metrics(ctf_weights_df, ctf_daily_ret_df, compute_benchmark=True)
        assert result.benchmark_sharpe is not None
        assert result.benchmark_annual_return is not None

    def test_benchmark_skipped_when_disabled(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        result = compute_metrics(ctf_weights_df, ctf_daily_ret_df, compute_benchmark=False)
        assert result.benchmark_sharpe is None
        assert result.benchmark_annual_return is None

    def test_missing_columns_raises(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        bad_weights = ctf_weights_df.drop(columns=["w"])
        with pytest.raises(ValueError, match="missing column"):
            compute_metrics(bad_weights, ctf_daily_ret_df)

    def test_str_output(
        self, ctf_weights_df: pd.DataFrame, ctf_daily_ret_df: pd.DataFrame
    ) -> None:
        result = compute_metrics(ctf_weights_df, ctf_daily_ret_df)
        text = str(result)
        assert "Sharpe" in text
        assert "Annual return" in text
