"""Tests for adversarial simulation.

Tests cover:
- Rust types: AdversaryType, AdversaryConfig, AdversaryAction, SimulationMetrics,
  AdversarialResult
- Rust functions: whale_action, front_runner_action, spoofing_action,
  liquidity_drainer_action, compute_simulation_metrics, vulnerability_score
- Edge cases: empty PnL vectors, zero baselines, boundary conditions
"""

import math
import pytest


# ---------------------------------------------------------------------------
# 1. whale_action
# ---------------------------------------------------------------------------


class TestWhaleAction:
    def test_basic(self):
        from horizon._horizon import whale_action, AdversaryType

        action = whale_action("M1", 0.50, 100.0, 1.0, 42)
        assert action.adversary_type == AdversaryType.Whale
        assert action.market_id == "M1"
        assert action.price > 0.0
        assert action.price < 1.0
        assert action.is_cancel is False
        assert len(action.description) > 0

    def test_amplified_size(self):
        from horizon._horizon import whale_action

        action = whale_action("M1", 0.50, 100.0, 1.0, 42)
        # Should be 3-10x average size
        assert action.size > 100.0

    def test_high_intensity(self):
        from horizon._horizon import whale_action

        action = whale_action("M1", 0.50, 100.0, 5.0, 42)
        # Higher intensity should produce larger sizes
        assert action.size > 500.0  # 5x intensity * 3-10x avg

    def test_side_is_buy_or_sell(self):
        from horizon._horizon import whale_action

        action = whale_action("M1", 0.50, 100.0, 1.0, 42)
        assert action.side in ("buy", "sell")

    def test_price_clamped(self):
        from horizon._horizon import whale_action

        # Edge price near boundaries
        action = whale_action("M1", 0.99, 100.0, 1.0, 42)
        assert action.price <= 0.99
        assert action.price >= 0.01


# ---------------------------------------------------------------------------
# 2. front_runner_action
# ---------------------------------------------------------------------------


class TestFrontRunnerAction:
    def test_buy(self):
        from horizon._horizon import front_runner_action, AdversaryType

        action = front_runner_action("M1", "buy", 0.50, 100.0, 50.0)
        assert action.adversary_type == AdversaryType.FrontRunner
        assert action.side == "buy"
        assert action.price < 0.50  # front-runs by buying cheaper
        assert action.size == pytest.approx(50.0, abs=1e-10)  # half the detected size
        assert action.is_cancel is False

    def test_sell(self):
        from horizon._horizon import front_runner_action, AdversaryType

        action = front_runner_action("M1", "sell", 0.50, 100.0, 50.0)
        assert action.adversary_type == AdversaryType.FrontRunner
        assert action.side == "sell"
        assert action.price > 0.50  # front-runs by selling higher
        assert action.size == pytest.approx(50.0, abs=1e-10)

    def test_edge_bps(self):
        from horizon._horizon import front_runner_action

        action = front_runner_action("M1", "buy", 0.50, 100.0, 100.0)
        # edge = 100bps = 0.01
        assert action.price == pytest.approx(0.49, abs=1e-6)

    def test_price_clamped(self):
        from horizon._horizon import front_runner_action

        # Near boundary
        action = front_runner_action("M1", "buy", 0.01, 100.0, 500.0)
        assert action.price >= 0.01


# ---------------------------------------------------------------------------
# 3. spoofing_action
# ---------------------------------------------------------------------------


class TestSpoofingAction:
    def test_levels(self):
        from horizon._horizon import spoofing_action, AdversaryType

        actions = spoofing_action("M1", 0.50, 1000.0, 3)
        assert len(actions) == 3
        for a in actions:
            assert a.adversary_type == AdversaryType.Spoofing
            assert a.is_cancel is True
            assert a.size == pytest.approx(1000.0, abs=1e-10)
            assert a.market_id == "M1"

    def test_is_cancel(self):
        from horizon._horizon import spoofing_action

        actions = spoofing_action("M1", 0.50, 500.0, 5)
        for a in actions:
            assert a.is_cancel is True

    def test_prices_decrease(self):
        from horizon._horizon import spoofing_action

        actions = spoofing_action("M1", 0.50, 500.0, 3)
        # Each level should be further from current price
        for i in range(1, len(actions)):
            assert actions[i].price < actions[i - 1].price

    def test_single_level(self):
        from horizon._horizon import spoofing_action

        actions = spoofing_action("M1", 0.50, 500.0, 1)
        assert len(actions) == 1

    def test_zero_levels(self):
        from horizon._horizon import spoofing_action

        actions = spoofing_action("M1", 0.50, 500.0, 0)
        assert len(actions) == 0


# ---------------------------------------------------------------------------
# 4. liquidity_drainer_action
# ---------------------------------------------------------------------------


class TestLiquidityDrainer:
    def test_basic(self):
        from horizon._horizon import liquidity_drainer_action, AdversaryType

        actions = liquidity_drainer_action("M1", 0.48, 0.52, 500.0)
        assert len(actions) == 2
        for a in actions:
            assert a.adversary_type == AdversaryType.LiquidityDrainer
            assert a.is_cancel is False

    def test_sides(self):
        from horizon._horizon import liquidity_drainer_action

        actions = liquidity_drainer_action("M1", 0.48, 0.52, 500.0)
        assert actions[0].side == "sell"
        assert actions[0].price == pytest.approx(0.48, abs=1e-10)
        assert actions[1].side == "buy"
        assert actions[1].price == pytest.approx(0.52, abs=1e-10)

    def test_zero_bid(self):
        from horizon._horizon import liquidity_drainer_action

        actions = liquidity_drainer_action("M1", 0.0, 0.52, 500.0)
        # Should only have ask-side drain
        assert len(actions) == 1
        assert actions[0].side == "buy"

    def test_zero_size(self):
        from horizon._horizon import liquidity_drainer_action

        actions = liquidity_drainer_action("M1", 0.48, 0.52, 0.0)
        assert len(actions) == 0

    def test_sizes(self):
        from horizon._horizon import liquidity_drainer_action

        actions = liquidity_drainer_action("M1", 0.48, 0.52, 500.0)
        for a in actions:
            assert a.size == pytest.approx(500.0, abs=1e-10)


# ---------------------------------------------------------------------------
# 5. compute_simulation_metrics
# ---------------------------------------------------------------------------


class TestComputeSimMetrics:
    def test_basic(self):
        from horizon._horizon import compute_simulation_metrics

        baseline = [10.0, -5.0, 15.0, -2.0, 8.0]
        adversarial = [5.0, -10.0, 8.0, -5.0, 3.0]
        result = compute_simulation_metrics(baseline, adversarial)
        assert result.pnl_impact < 0.0  # adversarial is worse
        assert result.vulnerability_score > 0.0
        assert result.baseline.total_pnl == pytest.approx(26.0, abs=1e-10)
        assert result.baseline.num_trades == 5
        assert result.baseline.win_rate == pytest.approx(0.6, abs=1e-10)
        assert result.adversarial.total_pnl < result.baseline.total_pnl

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

        result = compute_simulation_metrics([], [])
        assert result.pnl_impact == pytest.approx(0.0, abs=1e-10)
        assert result.baseline.total_pnl == pytest.approx(0.0, abs=1e-10)

    def test_identical(self):
        from horizon._horizon import compute_simulation_metrics

        pnls = [10.0, -5.0, 15.0]
        result = compute_simulation_metrics(pnls, pnls.copy())
        assert result.pnl_impact == pytest.approx(0.0, abs=1e-10)

    def test_max_drawdown(self):
        from horizon._horizon import compute_simulation_metrics

        baseline = [10.0, -20.0, 5.0]
        result = compute_simulation_metrics(baseline, baseline)
        assert result.baseline.max_drawdown > 0.0


# ---------------------------------------------------------------------------
# 6. vulnerability_score
# ---------------------------------------------------------------------------


class TestVulnerabilityScore:
    def test_basic(self):
        from horizon._horizon import vulnerability_score

        score = vulnerability_score(1.5, 0.5, 1000.0, 200.0)
        assert 0.0 <= score <= 1.0

    def test_no_baseline(self):
        from horizon._horizon import vulnerability_score

        score = vulnerability_score(0.0, -0.5, 0.0, -100.0)
        assert score >= 0.0

    def test_identical_performance(self):
        from horizon._horizon import vulnerability_score

        score = vulnerability_score(1.5, 1.5, 1000.0, 1000.0)
        assert score == pytest.approx(0.0, abs=1e-10)

    def test_worse_adversarial(self):
        from horizon._horizon import vulnerability_score

        score = vulnerability_score(2.0, 0.5, 1000.0, 200.0)
        assert score > 0.0

    def test_clamped_to_one(self):
        from horizon._horizon import vulnerability_score

        # Much worse adversarial
        score = vulnerability_score(2.0, -5.0, 1000.0, -5000.0)
        assert score <= 1.0


# ---------------------------------------------------------------------------
# 7. AdversarialResult attributes
# ---------------------------------------------------------------------------


class TestAdversarialResult:
    def test_pnl_impact(self):
        from horizon._horizon import compute_simulation_metrics

        result = compute_simulation_metrics(
            [10.0, 5.0, 15.0],
            [5.0, 0.0, 10.0],
        )
        # Adversarial pnl = 15, baseline pnl = 30
        assert result.pnl_impact == pytest.approx(-15.0, abs=1e-10)

    def test_vulnerability(self):
        from horizon._horizon import compute_simulation_metrics

        result = compute_simulation_metrics(
            [10.0, 5.0, 15.0],
            [5.0, 0.0, 10.0],
        )
        assert 0.0 <= result.vulnerability_score <= 1.0

    def test_repr(self):
        from horizon._horizon import compute_simulation_metrics

        result = compute_simulation_metrics([1.0, 2.0], [0.5, 1.0])
        r = repr(result)
        assert "AdversarialResult" in r

    def test_adversary_config_creation(self):
        from horizon._horizon import AdversaryConfig, AdversaryType

        config = AdversaryConfig(AdversaryType.Whale, "M1", intensity=2.0, size_multiplier=5.0)
        assert config.adversary_type == AdversaryType.Whale
        assert config.market_id == "M1"
        assert config.intensity == pytest.approx(2.0, abs=1e-10)
        assert config.size_multiplier == pytest.approx(5.0, abs=1e-10)

    def test_simulation_metrics_creation(self):
        from horizon._horizon import SimulationMetrics

        m = SimulationMetrics(
            total_pnl=100.0, max_drawdown=20.0, sharpe_ratio=1.5,
            num_trades=50, win_rate=0.6, avg_slippage=0.01,
        )
        assert m.total_pnl == pytest.approx(100.0, abs=1e-10)
        assert m.max_drawdown == pytest.approx(20.0, abs=1e-10)
        assert m.sharpe_ratio == pytest.approx(1.5, abs=1e-10)
        assert m.num_trades == 50
        assert m.win_rate == pytest.approx(0.6, abs=1e-10)

    def test_simulation_metrics_defaults(self):
        from horizon._horizon import SimulationMetrics

        m = SimulationMetrics()
        assert m.total_pnl == pytest.approx(0.0, abs=1e-10)
        assert m.num_trades == 0
