"""Tests for ``arautopilot.studio.simulator.rudder_dynamics``."""

from __future__ import annotations

import pytest

from arautopilot.studio.simulator.rudder_dynamics import (
    RudderDynamicsConfig,
    RudderSimulator,
    RunRecorder,
)


def test_zero_input_stays_at_rest() -> None:
    sim = RudderSimulator()
    sim.reset(angle_deg=0.0)
    for _ in range(1000):
        sim.step(dt=0.001, pwm_pct=0.0)
    assert sim.state.angle_deg == pytest.approx(0.0, abs=1e-9)
    assert sim.state.angular_vel_dps == pytest.approx(0.0, abs=1e-9)


def test_full_starboard_command_drives_to_stbd_endstop() -> None:
    """With marine-realistic gain (v_max ~5 dps) reaching 35 deg takes ~8 s."""
    sim = RudderSimulator()
    sim.reset(angle_deg=0.0)
    for _ in range(15000):
        sim.step(dt=0.001, pwm_pct=100.0)
    assert sim.state.angle_deg == pytest.approx(sim.config.max_angle_deg, abs=1e-6)
    assert sim.state.angular_vel_dps == pytest.approx(0.0, abs=1e-6)


def test_full_port_command_drives_to_port_endstop() -> None:
    sim = RudderSimulator()
    sim.reset(angle_deg=0.0)
    for _ in range(15000):
        sim.step(dt=0.001, pwm_pct=-100.0)
    assert sim.state.angle_deg == pytest.approx(-sim.config.max_angle_deg, abs=1e-6)
    assert sim.state.angular_vel_dps == pytest.approx(0.0, abs=1e-6)


def test_command_inside_deadband_produces_no_motion() -> None:
    sim = RudderSimulator(RudderDynamicsConfig(deadband_pct=10.0,
                                                min_useful_pwm_pct=15.0))
    sim.reset(angle_deg=0.0)
    for _ in range(2000):
        sim.step(dt=0.001, pwm_pct=5.0)  # under deadband
    assert sim.state.angle_deg == pytest.approx(0.0, abs=1e-9)


def test_command_just_above_deadband_snaps_to_min_useful() -> None:
    """A command of 11% with deadband=10/min_useful=15 should act like 15%."""
    cfg = RudderDynamicsConfig(deadband_pct=10.0, min_useful_pwm_pct=15.0)
    sim_a = RudderSimulator(cfg)
    sim_b = RudderSimulator(cfg)
    sim_a.reset()
    sim_b.reset()
    for _ in range(3000):
        sim_a.step(dt=0.001, pwm_pct=11.0)
        sim_b.step(dt=0.001, pwm_pct=15.0)
    assert sim_a.state.angle_deg == pytest.approx(sim_b.state.angle_deg, abs=1e-9)


def test_asymmetry_makes_starboard_faster() -> None:
    cfg = RudderDynamicsConfig(asymmetry_stbd_over_port=1.5,
                                deadband_pct=0.0, min_useful_pwm_pct=0.0)
    sim_s = RudderSimulator(cfg)
    sim_p = RudderSimulator(cfg)
    sim_s.reset()
    sim_p.reset()
    for _ in range(2000):
        sim_s.step(dt=0.001, pwm_pct=+50.0)
        sim_p.step(dt=0.001, pwm_pct=-50.0)
    # Starboard angle should be larger in magnitude than port at the same
    # effort, because the pump pushes harder to starboard.
    assert abs(sim_s.state.angle_deg) > abs(sim_p.state.angle_deg)


def test_recorder_captures_trajectory() -> None:
    """Sanity check the recorder: capture a few seconds of constant-command
    motion and verify the samples reflect the rudder moving in the commanded
    direction with strictly monotonic position (no oscillation under constant
    drive)."""
    sim = RudderSimulator()
    sim.reset()
    rec = RunRecorder()
    for k in range(2000):
        t = k * 0.001
        sim.step(dt=0.001, pwm_pct=80.0)
        rec.record(t=t, setpoint_deg=25.0, sim=sim, pwm_pct=80.0)
    assert len(rec.samples) == 2000
    # Monotonically increasing angle (constant +ve command + viscous damping
    # means no overshoot during the run).
    assert rec.samples[-1].angle_deg > rec.samples[0].angle_deg
    assert all(rec.samples[i + 1].angle_deg >= rec.samples[i].angle_deg
               for i in range(len(rec.samples) - 1))


def test_invalid_dt_rejected() -> None:
    sim = RudderSimulator()
    with pytest.raises(ValueError):
        sim.step(dt=0.0, pwm_pct=50.0)
    with pytest.raises(ValueError):
        sim.step(dt=-0.001, pwm_pct=50.0)


def test_endstop_zeros_outward_velocity_only() -> None:
    """Hitting the stbd end-stop should kill +velocity but allow reverse."""
    sim = RudderSimulator()
    sim.reset(angle_deg=sim.config.max_angle_deg, angular_vel_dps=20.0)
    sim.step(dt=0.001, pwm_pct=0.0)
    assert sim.state.angle_deg == pytest.approx(sim.config.max_angle_deg, abs=1e-9)
    assert sim.state.angular_vel_dps == 0.0
    # Now reverse: should move away from the stop.
    for _ in range(200):
        sim.step(dt=0.001, pwm_pct=-80.0)
    assert sim.state.angle_deg < sim.config.max_angle_deg
