"""Regression tests for the bounded SEW retargeting differential."""

from pathlib import Path
import sys
import unittest

import numpy as np
import pinocchio as pin


CODE_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(CODE_ROOT))

from core.sew_mapper2 import (  # noqa: E402
    BallJointConfig,
    SEWMapper,
    _wrap_angle_delta,
)


def build_real_mapper() -> SEWMapper:
    config_dir = CODE_ROOT / "config"
    master_model = pin.buildModelFromUrdf(str(config_dir / "master_7dof.urdf"))
    slave_model = pin.buildModelFromUrdf(str(config_dir / "real_slave_7dof.urdf"))

    master_joints = (
        "master_shoulder_pitch_joint",
        "master_shoulder_yaw_joint",
        "master_shoulder_roll_joint",
        "master_elbow_flex_joint",
        "master_wrist_roll_joint",
        "master_wrist_yaw_joint",
        "master_wrist_pitch_joint",
    )
    slave_joints = (
        "R_SHOULDER_P",
        "R_SHOULDER_R",
        "R_SHOULDER_Y",
        "R_ELBOW_R",
        "R_WRIST_P",
        "R_WRIST_Y",
        "R_WRIST_R",
    )
    shoulder = BallJointConfig(
        axis_order="yxy",
        joint_names=("R_SHOULDER_P", "R_SHOULDER_R", "R_SHOULDER_Y"),
        signs=(-1.0, 1.0, -1.0),
    )
    wrist = BallJointConfig(
        axis_order="yzx",
        joint_names=("R_WRIST_P", "R_WRIST_Y", "R_WRIST_R"),
        signs=(-1.0, 1.0, 1.0),
    )
    return SEWMapper(
        master_model=master_model,
        slave_model=slave_model,
        m_shoulder="master_shoulder",
        m_elbow="master_forearm",
        m_wrist="master_wrist",
        m_ee="master_ee",
        s_shoulder="R_SHOULDER_R_S",
        s_elbow="R_ELBOW_R_S",
        s_wrist="R_WRIST_R_S",
        s_ee="R_WRIST_R_S",
        master_joint_names=master_joints,
        slave_joint_names=slave_joints,
        slave_shoulder_cfg=shoulder,
        slave_wrist_cfg=wrist,
    )


class TestRetargetingDifferential(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.mapper = build_real_mapper()
        # An interior, exactly recoverable pose on the real slave arm.
        cls.q_master = np.array(
            [0.534, 0.314, -0.1, 2.14, 0.38, 0.38, -0.72],
            dtype=float,
        )

    def test_virtual_work_identity(self) -> None:
        q_slave, A, debug = self.mapper.retarget_with_differential(self.q_master)

        self.assertTrue(debug["success"], debug["events"])
        self.assertTrue(
            debug["differential_valid"],
            debug["differential"]["events"],
        )
        self.assertFalse(debug["clipped"])
        self.assertFalse(debug["near_limit"])
        q_slave7 = self.mapper._slave_q7(q_slave)
        self.assertTrue(np.all(q_slave7 >= self.mapper.s_lower7))
        self.assertTrue(np.all(q_slave7 <= self.mapper.s_upper7))

        qdot_master = np.array(
            [0.21, -0.34, 0.17, 0.09, -0.11, 0.28, -0.07]
        )
        tau_slave = np.array(
            [-1.2, 0.4, 2.3, -0.7, 0.5, -1.1, 0.8]
        )
        qdot_slave = A @ qdot_master
        tau_master = A.T @ tau_slave
        self.assertAlmostEqual(
            float(tau_master @ qdot_master),
            float(tau_slave @ qdot_slave),
            places=12,
        )

    def test_matches_independent_directional_finite_difference(self) -> None:
        q_slave, A, debug = self.mapper.retarget_with_differential(
            self.q_master, fd_step=1e-4
        )
        self.assertTrue(debug["differential_valid"])

        direction = np.array([0.3, -0.2, 0.5, -0.1, 0.4, -0.25, 0.35])
        direction /= np.linalg.norm(direction)
        independent_step = 2.5e-5
        q_plus, plus_debug = self.mapper.retarget(
            self.q_master + independent_step * direction,
            q_s_init=q_slave,
        )
        q_minus, minus_debug = self.mapper.retarget(
            self.q_master - independent_step * direction,
            q_s_init=q_slave,
        )
        self.assertTrue(plus_debug["success"], plus_debug["events"])
        self.assertTrue(minus_debug["success"], minus_debug["events"])

        directional_fd = _wrap_angle_delta(
            self.mapper._slave_q7(q_plus) - self.mapper._slave_q7(q_minus)
        ) / (2.0 * independent_step)
        np.testing.assert_allclose(
            directional_fd,
            A @ direction,
            rtol=2e-4,
            atol=2e-5,
        )

    def test_degenerate_geometry_invalidates_differential(self) -> None:
        # Zero elbow flex makes the master shoulder-elbow-wrist plane undefined.
        q_degenerate = np.array([-np.pi / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        A, debug = self.mapper.compute_differential(q_degenerate)

        self.assertFalse(debug["valid"])
        self.assertTrue(np.isnan(A).all())
        self.assertIn("base_invalid", debug["events"])
        self.assertIn(
            "master_arm_plane_degenerate",
            debug["base"]["events"],
        )

    def test_active_limit_invalidates_differential(self) -> None:
        old_margin = self.mapper.joint_limit_margin
        try:
            # Enlarge only the diagnostic margin to exercise the active-set
            # safety gate without changing the bounded pose solution.
            self.mapper.joint_limit_margin = 10.0
            A, debug = self.mapper.compute_differential(self.q_master)
        finally:
            self.mapper.joint_limit_margin = old_margin

        self.assertTrue(debug["base"]["success"])
        self.assertTrue(debug["base"]["near_limit"])
        self.assertFalse(debug["valid"])
        self.assertTrue(np.isnan(A).all())
        self.assertIn("base_nonsmooth", debug["events"])

    def test_angle_difference_wrap(self) -> None:
        raw = np.array([2.0 * np.pi - 0.1, -2.0 * np.pi + 0.2, np.pi])
        np.testing.assert_allclose(
            _wrap_angle_delta(raw),
            np.array([-0.1, 0.2, -np.pi]),
            atol=1e-14,
        )


if __name__ == "__main__":
    unittest.main()
