"""Tests for portable alpha / market-neutral decomposition.

Tests cover:
- Rust functions: beta_decompose, compute_factor_exposures,
  neutrality_adjustment, incremental_beta_update
- Rust types: AlphaDecomposition, FactorExposure, NeutralityAdjustment
- Python pipeline functions: beta_decompose_pipeline, market_neutral,
  portable_alpha_pipeline, factor_model
"""

import pytest


# ---------------------------------------------------------------------------
# 1. beta_decompose
# ---------------------------------------------------------------------------


class TestBetaDecompose:
    def test_perfect(self):
        """portfolio = 2 * factor should give beta=2, R2~1."""
        from horizon._horizon import beta_decompose

        factor = [0.01, -0.02, 0.03, -0.01, 0.02]
        port = [2.0 * f for f in factor]
        result = beta_decompose(port, factor)
        assert result.beta == pytest.approx(2.0, abs=1e-6)
        assert result.r_squared > 0.99
        assert result.alpha == pytest.approx(0.0, abs=1e-6)

    def test_with_alpha(self):
        """portfolio = factor + constant alpha."""
        from horizon._horizon import beta_decompose

        factor = [0.01, -0.02, 0.03, -0.01, 0.02]
        port = [f + 0.005 for f in factor]
        result = beta_decompose(port, factor)
        assert result.beta == pytest.approx(1.0, abs=1e-6)
        assert result.alpha == pytest.approx(0.005, abs=0.001)

    def test_short(self):
        """Fewer than 3 observations returns zero decomposition."""
        from horizon._horizon import beta_decompose

        result = beta_decompose([0.01, 0.02], [0.01, 0.02])
        assert result.beta == 0.0
        assert result.r_squared == 0.0

    def test_empty(self):
        from horizon._horizon import beta_decompose

        result = beta_decompose([], [])
        assert result.beta == 0.0

    def test_uncorrelated(self):
        from horizon._horizon import beta_decompose

        factor = [0.01, -0.01, 0.01, -0.01]
        port = [0.02, 0.02, -0.02, -0.02]  # unrelated pattern
        result = beta_decompose(port, factor)
        assert result.r_squared < 0.5

    def test_residuals_present(self):
        from horizon._horizon import beta_decompose

        factor = [0.01, -0.02, 0.03, -0.01, 0.02]
        port = [f * 1.5 + 0.001 for f in factor]
        result = beta_decompose(port, factor)
        assert len(result.residual_returns) > 0

    def test_tracking_error_and_ir(self):
        from horizon._horizon import beta_decompose

        factor = [0.01, -0.02, 0.03, -0.01, 0.02]
        port = [f + 0.005 for f in factor]
        result = beta_decompose(port, factor)
        assert result.tracking_error >= 0.0
        assert isinstance(result.information_ratio, float)


# ---------------------------------------------------------------------------
# 2. compute_factor_exposures
# ---------------------------------------------------------------------------


class TestComputeFactorExposures:
    def test_single_factor(self):
        from horizon._horizon import compute_factor_exposures

        port = [0.02, -0.01, 0.03, -0.02, 0.01]
        factor = [0.01, -0.005, 0.015, -0.01, 0.005]
        exposures = compute_factor_exposures(
            port, ["market"], [factor]
        )
        assert len(exposures) == 1
        assert exposures[0].factor_name == "market"
        assert exposures[0].beta > 0.0
        assert 0.0 <= exposures[0].r_squared <= 1.0

    def test_multiple_factors(self):
        from horizon._horizon import compute_factor_exposures

        port = [0.02, -0.01, 0.03, -0.02, 0.01]
        f1 = [0.01, -0.005, 0.015, -0.01, 0.005]
        f2 = [0.005, 0.01, -0.005, 0.01, -0.005]
        exposures = compute_factor_exposures(
            port, ["market", "vol"], [f1, f2]
        )
        assert len(exposures) == 2
        assert exposures[0].factor_name == "market"
        assert exposures[1].factor_name == "vol"

    def test_empty_factors(self):
        from horizon._horizon import compute_factor_exposures

        port = [0.01, 0.02, 0.03]
        exposures = compute_factor_exposures(port, [], [])
        assert len(exposures) == 0


# ---------------------------------------------------------------------------
# 3. neutrality_adjustment
# ---------------------------------------------------------------------------


class TestNeutralityAdjustment:
    def test_within_tolerance(self):
        from horizon._horizon import neutrality_adjustment

        adjs = neutrality_adjustment(
            [0.02], ["market"], [0.0], [0.05]
        )
        assert len(adjs) == 1
        assert adjs[0].is_within_tolerance
        assert adjs[0].factor_name == "market"

    def test_outside_tolerance(self):
        from horizon._horizon import neutrality_adjustment

        adjs = neutrality_adjustment(
            [1.2, 0.3],
            ["market", "vol"],
            [0.0, 0.0],
            [0.05, 0.05],
        )
        assert len(adjs) == 2
        assert not adjs[0].is_within_tolerance
        assert adjs[0].hedge_ratio == pytest.approx(-1.2, abs=1e-10)
        assert not adjs[1].is_within_tolerance
        assert adjs[1].hedge_ratio == pytest.approx(-0.3, abs=1e-10)

    def test_exact_target(self):
        from horizon._horizon import neutrality_adjustment

        adjs = neutrality_adjustment(
            [0.0], ["market"], [0.0], [0.05]
        )
        assert adjs[0].is_within_tolerance
        assert adjs[0].hedge_ratio == pytest.approx(0.0, abs=1e-10)

    def test_empty(self):
        from horizon._horizon import neutrality_adjustment

        adjs = neutrality_adjustment([], [], [], [])
        assert len(adjs) == 0


# ---------------------------------------------------------------------------
# 4. incremental_beta_update
# ---------------------------------------------------------------------------


class TestIncrementalBetaUpdate:
    def test_basic(self):
        from horizon._horizon import incremental_beta_update

        cov, var, beta = incremental_beta_update(0.0, 0.0, 0.02, 0.01, 0.94)
        assert cov > 0.0
        assert var > 0.0
        assert beta > 0.0

    def test_zero_variance(self):
        from horizon._horizon import incremental_beta_update

        _, _, beta = incremental_beta_update(0.0, 0.0, 0.02, 0.0, 0.94)
        assert beta == 0.0

    def test_decay_effect(self):
        from horizon._horizon import incremental_beta_update

        # Higher decay = more weight on history
        c1, v1, b1 = incremental_beta_update(0.001, 0.001, 0.02, 0.01, 0.5)
        c2, v2, b2 = incremental_beta_update(0.001, 0.001, 0.02, 0.01, 0.99)
        # With higher decay, the update should rely more on prior values
        assert c1 != c2

    def test_negative_beta(self):
        from horizon._horizon import incremental_beta_update

        # Negative port return with positive factor return => negative cov
        cov, var, beta = incremental_beta_update(0.0, 0.0, -0.02, 0.01, 0.94)
        assert beta < 0.0

    def test_cumulative_updates(self):
        from horizon._horizon import incremental_beta_update

        cov, var, beta = 0.0, 0.0, 0.0
        for i in range(10):
            p_ret = 0.02 * (i + 1)
            f_ret = 0.01 * (i + 1)
            cov, var, beta = incremental_beta_update(cov, var, p_ret, f_ret, 0.94)
        assert beta > 0.0
        assert var > 0.0


# ---------------------------------------------------------------------------
# 5. Pipeline functions (Ultra tier)
# ---------------------------------------------------------------------------


class TestPipeline:
    def test_beta_decompose_pipeline(self):
        try:
            from horizon.portable_alpha import beta_decompose_pipeline
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = beta_decompose_pipeline("portfolio", "factor", window=50)
        assert callable(fn)
        assert fn.__name__ == "beta_decompose"

    def test_market_neutral(self):
        try:
            from horizon.portable_alpha import market_neutral
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = market_neutral("portfolio", "factor", target_beta=0.0, tolerance=0.05)
        assert callable(fn)
        assert fn.__name__ == "market_neutral"

    def test_portable_alpha_pipeline(self):
        try:
            from horizon.portable_alpha import portable_alpha
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = portable_alpha(
            "portfolio",
            factor_feeds={"market": "mkt_feed", "vol": "vol_feed"},
        )
        assert callable(fn)
        assert fn.__name__ == "portable_alpha"

    def test_factor_model(self):
        try:
            from horizon.portable_alpha import factor_model
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = factor_model(factor_feeds={"market": "mkt"}, decay=0.94)
        assert callable(fn)
        assert fn.__name__ == "factor_model"
