"""Tests for Garleanu-Pedersen optimal execution."""

import math
import pytest

from horizon._horizon import (
    GPTrajectory,
    gp_execution_cost,
    gp_optimal_trajectory,
    gp_urgency,
)


# ===========================================================================
# gp_urgency
# ===========================================================================


class TestGPUrgency:
    def test_basic(self):
        u = gp_urgency(0.5, 0.01, 1.0, 0.2)
        assert u > 0.0
        assert math.isfinite(u)

    def test_formula(self):
        # urgency = sqrt(alpha * risk * vol^2 / temp)
        alpha, temp, risk, vol = 0.5, 0.01, 1.0, 0.2
        expected = math.sqrt(alpha * risk * vol * vol / temp)
        result = gp_urgency(alpha, temp, risk, vol)
        assert result == pytest.approx(expected, rel=1e-6)

    def test_higher_alpha_higher_urgency(self):
        u1 = gp_urgency(0.1, 0.01, 1.0, 0.2)
        u2 = gp_urgency(1.0, 0.01, 1.0, 0.2)
        assert u2 > u1

    def test_higher_temp_impact_lower_urgency(self):
        u1 = gp_urgency(0.5, 0.01, 1.0, 0.2)
        u2 = gp_urgency(0.5, 0.1, 1.0, 0.2)
        assert u2 < u1

    def test_higher_risk_aversion_higher_urgency(self):
        u1 = gp_urgency(0.5, 0.01, 0.5, 0.2)
        u2 = gp_urgency(0.5, 0.01, 2.0, 0.2)
        assert u2 > u1

    def test_higher_volatility_higher_urgency(self):
        u1 = gp_urgency(0.5, 0.01, 1.0, 0.1)
        u2 = gp_urgency(0.5, 0.01, 1.0, 0.5)
        assert u2 > u1

    def test_zero_alpha_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(0.0, 0.01, 1.0, 0.2)

    def test_negative_alpha_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(-0.5, 0.01, 1.0, 0.2)

    def test_zero_temp_impact_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(0.5, 0.0, 1.0, 0.2)

    def test_zero_risk_aversion_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(0.5, 0.01, 0.0, 0.2)

    def test_zero_volatility_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(0.5, 0.01, 1.0, 0.0)

    def test_nan_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(float("nan"), 0.01, 1.0, 0.2)

    def test_inf_raises(self):
        with pytest.raises(ValueError):
            gp_urgency(float("inf"), 0.01, 1.0, 0.2)


# ===========================================================================
# gp_optimal_trajectory
# ===========================================================================


class TestGPOptimalTrajectory:
    def test_basic(self):
        traj = gp_optimal_trajectory(
            current_position=0.0,
            target_position=100.0,
            alpha_decay_rate=0.5,
            temporary_impact=0.01,
            permanent_impact=0.001,
            risk_aversion=1.0,
            n_steps=20,
            dt=0.1,
        )
        assert isinstance(traj, GPTrajectory)

    def test_result_fields(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert hasattr(traj, "times")
        assert hasattr(traj, "positions")
        assert hasattr(traj, "target")
        assert hasattr(traj, "rate")
        assert hasattr(traj, "total_cost")

    def test_trajectory_length(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        # n_steps + 1 (including initial)
        assert len(traj.times) == 21
        assert len(traj.positions) == 21

    def test_starts_at_current_position(self):
        traj = gp_optimal_trajectory(50.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert traj.positions[0] == pytest.approx(50.0)

    def test_target_stored(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert traj.target == pytest.approx(100.0)

    def test_rate_positive(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert traj.rate > 0.0

    def test_cost_non_negative(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert traj.total_cost >= 0.0

    def test_moves_toward_target(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.0, 1.0, 50, 0.1)
        # Position should increase toward target
        assert traj.positions[-1] > traj.positions[0]

    def test_zero_n_steps_raises(self):
        with pytest.raises(ValueError):
            gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 0, 0.1)

    def test_zero_dt_raises(self):
        with pytest.raises(ValueError):
            gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.0)

    def test_non_finite_current_raises(self):
        with pytest.raises(ValueError):
            gp_optimal_trajectory(float("inf"), 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)

    def test_negative_perm_impact_raises(self):
        with pytest.raises(ValueError):
            gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, -0.001, 1.0, 20, 0.1)

    def test_zero_perm_impact_ok(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.0, 1.0, 20, 0.1)
        assert len(traj.positions) == 21

    def test_time_increasing(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 10, 0.5)
        for i in range(len(traj.times) - 1):
            assert traj.times[i + 1] > traj.times[i]

    def test_sell_trajectory(self):
        traj = gp_optimal_trajectory(100.0, 0.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        # Position should decrease toward 0
        assert traj.positions[-1] < traj.positions[0]

    def test_same_position_cost_finite(self):
        # GP model's alpha decay dynamics can produce nonzero cost even
        # when current == target, so we just verify finite non-negative cost
        traj = gp_optimal_trajectory(50.0, 50.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        assert traj.total_cost >= 0.0
        assert math.isfinite(traj.total_cost)


# ===========================================================================
# gp_execution_cost
# ===========================================================================


class TestGPExecutionCost:
    def test_basic(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        cost = gp_execution_cost(traj, 0.01, 0.001)
        assert cost >= 0.0
        assert math.isfinite(cost)

    def test_zero_impact_zero_cost(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.0, 1.0, 20, 0.1)
        cost = gp_execution_cost(traj, 0.0, 0.0)
        assert cost == pytest.approx(0.0, abs=1e-10)

    def test_higher_impact_higher_cost(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        cost_lo = gp_execution_cost(traj, 0.001, 0.0)
        cost_hi = gp_execution_cost(traj, 0.1, 0.0)
        assert cost_hi >= cost_lo

    def test_negative_temp_impact_raises(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        with pytest.raises(ValueError):
            gp_execution_cost(traj, -0.01, 0.001)

    def test_negative_perm_impact_raises(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        with pytest.raises(ValueError):
            gp_execution_cost(traj, 0.01, -0.001)

    def test_nan_impact_raises(self):
        traj = gp_optimal_trajectory(0.0, 100.0, 0.5, 0.01, 0.001, 1.0, 20, 0.1)
        with pytest.raises(ValueError):
            gp_execution_cost(traj, float("nan"), 0.001)
