"""Validation suite — TDD of the López de Prado discipline machinery ported from Rust ml-validation."""
from __future__ import annotations

import numpy as np

from fxhnt.domain.validation import (
    deflated_sharpe_ratio,
    fdr_correct,
    normal_cdf,
    normal_ppf,
    permutation_test,
    probability_of_backtest_overfitting,
    sharpe_ratio,
)


def test_normal_ppf_known_quantiles() -> None:
    assert abs(normal_ppf(0.975) - 1.959963985) < 1e-6
    assert abs(normal_ppf(0.025) + 1.959963985) < 1e-6
    assert normal_ppf(0.5) == 0.0


def test_normal_cdf_known_and_roundtrip() -> None:
    assert abs(normal_cdf(0.0) - 0.5) < 1e-9
    assert abs(normal_cdf(1.959963985) - 0.975) < 1e-4
    for p in (0.1, 0.3, 0.8, 0.95):
        assert abs(normal_cdf(normal_ppf(p)) - p) < 1e-4         # round-trip


def test_sharpe_ratio() -> None:
    assert sharpe_ratio(np.array([1.0, 1.0, 1.0])) == 0.0        # zero variance
    r = np.array([0.01, -0.005, 0.02, 0.0, 0.015])
    assert abs(sharpe_ratio(r) - r.mean() / r.std(ddof=1)) < 1e-12


def test_dsr_more_trials_raises_the_bar() -> None:
    one = deflated_sharpe_ratio(0.5, num_trials=1, sharpe_variance=0.04, skew=0.0, kurt=0.0, num_observations=100)
    fifty = deflated_sharpe_ratio(0.5, num_trials=50, sharpe_variance=0.04, skew=0.0, kurt=0.0, num_observations=100)
    assert fifty.expected_max_sharpe > one.expected_max_sharpe   # more trials -> higher null bar
    assert fifty.deflated_sharpe < one.deflated_sharpe           # -> lower deflated statistic
    assert fifty.pvalue > one.pvalue                             # -> less significant


def test_dsr_negative_skew_widens_se() -> None:
    base = deflated_sharpe_ratio(1.5, 10, 0.02, 0.0, 0.0, 800)
    skewed = deflated_sharpe_ratio(1.5, 10, 0.02, -1.0, 0.0, 800)   # negative skew penalises a positive SR
    assert skewed.sharpe_std_error > base.sharpe_std_error


def test_pbo_degenerate_and_ties() -> None:
    assert probability_of_backtest_overfitting([1.0, 2.0]).pbo == 0.5          # n < 4
    assert probability_of_backtest_overfitting([1.0, 2.0, 3.0]).pbo == 0.5     # odd n
    assert probability_of_backtest_overfitting([3.0, 3.0, 3.0, 3.0]).pbo == 0.0  # all-equal: IS never beats OOS
    res = probability_of_backtest_overfitting([5.0, 4.0, 3.0, 2.0, 1.0, 0.0])
    assert 0.0 <= res.pbo <= 1.0 and res.num_combinations == 20                # C(6,3)


def test_permutation_test_is_order_invariant_for_sharpe() -> None:
    # FINDING (faithful to Rust, but a latent bug): shuffling returns leaves mean & std — hence Sharpe —
    # unchanged, so this permutation test is degenerate (null_std ~0, pvalue ~1). A meaningful version
    # must permute the POSITION SIGNAL against fixed returns, not the returns themselves.
    r = np.full(300, 0.001) + np.random.default_rng(0).normal(0, 0.0005, 300)
    res = permutation_test(r, num_permutations=500, seed=7)
    assert abs(res.observed_sharpe - sharpe_ratio(r)) < 1e-12
    assert res.null_std < 1e-9 and res.pvalue > 0.5            # degenerate: all perms ~equal Sharpe


def test_fdr_benjamini_hochberg() -> None:
    res = fdr_correct([0.01, 0.02, 0.03, 0.04, 0.05], method="bh")
    assert all(abs(a - 0.05) < 1e-12 for a in res.adjusted_pvalues)            # all collapse to 0.05
    res2 = fdr_correct([0.001, 0.5, 0.5, 0.5, 0.5], method="bh", alpha=0.05)
    assert abs(res2.adjusted_pvalues[0] - 0.005) < 1e-12 and res2.rejected[0]
    assert res2.num_significant == 1                                            # only the strong one


def test_fdr_yekutieli_is_more_conservative() -> None:
    bh = fdr_correct([0.001, 0.5, 0.5, 0.5, 0.5], method="bh")
    by = fdr_correct([0.001, 0.5, 0.5, 0.5, 0.5], method="by")
    assert by.adjusted_pvalues[0] > bh.adjusted_pvalues[0]                      # BY penalises dependence
