"""Tests for horizon.robustness - statistical robustness testing."""

from __future__ import annotations

from types import SimpleNamespace

import pytest

from horizon.analytics import _analyze_trades
from horizon.result import BacktestResult
from horizon.robustness import (
    OutcomeRandomizationResult,
    PathSimulationResult,
    PermutationTestResult,
    RobustnessReport,
    outcome_randomization,
    path_simulation,
    permutation_test,
    robustness_test,
)


# ---------------------------------------------------------------------------
# Test fixtures
# ---------------------------------------------------------------------------

def _make_fill(market_id: str, side: str, price: float, size: float,
               fee: float = 0.0, timestamp: float = 0.0):
    """Create a mock fill object."""
    return SimpleNamespace(
        market_id=market_id,
        order_side=side,
        price=price,
        size=size,
        fee=fee,
        timestamp=timestamp,
        fill_id=f"{market_id}-{side}-{timestamp}",
    )


def _build_result(
    trades: list,
    outcomes: dict[str, float] | None = None,
    initial_capital: float = 1000.0,
) -> BacktestResult:
    """Build a BacktestResult with given trades and outcomes."""
    # Build a simple equity curve from trades
    equity = initial_capital
    equity_curve = [(0.0, initial_capital)]
    for i, fill in enumerate(trades):
        ts = getattr(fill, "timestamp", float(i))
        equity_curve.append((ts + 0.001, equity))

    return BacktestResult(
        equity_curve=equity_curve,
        trades=trades,
        positions=[],
        initial_capital=initial_capital,
        outcomes=outcomes,
    )


def _multi_market_result() -> BacktestResult:
    """Result with multiple markets and clear edge."""
    trades = [
        # Market A: buy at 0.3, outcome=1.0 -> edge on open position
        _make_fill("mkt_a", "Buy", 0.30, 10.0, 0.0, 100.0),
        # Market B: buy at 0.7, outcome=0.0 -> loss on open position
        _make_fill("mkt_b", "Buy", 0.70, 10.0, 0.0, 200.0),
        # Market C: buy at 0.4, sell at 0.6 -> closed round-trip (PnL = 2.0)
        _make_fill("mkt_c", "Buy", 0.40, 10.0, 0.0, 300.0),
        _make_fill("mkt_c", "Sell", 0.60, 10.0, 0.0, 400.0),
        # Market D: buy at 0.5, outcome=1.0 -> edge on open position
        _make_fill("mkt_d", "Buy", 0.50, 10.0, 0.0, 500.0),
    ]
    outcomes = {
        "mkt_a": 1.0,
        "mkt_b": 0.0,
        "mkt_c": 1.0,
        "mkt_d": 1.0,
    }
    return _build_result(trades, outcomes)


def _round_trip_result() -> BacktestResult:
    """Result with multiple closed round-trips for path simulation."""
    trades = []
    # 10 profitable trades and 4 losing trades (net positive)
    for i in range(10):
        trades.append(_make_fill("mkt", "Buy", 0.40, 5.0, 0.0, float(i * 2)))
        trades.append(_make_fill("mkt", "Sell", 0.55, 5.0, 0.0, float(i * 2 + 1)))
    for i in range(4):
        t = 20.0 + i * 2
        trades.append(_make_fill("mkt", "Buy", 0.60, 5.0, 0.0, t))
        trades.append(_make_fill("mkt", "Sell", 0.45, 5.0, 0.0, t + 1))
    return _build_result(trades)


# ===========================================================================
# Dataclass defaults
# ===========================================================================

class TestPermutationTestResult:
    def test_defaults(self):
        r = PermutationTestResult()
        assert r.observed_pnl == 0.0
        assert r.p_value == 1.0
        assert not r.is_significant

    def test_significant(self):
        r = PermutationTestResult(p_value=0.01)
        assert r.is_significant

    def test_summary(self):
        r = PermutationTestResult(observed_pnl=5.0, p_value=0.01, n_permutations=100)
        s = r.summary()
        assert "SIGNIFICANT" in s
        assert "100" in s


class TestOutcomeRandomizationResult:
    def test_defaults(self):
        r = OutcomeRandomizationResult()
        assert r.p_value == 1.0
        assert not r.is_significant

    def test_summary(self):
        r = OutcomeRandomizationResult(observed_pnl=3.0, p_value=0.03, n_simulations=500)
        s = r.summary()
        assert "SIGNIFICANT" in s


class TestPathSimulationResult:
    def test_defaults(self):
        r = PathSimulationResult()
        assert r.p_value_drawdown == 1.0

    def test_favorable(self):
        r = PathSimulationResult(p_value_drawdown=0.8)
        assert r.is_favorable

    def test_unfavorable(self):
        r = PathSimulationResult(p_value_drawdown=0.2)
        assert not r.is_favorable


class TestRobustnessReport:
    def test_is_robust_all_pass(self):
        report = RobustnessReport(
            permutation=PermutationTestResult(p_value=0.01),
            outcome=OutcomeRandomizationResult(p_value=0.02),
            path=PathSimulationResult(p_value_drawdown=0.7),
        )
        assert report.is_robust

    def test_not_robust_mixed(self):
        report = RobustnessReport(
            permutation=PermutationTestResult(p_value=0.01),
            outcome=OutcomeRandomizationResult(p_value=0.50),  # not significant
        )
        assert not report.is_robust

    def test_summary_format(self):
        report = RobustnessReport(
            path=PathSimulationResult(
                observed_max_drawdown=5.0,
                p_value_drawdown=0.6,
                n_simulations=100,
            ),
        )
        s = report.summary()
        assert "ROBUSTNESS REPORT" in s
        assert "ROBUST" in s

    def test_empty_not_robust(self):
        report = RobustnessReport()
        assert not report.is_robust


# ===========================================================================
# Permutation test
# ===========================================================================

class TestPermutationTest:
    def test_basic(self):
        result = _multi_market_result()
        r = permutation_test(result, n_permutations=100, seed=42)
        assert isinstance(r, PermutationTestResult)
        assert len(r.permuted_pnls) == 100
        assert 0 <= r.p_value <= 1.0

    def test_requires_outcomes(self):
        result = _build_result(
            [_make_fill("m", "Buy", 0.5, 1.0)],
            outcomes=None,
        )
        with pytest.raises(ValueError, match="outcomes"):
            permutation_test(result, n_permutations=10)

    def test_requires_two_markets(self):
        result = _build_result(
            [_make_fill("m", "Buy", 0.5, 1.0)],
            outcomes={"m": 1.0},
        )
        with pytest.raises(ValueError, match="at least 2"):
            permutation_test(result, n_permutations=10)

    def test_deterministic(self):
        result = _multi_market_result()
        r1 = permutation_test(result, n_permutations=50, seed=123)
        r2 = permutation_test(result, n_permutations=50, seed=123)
        assert r1.p_value == r2.p_value
        assert r1.permuted_pnls == r2.permuted_pnls

    def test_p_value_range(self):
        result = _multi_market_result()
        r = permutation_test(result, n_permutations=200, seed=7)
        assert 0.0 < r.p_value <= 1.0


# ===========================================================================
# Outcome randomization
# ===========================================================================

class TestOutcomeRandomization:
    def test_basic(self):
        result = _multi_market_result()
        r = outcome_randomization(result, n_simulations=100, seed=42)
        assert isinstance(r, OutcomeRandomizationResult)
        assert len(r.simulated_pnls) == 100
        assert 0 <= r.p_value <= 1.0

    def test_requires_outcomes(self):
        result = _build_result(
            [_make_fill("m", "Buy", 0.5, 1.0)],
            outcomes=None,
        )
        with pytest.raises(ValueError, match="outcomes"):
            outcome_randomization(result, n_simulations=10)

    def test_deterministic(self):
        result = _multi_market_result()
        r1 = outcome_randomization(result, n_simulations=50, seed=99)
        r2 = outcome_randomization(result, n_simulations=50, seed=99)
        assert r1.p_value == r2.p_value
        assert r1.simulated_pnls == r2.simulated_pnls

    def test_custom_probabilities(self):
        result = _multi_market_result()
        # All markets have p=0.5
        probs = {m: 0.5 for m in result.outcomes}
        r = outcome_randomization(result, n_simulations=100, seed=42,
                                  probabilities=probs)
        assert len(r.simulated_pnls) == 100

    def test_all_closed_positions(self):
        """When all positions are closed, resolution PnL is 0 so all sims equal."""
        trades = [
            _make_fill("m", "Buy", 0.4, 5.0, 0.0, 1.0),
            _make_fill("m", "Sell", 0.6, 5.0, 0.0, 2.0),
        ]
        result = _build_result(trades, outcomes={"m": 1.0})
        r = outcome_randomization(result, n_simulations=50, seed=42)
        # All simulated PnLs should be identical (only rt_pnl, no resolution)
        assert all(p == r.simulated_pnls[0] for p in r.simulated_pnls)

    def test_p_value_range(self):
        result = _multi_market_result()
        r = outcome_randomization(result, n_simulations=200, seed=7)
        assert 0.0 < r.p_value <= 1.0


# ===========================================================================
# Path simulation
# ===========================================================================

class TestPathSimulation:
    def test_basic(self):
        result = _round_trip_result()
        r = path_simulation(result, n_simulations=100, seed=42)
        assert isinstance(r, PathSimulationResult)
        assert len(r.simulated_max_drawdowns) == 100
        assert len(r.simulated_max_consecutive_losses) == 100

    def test_deterministic(self):
        result = _round_trip_result()
        r1 = path_simulation(result, n_simulations=50, seed=77)
        r2 = path_simulation(result, n_simulations=50, seed=77)
        assert r1.simulated_max_drawdowns == r2.simulated_max_drawdowns
        assert r1.p_value_drawdown == r2.p_value_drawdown

    def test_few_trades(self):
        """< 2 round-trips returns degenerate result."""
        trades = [_make_fill("m", "Buy", 0.4, 5.0, 0.0, 1.0)]
        result = _build_result(trades)
        r = path_simulation(result, n_simulations=50, seed=42)
        assert r.n_simulations == 0
        assert r.p_value_drawdown == 0.5

    def test_max_drawdowns_nonnegative(self):
        result = _round_trip_result()
        r = path_simulation(result, n_simulations=100, seed=42)
        assert all(dd >= 0 for dd in r.simulated_max_drawdowns)
        assert r.observed_max_drawdown >= 0

    def test_terminal_equity_invariant(self):
        """All shuffled paths end at the same terminal equity."""
        result = _round_trip_result()
        r = path_simulation(result, n_simulations=50, seed=42)
        # Terminal equity = initial + sum(pnls), which is invariant to ordering
        assert r.observed_terminal_equity == pytest.approx(
            r.observed_terminal_equity, abs=0.001
        )

    def test_consecutive_losses_nonnegative(self):
        result = _round_trip_result()
        r = path_simulation(result, n_simulations=100, seed=42)
        assert all(s >= 0 for s in r.simulated_max_consecutive_losses)


# ===========================================================================
# Convenience wrapper
# ===========================================================================

class TestRobustnessTest:
    def test_all_tests_with_outcomes(self):
        result = _multi_market_result()
        report = robustness_test(result, n_simulations=50, seed=42)
        assert report.permutation is not None
        assert report.outcome is not None
        assert report.path is not None

    def test_auto_skip_without_outcomes(self):
        result = _round_trip_result()
        report = robustness_test(result, n_simulations=50, seed=42)
        # No outcomes -> permutation and outcome skipped
        assert report.permutation is None
        assert report.outcome is None
        assert report.path is not None

    def test_subset_tests(self):
        result = _round_trip_result()
        report = robustness_test(result, tests=["path"], n_simulations=50, seed=42)
        assert report.path is not None
        assert report.permutation is None
        assert report.outcome is None

    def test_empty_trades(self):
        result = _build_result([], outcomes=None)
        report = robustness_test(result, tests=["path"], n_simulations=50, seed=42)
        # No trades -> path test skipped
        assert report.path is None


# ===========================================================================
# BacktestResult.robustness() integration
# ===========================================================================

class TestBacktestResultRobustness:
    def test_method_exists(self):
        result = _round_trip_result()
        report = result.robustness(tests=["path"], n_simulations=20, seed=42)
        assert isinstance(report, RobustnessReport)
        assert report.path is not None

    def test_with_outcomes(self):
        result = _multi_market_result()
        report = result.robustness(n_simulations=20, seed=42)
        assert report.permutation is not None
        assert report.outcome is not None
        assert report.path is not None
