"""Tests for stock market hedging module.

Tests cover:
- Rust hedge math functions (hedge_ratio_ols, rolling_correlation, etc.)
- HedgeCostTracker stateful operations
- HedgeSensitivities, MultiHedgeResult, ScenarioOutcome PyClasses
- Alpaca exchange and feed config dataclasses
- Python pipeline functions (hedge_monitor, correlation_tracker, stock_hedge)
- Edge cases (NaN, empty inputs, zero variance)
"""

import math
import pytest

import horizon as hz
from horizon._horizon import (
    HedgeCostTracker,
    HedgeSensitivities,
    MultiHedgeResult,
    ScenarioOutcome,
    basis_risk,
    compute_hedge_sensitivities,
    hedge_effectiveness,
    hedge_ratio_ols,
    hedge_scenario,
    multi_ticker_hedge,
    optimal_hedge_size,
    rolling_correlation,
)


# ---------------------------------------------------------------------------
# 1. hedge_ratio_ols
# ---------------------------------------------------------------------------


class TestHedgeRatioOLS:
    def test_correlated_series(self):
        """Correlated linear series should produce a finite positive ratio."""
        spot = [50.0 + i * 0.1 for i in range(100)]
        hedge = [100.0 + i * 0.2 for i in range(100)]
        h = hedge_ratio_ols(spot, hedge, 60)
        assert h > 0 and math.isfinite(h)

    def test_zero_hedge_variance(self):
        """Constant hedge prices should return 0."""
        spot = [50.0 + i * 0.1 for i in range(20)]
        hedge = [100.0] * 20
        h = hedge_ratio_ols(spot, hedge, 20)
        assert h == 0.0

    def test_empty_inputs(self):
        assert hedge_ratio_ols([], [], 60) == 0.0

    def test_single_element(self):
        assert hedge_ratio_ols([1.0], [1.0], 60) == 0.0

    def test_two_elements(self):
        h = hedge_ratio_ols([1.0, 1.1], [2.0, 2.2], 60)
        # Should produce a valid ratio
        assert isinstance(h, float)

    def test_window_larger_than_data(self):
        spot = [50.0 + i for i in range(10)]
        hedge = [100.0 + i * 2 for i in range(10)]
        h = hedge_ratio_ols(spot, hedge, 1000)
        # Should use all available data
        assert isinstance(h, float)

    def test_negative_correlation(self):
        """Inversely correlated series should give a negative ratio.

        Uses noisy anti-correlated returns to get Cov(rS,rH) < 0.
        """
        import random
        random.seed(42)
        spot_ret = [0.01 * ((-1) ** i) + random.gauss(0, 0.001) for i in range(100)]
        hedge_ret = [-r + random.gauss(0, 0.001) for r in spot_ret]
        # Build prices from returns
        spot = [50.0]
        hedge = [100.0]
        for i in range(100):
            spot.append(spot[-1] * math.exp(spot_ret[i]))
            hedge.append(hedge[-1] * math.exp(hedge_ret[i]))
        h = hedge_ratio_ols(spot, hedge, 60)
        assert h < 0, f"Expected negative hedge ratio, got {h}"


# ---------------------------------------------------------------------------
# 2. rolling_correlation
# ---------------------------------------------------------------------------


class TestRollingCorrelation:
    def test_perfect_positive(self):
        a = list(range(30))
        b = [x * 3 + 5 for x in range(30)]
        corr = rolling_correlation(a, b, 10)
        assert len(corr) == 21  # 30 - 10 + 1
        for c in corr:
            assert abs(c - 1.0) < 1e-8

    def test_perfect_negative(self):
        a = list(range(20))
        b = [100 - x for x in range(20)]
        corr = rolling_correlation(a, b, 10)
        for c in corr:
            assert abs(c - (-1.0)) < 1e-8

    def test_empty_series(self):
        assert rolling_correlation([], [], 10) == []

    def test_window_too_large(self):
        a = list(range(5))
        b = list(range(5))
        assert rolling_correlation(a, b, 10) == []

    def test_output_length(self):
        n, w = 50, 20
        a = [float(i) for i in range(n)]
        b = [float(i) * 2 for i in range(n)]
        corr = rolling_correlation(a, b, w)
        assert len(corr) == n - w + 1


# ---------------------------------------------------------------------------
# 3. hedge_effectiveness
# ---------------------------------------------------------------------------


class TestHedgeEffectiveness:
    def test_perfect_hedge(self):
        ret = [0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01]
        he = hedge_effectiveness(ret, ret, 1.0)
        assert abs(he - 1.0) < 1e-10

    def test_no_hedge(self):
        spot = [0.01, -0.02, 0.015, -0.01]
        hedge = [0.0, 0.0, 0.0, 0.0]
        he = hedge_effectiveness(spot, hedge, 0.0)
        assert abs(he) < 1e-10

    def test_bad_hedge_negative(self):
        """A hedge that increases variance should have HE < 0."""
        spot = [0.01, -0.02, 0.015, -0.01, 0.03, -0.02]
        hedge = [-0.01, 0.02, -0.015, 0.01, -0.03, 0.02]  # opposite
        # With ratio 1.0 on anti-correlated returns, hedged = spot - 1 * hedge = spot + spot = 2*spot
        he = hedge_effectiveness(spot, hedge, 1.0)
        assert he < 0

    def test_empty_returns(self):
        assert hedge_effectiveness([], [], 1.0) == 0.0


# ---------------------------------------------------------------------------
# 4. basis_risk
# ---------------------------------------------------------------------------


class TestBasisRisk:
    def test_zero_for_perfect_hedge(self):
        ret = [0.01, -0.02, 0.015, -0.01, 0.03]
        br = basis_risk(ret, ret, 1.0)
        assert br < 1e-20

    def test_positive_for_imperfect_hedge(self):
        spot = [0.01, -0.02, 0.015, -0.01, 0.03]
        hedge = [0.008, -0.018, 0.012, -0.008, 0.025]
        br = basis_risk(spot, hedge, 1.0)
        assert br > 0

    def test_empty_returns(self):
        assert basis_risk([], [], 1.0) == 0.0


# ---------------------------------------------------------------------------
# 5. optimal_hedge_size
# ---------------------------------------------------------------------------


class TestOptimalHedgeSize:
    def test_basic(self):
        size = optimal_hedge_size(10000.0, 0.5)
        assert abs(size - 5000.0) < 1e-10

    def test_capped(self):
        size = optimal_hedge_size(10000.0, 0.8, 5000.0)
        assert abs(size - 5000.0) < 1e-10

    def test_uncapped(self):
        size = optimal_hedge_size(10000.0, 0.8)
        assert abs(size - 8000.0) < 1e-10

    def test_zero_ratio(self):
        size = optimal_hedge_size(10000.0, 0.0)
        assert abs(size) < 1e-10

    def test_negative_ratio(self):
        size = optimal_hedge_size(10000.0, -0.5)
        assert abs(size - 5000.0) < 1e-10  # abs value


# ---------------------------------------------------------------------------
# 6. compute_hedge_sensitivities
# ---------------------------------------------------------------------------


class TestHedgeSensitivities:
    def test_basic_output(self):
        spot = [50.0 + i * 0.1 for i in range(100)]
        hedge = [100.0 + i * 0.15 for i in range(100)]
        sens = compute_hedge_sensitivities(spot, hedge, 100.0, 60)
        assert isinstance(sens, HedgeSensitivities)
        assert sens.tracking_error >= 0.0
        assert math.isfinite(sens.delta)
        assert math.isfinite(sens.beta)

    def test_empty_inputs(self):
        sens = compute_hedge_sensitivities([], [], 1.0, 60)
        assert sens.delta == 0.0
        assert sens.beta == 0.0
        assert sens.tracking_error == 0.0

    def test_repr(self):
        sens = compute_hedge_sensitivities(
            [50.0 + i for i in range(20)],
            [100.0 + i * 2 for i in range(20)],
            1.0, 10,
        )
        assert "HedgeSensitivities" in repr(sens)


# ---------------------------------------------------------------------------
# 7. multi_ticker_hedge
# ---------------------------------------------------------------------------


class TestMultiTickerHedge:
    def test_single_ticker_perfect(self):
        spot = [0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01]
        hedge = [spot]
        result = multi_ticker_hedge(spot, hedge, ["SPY"])
        assert isinstance(result, MultiHedgeResult)
        assert len(result.weights) == 1
        assert abs(result.weights[0] - 1.0) < 0.01
        assert result.hedge_effectiveness > 0.99

    def test_two_tickers(self):
        spot = [0.01, -0.02, 0.015, -0.01, 0.03, -0.02, 0.01, -0.015, 0.02, -0.01]
        h1 = [0.008, -0.016, 0.012, -0.008, 0.024, -0.016, 0.008, -0.012, 0.016, -0.008]
        h2 = [0.002, -0.004, 0.003, -0.002, 0.006, -0.004, 0.002, -0.003, 0.004, -0.002]
        result = multi_ticker_hedge(spot, [h1, h2], ["TLT", "GLD"])
        assert len(result.weights) == 2
        assert len(result.tickers) == 2

    def test_empty_inputs(self):
        result = multi_ticker_hedge([], [], [])
        assert result.weights == []
        assert result.hedge_effectiveness == 0.0

    def test_repr(self):
        result = multi_ticker_hedge(
            [0.01, -0.02, 0.015, -0.01, 0.03],
            [[0.01, -0.02, 0.015, -0.01, 0.03]],
            ["SPY"],
        )
        assert "MultiHedgeResult" in repr(result)


# ---------------------------------------------------------------------------
# 8. hedge_scenario
# ---------------------------------------------------------------------------


class TestHedgeScenario:
    def test_basic_scenario(self):
        outcome = hedge_scenario(100.0, 50.0, -80.0, 100.0, -0.10, 0.8, 0.2, 0.2)
        assert isinstance(outcome, ScenarioOutcome)
        # Spot PnL: 100 * 50 * (-0.10) = -500
        assert abs(outcome.unhedged_pnl - (-500.0)) < 1.0
        # Hedge move: 0.8 * 1.0 * (-0.10) = -0.08
        assert abs(outcome.hedge_move_pct - (-0.08)) < 1e-10

    def test_zero_spot_move(self):
        outcome = hedge_scenario(100.0, 50.0, -80.0, 100.0, 0.0)
        assert abs(outcome.portfolio_pnl) < 1e-10
        assert abs(outcome.unhedged_pnl) < 1e-10

    def test_no_hedge_position(self):
        outcome = hedge_scenario(100.0, 50.0, 0.0, 100.0, -0.10)
        assert abs(outcome.portfolio_pnl - outcome.unhedged_pnl) < 1e-10

    def test_repr(self):
        outcome = hedge_scenario(100.0, 50.0, -80.0, 100.0, -0.10)
        assert "ScenarioOutcome" in repr(outcome)


# ---------------------------------------------------------------------------
# 9. HedgeCostTracker
# ---------------------------------------------------------------------------


class TestHedgeCostTracker:
    def test_empty_tracker(self):
        ct = HedgeCostTracker()
        assert ct.total_cost() == 0.0
        assert ct.cost_as_pct_of_notional() == 0.0
        assert ct.avg_cost_per_rebalance() == 0.0

    def test_single_trade(self):
        ct = HedgeCostTracker()
        ct.record_trade(10000.0, 5.0, 2.0)
        assert abs(ct.total_cost() - 7.0) < 1e-10
        assert abs(ct.cost_as_pct_of_notional() - 0.07) < 1e-10

    def test_multiple_trades(self):
        ct = HedgeCostTracker()
        ct.record_trade(10000.0, 5.0, 2.0)
        ct.record_trade(20000.0, 10.0, 4.0)
        assert abs(ct.total_cost() - 21.0) < 1e-10
        assert abs(ct.avg_cost_per_rebalance() - 10.5) < 1e-10

    def test_repr(self):
        ct = HedgeCostTracker()
        ct.record_trade(1000.0, 1.0, 0.5)
        assert "HedgeCostTracker" in repr(ct)


# ---------------------------------------------------------------------------
# 10. Alpaca Exchange Dataclass
# ---------------------------------------------------------------------------


class TestAlpacaExchangeConfig:
    def test_default_paper(self):
        exch = hz.Alpaca()
        assert exch.paper is True
        assert "paper" in exch.api_url

    def test_live_mode(self):
        exch = hz.Alpaca(paper=False, api_key="test", api_secret="test")
        assert exch.paper is False
        assert "paper" not in exch.api_url

    def test_custom_url(self):
        exch = hz.Alpaca(api_url="https://custom.api.com")
        assert exch.api_url == "https://custom.api.com"

    def test_data_source_default(self):
        exch = hz.Alpaca()
        assert exch.data_source == "iex"


# ---------------------------------------------------------------------------
# 11. AlpacaFeed Dataclass
# ---------------------------------------------------------------------------


class TestAlpacaFeedConfig:
    def test_default_fields(self):
        feed = hz.AlpacaFeed(symbols=["AAPL", "SPY"])
        assert feed.symbols == ["AAPL", "SPY"]
        assert feed._type == "alpaca"
        assert feed.data_source == "iex"

    def test_custom_data_source(self):
        feed = hz.AlpacaFeed(symbols=["AAPL"], data_source="sip")
        assert feed.data_source == "sip"

    def test_empty_symbols(self):
        feed = hz.AlpacaFeed()
        assert feed.symbols == []


# ---------------------------------------------------------------------------
# 12. Python pipeline: hedge_monitor
# ---------------------------------------------------------------------------


class TestHedgeMonitorPipeline:
    @pytest.fixture
    def engine(self):
        return hz.Engine()

    def test_hedge_monitor_returns_dict(self, engine):
        try:
            from horizon.stock_hedge import hedge_monitor
            fn = hedge_monitor("pred_feed", "stock_feed", window=10)
            assert callable(fn)
            assert fn.__name__ == "hedge_monitor"
        except PermissionError:
            pytest.skip("Ultra tier required")


# ---------------------------------------------------------------------------
# 13. Python pipeline: correlation_tracker
# ---------------------------------------------------------------------------


class TestCorrelationTrackerPipeline:
    def test_creates_callable(self):
        try:
            from horizon.stock_hedge import correlation_tracker
            fn = correlation_tracker(["feed_a", "feed_b"], window=10)
            assert callable(fn)
            assert fn.__name__ == "correlation_tracker"
        except PermissionError:
            pytest.skip("Ultra tier required")


# ---------------------------------------------------------------------------
# 14. Multi-market state isolation
# ---------------------------------------------------------------------------


class TestMultiMarketIsolation:
    def test_independent_ratios(self):
        """Different price series produce different hedge ratios."""
        spot_a = [50.0 + i * 0.1 for i in range(50)]
        hedge_a = [100.0 + i * 0.2 for i in range(50)]
        spot_b = [50.0 + i * 0.5 for i in range(50)]
        hedge_b = [100.0 + i * 0.1 for i in range(50)]

        h_a = hedge_ratio_ols(spot_a, hedge_a, 30)
        h_b = hedge_ratio_ols(spot_b, hedge_b, 30)

        # Different series → different ratios
        assert abs(h_a - h_b) > 0.01

    def test_rolling_correlation_independence(self):
        """Rolling correlations computed independently for each pair."""
        a = [float(i) for i in range(30)]
        b = [float(i) * 2 for i in range(30)]
        c = [30.0 - float(i) for i in range(30)]

        corr_ab = rolling_correlation(a, b, 10)
        corr_ac = rolling_correlation(a, c, 10)

        # a,b perfectly positive; a,c perfectly negative
        assert corr_ab[-1] > 0.9
        assert corr_ac[-1] < -0.9


# ---------------------------------------------------------------------------
# 15. compute_hedge_report standalone
# ---------------------------------------------------------------------------


class TestComputeHedgeReport:
    def test_returns_complete_dict(self):
        try:
            from horizon.stock_hedge import compute_hedge_report
            spot = [50.0 + i * 0.1 for i in range(100)]
            hedge = [100.0 + i * 0.15 for i in range(100)]
            report = compute_hedge_report(spot, hedge, window=30)

            assert "hedge_ratio" in report
            assert "hedge_effectiveness" in report
            assert "basis_risk" in report
            assert "sensitivities" in report
            assert "correlation_history" in report
            assert isinstance(report["sensitivities"], dict)
            assert "delta" in report["sensitivities"]
        except PermissionError:
            pytest.skip("Ultra tier required")


# ---------------------------------------------------------------------------
# 16. run_scenario standalone
# ---------------------------------------------------------------------------


class TestRunScenario:
    def test_multiple_scenarios(self):
        try:
            from horizon.stock_hedge import run_scenario
            results = run_scenario(
                spot_position=100.0,
                spot_price=0.65,
                hedge_position=-50.0,
                hedge_price=100.0,
                spot_moves=[-0.10, 0.0, 0.10],
            )
            assert len(results) == 3
            assert results[1]["portfolio_pnl"] == 0.0  # zero move
        except PermissionError:
            pytest.skip("Ultra tier required")


# ---------------------------------------------------------------------------
# 17. Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    def test_all_same_prices(self):
        prices = [100.0] * 50
        h = hedge_ratio_ols(prices, prices, 30)
        assert h == 0.0  # zero variance

    def test_very_small_window(self):
        spot = [50.0 + i * 0.1 for i in range(10)]
        hedge = [100.0 + i * 0.2 for i in range(10)]
        h = hedge_ratio_ols(spot, hedge, 3)
        assert math.isfinite(h)

    def test_mismatched_lengths(self):
        spot = [50.0 + i * 0.1 for i in range(100)]
        hedge = [100.0 + i * 0.2 for i in range(50)]
        h = hedge_ratio_ols(spot, hedge, 30)
        assert math.isfinite(h)

    def test_sensitivities_short_series(self):
        sens = compute_hedge_sensitivities([1.0, 2.0], [3.0, 4.0], 1.0, 60)
        assert math.isfinite(sens.delta)

    def test_multi_hedge_mismatched_tickers(self):
        """If tickers length != number of hedge return series, returns zeros."""
        result = multi_ticker_hedge([0.01, -0.02, 0.03], [[0.01, -0.02, 0.03]], ["A", "B"])
        assert len(result.weights) == 1  # k=1 since only 1 hedge series

    def test_scenario_zero_vol(self):
        outcome = hedge_scenario(100.0, 50.0, -80.0, 100.0, -0.10, 0.8, 0.2, 0.0)
        # spot_vol=0 → vol_ratio=inf → finite_or_zero should handle
        assert math.isfinite(outcome.hedge_move_pct)


# =========================================================================
# Auth module tests
# =========================================================================

class TestAuthKeyGeneration:
    """Tests for key generation and hashing (no network)."""

    def test_generate_sdk_key_format(self):
        from horizon.auth import generate_sdk_key
        key = generate_sdk_key()
        assert key.startswith("hz_sdk_")
        assert len(key) == 7 + 64  # "hz_sdk_" + 64 hex chars

    def test_generate_sdk_key_uniqueness(self):
        from horizon.auth import generate_sdk_key
        keys = {generate_sdk_key() for _ in range(100)}
        assert len(keys) == 100

    def test_generate_sdk_key_hex_chars(self):
        from horizon.auth import generate_sdk_key
        key = generate_sdk_key()
        hex_part = key[7:]
        assert all(c in "0123456789abcdef" for c in hex_part)

    def test_hash_api_key_sha256(self):
        from horizon.auth import hash_api_key
        h = hash_api_key("hello")
        assert h == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

    def test_hash_api_key_length(self):
        from horizon.auth import hash_api_key, generate_sdk_key
        key = generate_sdk_key()
        h = hash_api_key(key)
        assert len(h) == 64

    def test_hash_api_key_deterministic(self):
        from horizon.auth import hash_api_key
        assert hash_api_key("test") == hash_api_key("test")

    def test_hash_api_key_different_inputs(self):
        from horizon.auth import hash_api_key
        assert hash_api_key("a") != hash_api_key("b")


class TestAuthCredentialEncryption:
    """Tests for encryption/decryption of credentials at rest."""

    def test_encrypt_decrypt_roundtrip(self):
        from horizon.auth import _encrypt_key, _decrypt_key
        key = "hz_sdk_" + "a" * 64
        encrypted = _encrypt_key(key)
        assert encrypted != key  # not plaintext
        decrypted = _decrypt_key(encrypted)
        assert decrypted == key

    def test_encrypted_is_base64(self):
        from horizon.auth import _encrypt_key
        import base64
        encrypted = _encrypt_key("hz_sdk_test")
        # Should not raise
        base64.b64decode(encrypted)

    def test_different_keys_different_ciphertext(self):
        from horizon.auth import _encrypt_key
        c1 = _encrypt_key("hz_sdk_aaa")
        c2 = _encrypt_key("hz_sdk_bbb")
        assert c1 != c2


class TestAuthCredentialStorage:
    """Tests for save/load credentials (uses tmp directory)."""

    def test_save_and_load_v2_encrypted(self, tmp_path, monkeypatch):
        from horizon import auth
        cred_file = tmp_path / "credentials.json"
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", tmp_path)
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", cred_file)

        key = "hz_sdk_" + "a" * 64
        path = auth.save_credentials(key)
        assert path == str(cred_file)
        assert cred_file.exists()

        # Verify the file is v2 encrypted (not plaintext)
        import json
        data = json.loads(cred_file.read_text())
        assert data.get("version") == 2
        assert "api_key_encrypted" in data
        assert "api_key" not in data  # no plaintext key

        loaded = auth.load_credentials()
        assert loaded == key

    def test_load_legacy_v1_plaintext(self, tmp_path, monkeypatch):
        """Load credentials saved in old v1 (plaintext) format."""
        from horizon import auth
        import json
        cred_file = tmp_path / "credentials.json"
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", cred_file)
        # Write v1 format
        cred_file.write_text(json.dumps({"api_key": "hz_sdk_legacy", "created_at": "2024-01-01"}))
        loaded = auth.load_credentials()
        assert loaded == "hz_sdk_legacy"

    def test_load_missing_file(self, tmp_path, monkeypatch):
        from horizon import auth
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", tmp_path / "nonexistent.json")
        assert auth.load_credentials() is None

    def test_load_corrupt_file(self, tmp_path, monkeypatch):
        from horizon import auth
        cred_file = tmp_path / "credentials.json"
        cred_file.write_text("not json")
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", cred_file)
        assert auth.load_credentials() is None

    def test_save_creates_directory(self, tmp_path, monkeypatch):
        from horizon import auth
        nested = tmp_path / "deep" / "dir"
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", nested)
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", nested / "credentials.json")
        auth.save_credentials("hz_sdk_test")
        assert (nested / "credentials.json").exists()

    def test_save_includes_prefix(self, tmp_path, monkeypatch):
        from horizon import auth
        import json
        cred_file = tmp_path / "credentials.json"
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", tmp_path)
        monkeypatch.setattr(auth, "_CREDENTIALS_FILE", cred_file)
        auth.save_credentials("hz_sdk_abcdef1234567890")
        data = json.loads(cred_file.read_text())
        assert data["prefix"] == "hz_sdk_abcdef12"  # first 15 chars


class TestAuthJWTDecode:
    """Tests for JWT payload decoding (hardened)."""

    def test_decode_valid_jwt(self):
        from horizon.auth import _decode_jwt_payload
        import base64, json
        payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode().rstrip("=")
        token = f"header.{payload}.signature"
        result = _decode_jwt_payload(token)
        assert result["sub"] == "user123"

    def test_decode_invalid_jwt_no_dots(self):
        from horizon.auth import _decode_jwt_payload
        assert _decode_jwt_payload("not-a-jwt") == {}

    def test_decode_empty_string(self):
        from horizon.auth import _decode_jwt_payload
        assert _decode_jwt_payload("") == {}

    def test_decode_none_type(self):
        from horizon.auth import _decode_jwt_payload
        assert _decode_jwt_payload(None) == {}

    def test_decode_too_many_parts(self):
        from horizon.auth import _decode_jwt_payload
        # More than 3 parts should be rejected
        assert _decode_jwt_payload("a.b.c.d") == {}

    def test_decode_two_parts_rejected(self):
        from horizon.auth import _decode_jwt_payload
        assert _decode_jwt_payload("a.b") == {}

    def test_decode_non_dict_payload(self):
        from horizon.auth import _decode_jwt_payload
        import base64, json
        payload = base64.urlsafe_b64encode(json.dumps([1, 2, 3]).encode()).decode().rstrip("=")
        token = f"header.{payload}.signature"
        assert _decode_jwt_payload(token) == {}


class TestAuthRateLimiting:
    """Tests for client-side rate limiting."""

    def test_rate_limit_allows_initial_requests(self, tmp_path, monkeypatch):
        from horizon import auth
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", tmp_path)
        monkeypatch.setattr(auth, "_RATE_LIMIT_FILE", tmp_path / ".auth_rate_limit")
        for _ in range(5):
            assert auth._check_rate_limit() is None  # allowed

    def test_rate_limit_blocks_after_max(self, tmp_path, monkeypatch):
        from horizon import auth
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", tmp_path)
        monkeypatch.setattr(auth, "_RATE_LIMIT_FILE", tmp_path / ".auth_rate_limit")
        for _ in range(5):
            auth._check_rate_limit()
        result = auth._check_rate_limit()
        assert result is not None
        assert "rate limit exceeded" in result

    def test_rate_limit_resets_after_window(self, tmp_path, monkeypatch):
        from horizon import auth
        import time as _time
        monkeypatch.setattr(auth, "_CREDENTIALS_DIR", tmp_path)
        monkeypatch.setattr(auth, "_RATE_LIMIT_FILE", tmp_path / ".auth_rate_limit")
        monkeypatch.setattr(auth, "_RATE_LIMIT_WINDOW", 1)  # 1 second window
        for _ in range(5):
            auth._check_rate_limit()
        # Wait for window to expire
        _time.sleep(1.1)
        assert auth._check_rate_limit() is None


class TestAuthPasswordResolution:
    """Tests for password resolution (env var vs argument)."""

    def test_env_var_preferred(self, monkeypatch):
        from horizon.auth import resolve_password
        monkeypatch.setenv("HORIZON_PASSWORD", "from_env")
        assert resolve_password("from_arg") == "from_env"

    def test_falls_back_to_argument(self, monkeypatch):
        from horizon.auth import resolve_password
        monkeypatch.delenv("HORIZON_PASSWORD", raising=False)
        assert resolve_password("from_arg") == "from_arg"

    def test_returns_none_when_neither(self, monkeypatch):
        from horizon.auth import resolve_password
        monkeypatch.delenv("HORIZON_PASSWORD", raising=False)
        assert resolve_password(None) is None
        assert resolve_password("") is None

    def test_env_var_empty_uses_argument(self, monkeypatch):
        from horizon.auth import resolve_password
        monkeypatch.setenv("HORIZON_PASSWORD", "")
        assert resolve_password("from_arg") == "from_arg"


class TestAuthToolFunctions:
    """Tests for tool wrapper functions (mocked network)."""

    def test_key_status_no_key(self, monkeypatch):
        from horizon import tools, auth
        monkeypatch.delenv("HORIZON_API_KEY", raising=False)
        monkeypatch.setattr(auth, "load_credentials", lambda: None)
        result = tools.horizon_key_status()
        assert result["has_env_key"] is False
        assert result["has_saved_key"] is False
        assert result["active_source"] == "none"

    def test_key_status_env_key(self, monkeypatch):
        from horizon import tools, auth
        monkeypatch.setenv("HORIZON_API_KEY", "hz_sdk_test123")
        monkeypatch.setattr(auth, "load_credentials", lambda: None)
        result = tools.horizon_key_status()
        assert result["has_env_key"] is True
        assert result["active_source"] == "env"
        assert result["prefix"] == "hz_sdk_test123"[:15]

    def test_key_status_saved_key(self, monkeypatch):
        from horizon import tools, auth
        monkeypatch.delenv("HORIZON_API_KEY", raising=False)
        monkeypatch.setattr(auth, "load_credentials", lambda: "hz_sdk_saved_key_here")
        result = tools.horizon_key_status()
        assert result["has_env_key"] is False
        assert result["has_saved_key"] is True
        assert result["active_source"] == "file"

    def test_setup_no_password_returns_error(self, monkeypatch):
        from horizon import tools
        monkeypatch.delenv("HORIZON_PASSWORD", raising=False)
        result = tools.horizon_setup("user@test.com", "")
        assert "error" in result
        assert "no password" in result["error"]

    def test_login_no_password_returns_error(self, monkeypatch):
        from horizon import tools
        monkeypatch.delenv("HORIZON_PASSWORD", raising=False)
        result = tools.horizon_login("user@test.com", "")
        assert "error" in result
        assert "no password" in result["error"]

    def test_setup_response_has_no_full_key(self, monkeypatch):
        """Verify the setup response never contains the full API key."""
        from horizon import tools, auth

        # Mock the full auth flow
        def mock_setup(email, password, name=""):
            return {
                "status": "ok",
                "prefix": "hz_sdk_abcdef1",
                "credentials_path": "/tmp/test",
                "message": "saved",
            }

        monkeypatch.setenv("HORIZON_PASSWORD", "testpass")
        monkeypatch.setattr(auth, "setup", mock_setup)
        result = tools.horizon_setup("user@test.com")
        assert "api_key" not in result
        assert "key" not in result or result.get("key", "").startswith("hz_sdk_") is False
