"""Tests for ``arautopilot.core.pid_config``."""

from __future__ import annotations

import pytest
from pydantic import ValidationError

from arautopilot.core.pid_config import (
    AccessLevel,
    GainSchedulePoint,
    PidConfig,
    PidGains,
    interpolate_gains,
)


def _base_kwargs(**overrides: object) -> dict[str, object]:
    base = {
        "inner_loop_base": PidGains(kp=2.5, ki=0.15, kd=0.30),
        "outer_loop_base": PidGains(kp=0.90, ki=0.02, kd=1.20),
    }
    base.update(overrides)
    return base


def test_basic_config_validates() -> None:
    cfg = PidConfig(**_base_kwargs())  # type: ignore[arg-type]
    assert cfg.inner_loop_freq_hz > cfg.outer_loop_freq_hz
    assert cfg.adaptive_max_deviation_pct == 50.0


def test_inner_must_be_faster_than_outer() -> None:
    with pytest.raises(ValidationError):
        PidConfig(**_base_kwargs(inner_loop_freq_hz=10.0, outer_loop_freq_hz=10.0))  # type: ignore[arg-type]
    with pytest.raises(ValidationError):
        PidConfig(**_base_kwargs(inner_loop_freq_hz=5.0, outer_loop_freq_hz=10.0))  # type: ignore[arg-type]


def test_negative_gains_rejected() -> None:
    with pytest.raises(ValidationError):
        PidGains(kp=-1.0)


def test_gain_schedule_must_be_sorted_by_speed() -> None:
    unsorted = [
        GainSchedulePoint(speed_knots=15.0, gains=PidGains(kp=0.9)),
        GainSchedulePoint(speed_knots=5.0,  gains=PidGains(kp=1.2)),
    ]
    with pytest.raises(ValidationError):
        PidConfig(**_base_kwargs(gain_schedule=unsorted))  # type: ignore[arg-type]


def test_gain_schedule_rejects_duplicate_speeds() -> None:
    dup = [
        GainSchedulePoint(speed_knots=10.0, gains=PidGains(kp=0.9)),
        GainSchedulePoint(speed_knots=10.0, gains=PidGains(kp=1.0)),
    ]
    with pytest.raises(ValidationError):
        PidConfig(**_base_kwargs(gain_schedule=dup))  # type: ignore[arg-type]


def test_interpolate_at_endpoints() -> None:
    schedule = [
        GainSchedulePoint(speed_knots=5.0,  gains=PidGains(kp=1.20, ki=0.03, kd=0.80)),
        GainSchedulePoint(speed_knots=28.0, gains=PidGains(kp=0.55, ki=0.01, kd=1.80)),
    ]
    lo = interpolate_gains(schedule, 5.0)
    hi = interpolate_gains(schedule, 28.0)
    assert lo == schedule[0].gains
    assert hi == schedule[1].gains


def test_interpolate_holds_below_and_above_range() -> None:
    schedule = [
        GainSchedulePoint(speed_knots=5.0,  gains=PidGains(kp=1.20)),
        GainSchedulePoint(speed_knots=28.0, gains=PidGains(kp=0.55)),
    ]
    assert interpolate_gains(schedule, 0.0).kp == pytest.approx(1.20)
    assert interpolate_gains(schedule, 100.0).kp == pytest.approx(0.55)


def test_interpolate_linear_midpoint() -> None:
    schedule = [
        GainSchedulePoint(speed_knots=5.0,  gains=PidGains(kp=1.00, ki=0.04, kd=0.80)),
        GainSchedulePoint(speed_knots=15.0, gains=PidGains(kp=0.50, ki=0.02, kd=1.20)),
    ]
    # Midpoint at 10 kn should be the midpoint of every gain.
    mid = interpolate_gains(schedule, 10.0)
    assert mid.kp == pytest.approx(0.75)
    assert mid.ki == pytest.approx(0.03)
    assert mid.kd == pytest.approx(1.00)


def test_interpolate_empty_raises() -> None:
    with pytest.raises(ValueError):
        interpolate_gains([], 10.0)


def test_adaptive_bound_enforces_50_percent_envelope() -> None:
    """Brief section 6: 'ganancias adaptativas nunca salen de ±50% respecto a las base'."""
    cfg = PidConfig(**_base_kwargs(adaptive_max_deviation_pct=50.0))  # type: ignore[arg-type]
    base = cfg.outer_loop_base  # kp=0.90, ki=0.02, kd=1.20

    # Within bound
    assert cfg.is_within_adaptive_bound(PidGains(kp=base.kp * 1.49, ki=base.ki, kd=base.kd))
    assert cfg.is_within_adaptive_bound(PidGains(kp=base.kp * 0.51, ki=base.ki, kd=base.kd))

    # Outside bound
    assert not cfg.is_within_adaptive_bound(PidGains(kp=base.kp * 1.51, ki=base.ki, kd=base.kd))
    assert not cfg.is_within_adaptive_bound(PidGains(kp=base.kp * 0.49, ki=base.ki, kd=base.kd))


def test_adaptive_bound_with_zero_base_requires_zero_candidate() -> None:
    cfg = PidConfig(
        **_base_kwargs(  # type: ignore[arg-type]
            outer_loop_base=PidGains(kp=1.0, ki=0.0, kd=0.0),
        )
    )
    assert cfg.is_within_adaptive_bound(PidGains(kp=1.0, ki=0.0, kd=0.0))
    assert not cfg.is_within_adaptive_bound(PidGains(kp=1.0, ki=0.01, kd=0.0))


def test_adaptive_max_deviation_capped_at_50() -> None:
    # Brief says ±50% is the hard ceiling. The model should refuse higher.
    with pytest.raises(ValidationError):
        PidConfig(**_base_kwargs(adaptive_max_deviation_pct=60.0))  # type: ignore[arg-type]


def test_access_level_enum_has_three_levels() -> None:
    assert {a.value for a in AccessLevel} == {"operator", "technician", "integrator"}
