"""Tests for constraint residual generation."""

import math

import pytest
from kindred_solver.constraints import (
    CoincidentConstraint,
    DistancePointPointConstraint,
    FixedConstraint,
)
from kindred_solver.entities import RigidBody
from kindred_solver.params import ParamTable


class TestCoincident:
    def test_satisfied(self):
        """Two bodies at same position → residuals are zero."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (0, 0, 0), (1, 0, 0, 0))
        c = CoincidentConstraint(b1, (0, 0, 0), b2, (0, 0, 0))
        env = pt.get_env()
        for r in c.residuals():
            assert abs(r.eval(env)) < 1e-10

    def test_unsatisfied(self):
        """Bodies at different positions → non-zero residuals."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (1, 2, 3), (1, 0, 0, 0))
        c = CoincidentConstraint(b1, (0, 0, 0), b2, (0, 0, 0))
        env = pt.get_env()
        vals = [r.eval(env) for r in c.residuals()]
        assert abs(vals[0] - (-1.0)) < 1e-10
        assert abs(vals[1] - (-2.0)) < 1e-10
        assert abs(vals[2] - (-3.0)) < 1e-10

    def test_with_markers(self):
        """Marker offsets shift the coincidence point."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (10, 0, 0), (1, 0, 0, 0))
        # marker on b1 at local (5,0,0), marker on b2 at local (-5,0,0)
        c = CoincidentConstraint(b1, (5, 0, 0), b2, (-5, 0, 0))
        env = pt.get_env()
        # b1 marker world = (5,0,0), b2 marker world = (10-5, 0, 0) = (5,0,0)
        for r in c.residuals():
            assert abs(r.eval(env)) < 1e-10

    def test_residual_count(self):
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0))
        b2 = RigidBody("b", pt, (0, 0, 0), (1, 0, 0, 0))
        c = CoincidentConstraint(b1, (0, 0, 0), b2, (0, 0, 0))
        assert len(c.residuals()) == 3


class TestDistancePointPoint:
    def test_satisfied(self):
        """Bodies at distance 5 apart with d=5 → residual zero."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (3, 4, 0), (1, 0, 0, 0))
        c = DistancePointPointConstraint(b1, (0, 0, 0), b2, (0, 0, 0), 5.0)
        env = pt.get_env()
        # 3^2 + 4^2 - 5^2 = 9+16-25 = 0
        assert abs(c.residuals()[0].eval(env)) < 1e-10

    def test_unsatisfied(self):
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (3, 4, 0), (1, 0, 0, 0))
        c = DistancePointPointConstraint(b1, (0, 0, 0), b2, (0, 0, 0), 3.0)
        env = pt.get_env()
        # 9+16-9 = 16
        assert abs(c.residuals()[0].eval(env) - 16.0) < 1e-10

    def test_residual_count(self):
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0))
        b2 = RigidBody("b", pt, (0, 0, 0), (1, 0, 0, 0))
        c = DistancePointPointConstraint(b1, (0, 0, 0), b2, (0, 0, 0), 1.0)
        assert len(c.residuals()) == 1


class TestFixed:
    def test_satisfied(self):
        """Identical pose + markers → all residuals zero."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (0, 0, 0), (1, 0, 0, 0))
        c = FixedConstraint(
            b1,
            (0, 0, 0),
            (1, 0, 0, 0),
            b2,
            (0, 0, 0),
            (1, 0, 0, 0),
        )
        env = pt.get_env()
        for r in c.residuals():
            assert abs(r.eval(env)) < 1e-10

    def test_residual_count(self):
        """Fixed constraint: 3 position + 3 orientation = 6 residuals."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0))
        b2 = RigidBody("b", pt, (0, 0, 0), (1, 0, 0, 0))
        c = FixedConstraint(
            b1,
            (0, 0, 0),
            (1, 0, 0, 0),
            b2,
            (0, 0, 0),
            (1, 0, 0, 0),
        )
        assert len(c.residuals()) == 6

    def test_unsatisfied_position(self):
        """Offset position → non-zero position residuals."""
        pt = ParamTable()
        b1 = RigidBody("a", pt, (0, 0, 0), (1, 0, 0, 0), grounded=True)
        b2 = RigidBody("b", pt, (1, 0, 0), (1, 0, 0, 0))
        c = FixedConstraint(
            b1,
            (0, 0, 0),
            (1, 0, 0, 0),
            b2,
            (0, 0, 0),
            (1, 0, 0, 0),
        )
        env = pt.get_env()
        vals = [r.eval(env) for r in c.residuals()]
        # Position residual [0] should be -1 (0 - 1)
        assert abs(vals[0] - (-1.0)) < 1e-10
