"""
Regression tests for tables.py functions, cross-validated against R PerformanceAnalytics.

R benchmark values were generated by scripts/gen_tables_benchmarks.R using the managers
dataset (1996–2006).

Coverage:
  - table_capm          (R: table.CAPM)
  - table_downside_risk (R: table.DownsideRisk)
  - table_capture_ratios(R: table.CaptureRatios)
  - table_up_down_ratios(R: table.UpDownRatios)
  - table_calendar_returns (R: table.CalendarReturns)
  - table_higher_moments   (R: table.HigherMoments)
  - table_prob_skewness_kurtosis (R: skewness/kurtosis with multiple methods)
  - table_variability   (R component functions: StdDev.annualized, MeanAbsoluteDeviation)
  - table_drawdowns     (R: table.Drawdowns)
  - table_annualized_returns (R: already covered in test_tables.py, expanded here)
"""

import json
import math
import os

import numpy as np
import pandas as pd
import pytest

from pyperfanalytics.tables import (
    table_annualized_returns,
    table_calendar_returns,
    table_capm,
    table_capture_ratios,
    table_downside_risk,
    table_drawdowns,
    table_higher_moments,
    table_prob_skewness_kurtosis,
    table_up_down_ratios,
    table_variability,
)

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def tables_bench():
    path = "data/tables_benchmarks.json"
    if not os.path.exists(path):
        pytest.skip("Tables benchmarks not found. Run scripts/gen_tables_benchmarks.R first.")
    with open(path) as f:
        return json.load(f)


@pytest.fixture(scope="module")
def managers():
    """Load managers 1996–2006, matching the R export."""
    path = "data/managers.csv"
    df = pd.read_csv(path, index_col=0)
    df.index = pd.to_datetime(df.index)
    return df["1996":"2006"]


@pytest.fixture(scope="module")
def Ra(managers):
    return managers.iloc[:, 0:3]  # HAM1, HAM2, HAM3


@pytest.fixture(scope="module")
def Ra6(managers):
    return managers.iloc[:, 0:6]  # HAM1..HAM6


@pytest.fixture(scope="module")
def Rb(managers):
    return managers.iloc[:, 7]  # SP500 TR


TOL = 1e-3


def _approx_or_nan(py_val, r_val, tol=TOL):
    """Assert equal, or both NaN."""
    if r_val is None or (isinstance(r_val, float) and math.isnan(r_val)):
        assert py_val is None or (isinstance(py_val, float) and math.isnan(py_val)) or np.isnan(py_val)
    else:
        assert py_val == pytest.approx(r_val, abs=tol)


# ---------------------------------------------------------------------------
# 1. table_capm
# ---------------------------------------------------------------------------


class TestTableCAPM:
    """Verify table_capm against R's table.CAPM output.

    Column positional order: HAM1(col0), HAM2(col1), HAM3(col2).
    R column keys: 'HAM1 to SP500.TR' (dots); Python keys: 'HAM1 to SP500 TR' (space).
    We use positional access (iloc) to avoid name-mangling issues.
    """

    R_COLS = ["HAM1 to SP500.TR", "HAM2 to SP500.TR", "HAM3 to SP500.TR"]  # R keys

    def _r_val(self, bench, col_key, row_name):
        """Get a value from the R benchmark dict by column key and row name."""
        row_idx = bench["table_capm_rownames"].index(row_name)
        return bench["table_capm"][col_key][row_idx]

    def test_alpha_ham1(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Alpha")
        assert result.iloc[result.index.get_loc("Alpha"), 0] == pytest.approx(r_val, abs=TOL)

    def test_beta_ham1(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Beta")
        assert result.iloc[result.index.get_loc("Beta"), 0] == pytest.approx(r_val, abs=TOL)

    def test_correlation_ham1(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Correlation")
        assert result.iloc[result.index.get_loc("Correlation"), 0] == pytest.approx(r_val, abs=TOL)

    def test_tracking_error_ham3(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[2], "Tracking Error")
        assert result.iloc[result.index.get_loc("Tracking Error"), 2] == pytest.approx(r_val, abs=TOL)

    def test_information_ratio_ham2(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[1], "Information Ratio")
        py_val = result.iloc[result.index.get_loc("Information Ratio"), 1]
        _approx_or_nan(py_val, r_val)

    def test_active_premium_all_cols(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        row_idx = result.index.get_loc("Active Premium")
        for i, r_col in enumerate(self.R_COLS):
            r_val = self._r_val(tables_bench, r_col, "Active Premium")
            _approx_or_nan(result.iloc[row_idx, i], r_val)

    def test_treynor_ratio_ham1(self, Ra, Rb, tables_bench):
        result = table_capm(Ra, Rb, scale=12, Rf=0)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Treynor Ratio")
        py_val = result.iloc[result.index.get_loc("Treynor Ratio"), 0]
        _approx_or_nan(py_val, r_val)


# ---------------------------------------------------------------------------
# 2. table_downside_risk
# ---------------------------------------------------------------------------


class TestTableDownsideRisk:
    """Verify table_downside_risk against R's table.DownsideRisk output."""

    def test_semi_deviation_ham1(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Semi Deviation")
        r_val = tables_bench["table_downside_risk"]["HAM1"][r_row_idx]
        assert result.loc["Semi Deviation", "HAM1"] == pytest.approx(r_val, abs=TOL)

    def test_max_drawdown_ham1(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Maximum Drawdown")
        r_val = tables_bench["table_downside_risk"]["HAM1"][r_row_idx]
        assert result.loc["Maximum Drawdown", "HAM1"] == pytest.approx(r_val, abs=TOL)

    def test_historical_var_ham3(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0, p=0.95)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Historical VaR (95%)")
        r_val = tables_bench["table_downside_risk"]["HAM3"][r_row_idx]
        assert result.loc["Historical VaR (95%)", "HAM3"] == pytest.approx(r_val, abs=TOL)

    def test_historical_es_ham2(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0, p=0.95)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Historical ES (95%)")
        r_val = tables_bench["table_downside_risk"]["HAM2"][r_row_idx]
        assert result.loc["Historical ES (95%)", "HAM2"] == pytest.approx(r_val, abs=TOL)

    def test_modified_var_ham1(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0, p=0.95)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Modified VaR (95%)")
        r_val = tables_bench["table_downside_risk"]["HAM1"][r_row_idx]
        assert result.loc["Modified VaR (95%)", "HAM1"] == pytest.approx(r_val, abs=TOL)

    def test_downside_deviation_zero_ham1(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0)
        r_row_idx = tables_bench["table_downside_risk_rownames"].index("Downside Deviation (0%)")
        r_val = tables_bench["table_downside_risk"]["HAM1"][r_row_idx]
        assert result.loc["Downside Deviation (0%)", "HAM1"] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, Ra, tables_bench):
        result = table_downside_risk(Ra, scale=12, Rf=0)
        assert result.shape[0] == 11  # 11 rows matching R
        assert list(result.columns) == ["HAM1", "HAM2", "HAM3"]


# ---------------------------------------------------------------------------
# 3. table_capture_ratios
# ---------------------------------------------------------------------------


class TestTableCaptureRatios:
    """Verify table_capture_ratios against R's table.CaptureRatios."""

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_up_capture(self, Ra, Rb, asset, tables_bench):
        result = table_capture_ratios(Ra, Rb)
        r_row_idx = tables_bench["table_capture_ratios_rownames"].index(asset)
        tables_bench["table_capture_ratios_colnames"].index("Up Capture")
        r_val = tables_bench["table_capture_ratios"]["Up Capture"][r_row_idx]
        assert result.loc[asset, "Up Capture"] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_down_capture(self, Ra, Rb, asset, tables_bench):
        result = table_capture_ratios(Ra, Rb)
        r_row_idx = tables_bench["table_capture_ratios_rownames"].index(asset)
        r_val = tables_bench["table_capture_ratios"]["Down Capture"][r_row_idx]
        assert result.loc[asset, "Down Capture"] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, Ra, Rb):
        result = table_capture_ratios(Ra, Rb)
        assert result.shape == (3, 2)
        assert list(result.columns) == ["Up Capture", "Down Capture"]


# ---------------------------------------------------------------------------
# 4. table_up_down_ratios
# ---------------------------------------------------------------------------


class TestTableUpDownRatios:
    """Verify table_up_down_ratios against R's table.UpDownRatios."""

    def test_up_capture_ham1(self, Ra, Rb, tables_bench):
        result = table_up_down_ratios(Ra, Rb)
        r_val = tables_bench["table_up_down_ratios"]["Up Capture"][0]
        # Python row name: "HAM1 to SP500 TR"
        row_name = result.index[0]
        assert result.loc[row_name, "Up Capture"] == pytest.approx(r_val, abs=TOL)

    def test_up_number_ham1(self, Ra, Rb, tables_bench):
        result = table_up_down_ratios(Ra, Rb)
        r_val = tables_bench["table_up_down_ratios"]["Up Number"][0]
        row_name = result.index[0]
        assert result.loc[row_name, "Up Number"] == pytest.approx(r_val, abs=TOL)

    def test_down_percent_ham3(self, Ra, Rb, tables_bench):
        result = table_up_down_ratios(Ra, Rb)
        r_val = tables_bench["table_up_down_ratios"]["Down Percent"][2]
        row_name = result.index[2]
        assert result.loc[row_name, "Down Percent"] == pytest.approx(r_val, abs=TOL)

    def test_output_columns(self, Ra, Rb):
        result = table_up_down_ratios(Ra, Rb)
        assert set(result.columns) >= {"Up Capture", "Down Capture", "Up Number", "Down Number"}


# ---------------------------------------------------------------------------
# 5. table_calendar_returns
# ---------------------------------------------------------------------------


class TestTableCalendarReturns:
    """Verify table_calendar_returns against R's table.CalendarReturns."""

    def test_year_1996_jan(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_calendar_returns(ham1)
        r_val = tables_bench["table_calendar_returns"][0]["Jan"]  # 1996 row
        if r_val is None:
            assert pd.isna(result.loc[1996, "Jan"])
        else:
            assert result.loc[1996, "Jan"] == pytest.approx(r_val, abs=TOL)

    def test_year_2000_annual(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_calendar_returns(ham1)
        # 2000 is row index 4 (1996, 97, 98, 99, 2000)
        r_row = tables_bench["table_calendar_returns"][4]
        # Annual column is named "HAM1" in R
        r_val = r_row.get("HAM1", r_row.get(list(r_row.keys())[-1]))
        if r_val is None:
            assert pd.isna(result.loc[2000, result.columns[-1]])
        else:
            assert result.loc[2000, result.columns[-1]] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_calendar_returns(ham1)
        r_years = len(tables_bench["table_calendar_returns_rownames"])
        assert result.shape[0] == r_years
        assert "Jan" in result.columns
        assert "Dec" in result.columns

    def test_annual_column_present(self, managers):
        ham1 = managers.iloc[:, 0]
        result = table_calendar_returns(ham1)
        # Last column should be the annual total
        assert result.shape[1] == 13  # 12 months + 1 annual


# ---------------------------------------------------------------------------
# 6. table_higher_moments
# ---------------------------------------------------------------------------


class TestTableHigherMoments:
    """Verify table_higher_moments against R's table.HigherMoments.

    Uses positional access (iloc) to avoid R dot-notation vs Python space-notation mismatch.
    """

    R_COLS = ["HAM1 to SP500.TR", "HAM2 to SP500.TR", "HAM3 to SP500.TR"]  # R keys

    def _r_val(self, bench, col_key, row_name):
        row_idx = bench["table_higher_moments_rownames"].index(row_name)
        return bench["table_higher_moments"][col_key][row_idx]

    def test_co_skewness_ham1(self, Ra, Rb, tables_bench):
        result = table_higher_moments(Ra, Rb)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "CoSkewness")
        assert result.iloc[result.index.get_loc("CoSkewness"), 0] == pytest.approx(r_val, abs=1e-5)

    def test_co_kurtosis_ham2(self, Ra, Rb, tables_bench):
        result = table_higher_moments(Ra, Rb)
        r_val = self._r_val(tables_bench, self.R_COLS[1], "CoKurtosis")
        assert result.iloc[result.index.get_loc("CoKurtosis"), 1] == pytest.approx(r_val, abs=1e-5)

    def test_beta_co_variance_ham3(self, Ra, Rb, tables_bench):
        result = table_higher_moments(Ra, Rb)
        r_val = self._r_val(tables_bench, self.R_COLS[2], "Beta CoVariance")
        assert result.iloc[result.index.get_loc("Beta CoVariance"), 2] == pytest.approx(r_val, abs=TOL)

    def test_beta_co_skewness_ham1(self, Ra, Rb, tables_bench):
        result = table_higher_moments(Ra, Rb)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Beta CoSkewness")
        assert result.iloc[result.index.get_loc("Beta CoSkewness"), 0] == pytest.approx(r_val, abs=TOL)

    def test_beta_co_kurtosis_ham1(self, Ra, Rb, tables_bench):
        result = table_higher_moments(Ra, Rb)
        r_val = self._r_val(tables_bench, self.R_COLS[0], "Beta CoKurtosis")
        assert result.iloc[result.index.get_loc("Beta CoKurtosis"), 0] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, Ra, Rb):
        result = table_higher_moments(Ra, Rb)
        assert result.shape == (5, 3)  # 5 metrics × 3 assets


# ---------------------------------------------------------------------------
# 7. table_prob_skewness_kurtosis
# ---------------------------------------------------------------------------


class TestTableProbSkewnessKurtosis:
    """Verify table_prob_skewness_kurtosis against manual R benchmarks."""

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_skewness_moment(self, Ra, asset, tables_bench):
        result = table_prob_skewness_kurtosis(Ra)
        r_val = tables_bench["table_prob_summary"][asset]["skewness_moment"]
        assert result.loc["Skewness (moment)", asset] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_kurtosis_excess(self, Ra, asset, tables_bench):
        result = table_prob_skewness_kurtosis(Ra)
        r_val = tables_bench["table_prob_summary"][asset]["kurtosis_excess"]
        assert result.loc["Kurtosis (excess)", asset] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_skewness_fisher(self, Ra, asset, tables_bench):
        result = table_prob_skewness_kurtosis(Ra)
        r_val = tables_bench["table_prob_summary"][asset]["skewness_fisher"]
        assert result.loc["Skewness (fisher)", asset] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_kurtosis_sample_excess(self, Ra, asset, tables_bench):
        result = table_prob_skewness_kurtosis(Ra)
        r_val = tables_bench["table_prob_summary"][asset]["kurtosis_sample_excess"]
        assert result.loc["Kurtosis (sample_excess)", asset] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, Ra):
        result = table_prob_skewness_kurtosis(Ra)
        assert result.shape == (8, 3)  # 8 methods × 3 assets


# ---------------------------------------------------------------------------
# 8. table_variability
# ---------------------------------------------------------------------------


class TestTableVariability:
    """Verify table_variability against R component function benchmarks."""

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_mean_absolute_deviation(self, Ra, asset, tables_bench):
        result = table_variability(Ra, scale=12)
        r_val = tables_bench["table_variability_manual"][asset]["MeanAbsoluteDeviation"]
        assert result.loc["Mean Absolute deviation", asset] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_annualized_std_dev(self, Ra, asset, tables_bench):
        result = table_variability(Ra, scale=12)
        r_val = tables_bench["table_variability_manual"][asset]["AnnualizedStdDev"]
        assert result.loc["Annualized Std Dev", asset] == pytest.approx(r_val, abs=TOL)

    @pytest.mark.parametrize("asset", ["HAM1", "HAM2", "HAM3"])
    def test_period_std_dev(self, Ra, asset, tables_bench):
        result = table_variability(Ra, scale=12)
        r_val = tables_bench["table_variability_manual"][asset]["PeriodStdDev"]
        assert result.loc["Period Std Dev", asset] == pytest.approx(r_val, abs=TOL)

    def test_output_shape(self, Ra):
        result = table_variability(Ra, scale=12)
        assert result.shape == (3, 3)  # 3 metrics × 3 assets


# ---------------------------------------------------------------------------
# 9. table_drawdowns
# ---------------------------------------------------------------------------


class TestTableDrawdowns:
    """Verify table_drawdowns against R's table.Drawdowns output."""

    def test_worst_drawdown_depth(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        # Worst drawdown is the first row
        r_depth = tables_bench["table_drawdowns"][0]["Depth"]
        assert result["Depth"].iloc[0] == pytest.approx(r_depth, abs=TOL)

    def test_worst_drawdown_length(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        r_length = tables_bench["table_drawdowns"][0]["Length"]
        assert result["Length"].iloc[0] == r_length

    def test_worst_drawdown_to_trough(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        r_val = tables_bench["table_drawdowns"][0]["To Trough"]
        assert result["To Trough"].iloc[0] == r_val

    def test_second_drawdown_depth(self, managers, tables_bench):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        r_depth = tables_bench["table_drawdowns"][1]["Depth"]
        _approx_or_nan(result["Depth"].iloc[1], r_depth)

    def test_output_top_n(self, managers):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=3)
        assert len(result) == 3

    def test_output_columns(self, managers):
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        expected_cols = {"From", "Trough", "To", "Depth", "Length", "To Trough", "Recovery"}
        assert set(result.columns) == expected_cols

    def test_depths_sorted_ascending(self, managers):
        """Worst drawdowns should be sorted by depth (most negative first)."""
        ham1 = managers.iloc[:, 0]
        result = table_drawdowns(ham1, top=5)
        depths = result["Depth"].dropna().values
        assert all(depths[i] <= depths[i + 1] for i in range(len(depths) - 1))


# ---------------------------------------------------------------------------
# 10. table_annualized_returns (expanded from existing test_tables.py)
# ---------------------------------------------------------------------------


class TestTableAnnualizedReturns:
    """Extended tests for table_annualized_returns."""

    def test_ham1_annualized_return(self, managers):
        ra = managers.iloc[:, 0:6]
        result = table_annualized_returns(ra)
        assert result.loc["Annualized Return", "HAM1"] == pytest.approx(0.137532, abs=1e-4)

    def test_ham1_std_dev(self, managers):
        ra = managers.iloc[:, 0:6]
        result = table_annualized_returns(ra)
        assert result.loc["Annualized Std Dev", "HAM1"] == pytest.approx(0.0887808, abs=1e-4)

    def test_sharpe_with_rf_zero(self, managers):
        ra = managers.iloc[:, 0:6]
        result = table_annualized_returns(ra, Rf=0)
        # Sharpe = Annualized Return / Annualized Std Dev (when Rf=0)
        ann_ret = result.loc["Annualized Return", "HAM1"]
        ann_std = result.loc["Annualized Std Dev", "HAM1"]
        sharpe_row = [row for row in result.index if "Sharpe" in row][0]
        expected_sharpe = ann_ret / ann_std
        assert result.loc[sharpe_row, "HAM1"] == pytest.approx(expected_sharpe, abs=TOL)

    def test_output_shape(self, managers):
        ra = managers.iloc[:, 0:6]
        result = table_annualized_returns(ra)
        assert result.shape[0] == 3  # 3 rows: Return, StdDev, Sharpe
        assert result.shape[1] == 6  # 6 assets

    def test_sharpe_label_has_rf(self, managers):
        ra = managers.iloc[:, 0:6]
        result = table_annualized_returns(ra, Rf=0.035 / 12)
        # Sharpe row label should contain "Rf="
        sharpe_rows = [row for row in result.index if "Sharpe" in row]
        assert len(sharpe_rows) == 1
        assert "Rf=" in sharpe_rows[0]
