from contextlib import nullcontext as DoesNotRaise

import numpy as np
import pytest

from supervision.key_points.core import KeyPoints
from tests.helpers import (
    _create_key_points,
    _FakeMediapipeLandmark,
    _FakeMediapipePose,
    _FakeMediapipeResults,
    _FakeYoloNasKeyPoint,
    _FakeYoloNasKeyPointResults,
)

KEY_POINTS = _create_key_points(
    xy=[
        [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
        [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
        [[20, 21], [22, 23], [24, 25], [26, 27], [28, 29]],
    ],
    confidence=[
        [0.8, 0.2, 0.6, 0.1, 0.5],
        [0.7, 0.9, 0.3, 0.4, 0.0],
        [0.1, 0.6, 0.8, 0.2, 0.7],
    ],
    class_id=[0, 1, 2],
)


@pytest.mark.parametrize(
    ("key_points", "index", "expected_result", "exception"),
    [
        (
            KeyPoints.empty(),
            slice(None),
            KeyPoints.empty(),
            DoesNotRaise(),
        ),  # slice all key points when key points object empty
        (
            KEY_POINTS,
            slice(None),
            KEY_POINTS,
            DoesNotRaise(),
        ),  # slice all key points when key points object nonempty
        (
            KEY_POINTS,
            slice(0, 1),
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]],
                confidence=[[0.8, 0.2, 0.6, 0.1, 0.5]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # select the first skeleton by slice
        (
            KEY_POINTS,
            slice(0, 2),
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
                    [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6, 0.1, 0.5],
                    [0.7, 0.9, 0.3, 0.4, 0.0],
                ],
                class_id=[0, 1],
            ),
            DoesNotRaise(),
        ),  # select the first skeleton by slice
        (
            KEY_POINTS,
            0,
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]],
                confidence=[[0.8, 0.2, 0.6, 0.1, 0.5]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # select the first skeleton by index
        (
            KEY_POINTS,
            -1,
            _create_key_points(
                xy=[[[20, 21], [22, 23], [24, 25], [26, 27], [28, 29]]],
                confidence=[[0.1, 0.6, 0.8, 0.2, 0.7]],
                class_id=[2],
            ),
            DoesNotRaise(),
        ),  # select the last skeleton by index
        (
            KEY_POINTS,
            [0, 1],
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
                    [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6, 0.1, 0.5],
                    [0.7, 0.9, 0.3, 0.4, 0.0],
                ],
                class_id=[0, 1],
            ),
            DoesNotRaise(),
        ),  # select the first two skeletons by index; list
        (
            KEY_POINTS,
            np.array([0, 1]),
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
                    [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6, 0.1, 0.5],
                    [0.7, 0.9, 0.3, 0.4, 0.0],
                ],
                class_id=[0, 1],
            ),
            DoesNotRaise(),
        ),  # select the first two skeletons by index; np.array
        (
            KEY_POINTS,
            [True, True, False],
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
                    [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6, 0.1, 0.5],
                    [0.7, 0.9, 0.3, 0.4, 0.0],
                ],
                class_id=[0, 1],
            ),
            DoesNotRaise(),
        ),  # select only skeletons associated with positive filter; list
        (
            KEY_POINTS,
            np.array([True, True, False]),
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
                    [[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6, 0.1, 0.5],
                    [0.7, 0.9, 0.3, 0.4, 0.0],
                ],
                class_id=[0, 1],
            ),
            DoesNotRaise(),
        ),  # select only skeletons associated with positive filter; list
        (
            KEY_POINTS,
            (slice(None), slice(None)),
            KEY_POINTS,
            DoesNotRaise(),
        ),  # slice all anchors from all skeletons
        (
            KEY_POINTS,
            (slice(None), slice(0, 1)),
            _create_key_points(
                xy=[[[0, 1]], [[10, 11]], [[20, 21]]],
                confidence=[[0.8], [0.7], [0.1]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # slice the first anchor from every skeleton
        (
            KEY_POINTS,
            (slice(None), slice(0, 2)),
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
                confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # slice the first anchor two anchors from every skeleton
        (
            KEY_POINTS,
            (slice(None), 0),
            _create_key_points(
                xy=[[[0, 1]], [[10, 11]], [[20, 21]]],
                confidence=[[0.8], [0.7], [0.1]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select the first anchor from every skeleton by index
        (
            KEY_POINTS,
            (slice(None), -1),
            _create_key_points(
                xy=[[[8, 9]], [[18, 19]], [[28, 29]]],
                confidence=[[0.5], [0.0], [0.7]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select the last anchor from every skeleton by index
        (
            KEY_POINTS,
            (slice(None), [0, 1]),
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
                confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select the first two anchors from every skeleton by index; list
        (
            KEY_POINTS,
            (slice(None), np.array([0, 1])),
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
                confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select the first two anchors from every skeleton by index; np.array
        (
            KEY_POINTS,
            (slice(None), [True, True, False, False, False]),
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
                confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select only anchors associated with positive filter; list
        (
            KEY_POINTS,
            (slice(None), np.array([True, True, False, False, False])),
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
                confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # select only anchors associated with positive filter; np.array
        (
            KEY_POINTS,
            (0, 0),
            _create_key_points(xy=[[[0, 1]]], confidence=[[0.8]], class_id=[0]),
            DoesNotRaise(),
        ),  # select the first anchor from the first skeleton by index
        (
            KEY_POINTS,
            (0, -1),
            _create_key_points(xy=[[[8, 9]]], confidence=[[0.5]], class_id=[0]),
            DoesNotRaise(),
        ),  # select the last anchor from the first skeleton by index
        (
            KEY_POINTS,
            np.array(
                [
                    [True, False, True, False, False],
                    [True, True, False, False, False],
                    [False, True, True, False, False],
                ]
            ),
            _create_key_points(
                xy=[
                    [[0, 1], [4, 5]],
                    [[10, 11], [12, 13]],
                    [[22, 23], [24, 25]],
                ],
                confidence=[[0.8, 0.6], [0.7, 0.9], [0.6, 0.8]],
                class_id=[0, 1, 2],
            ),
            DoesNotRaise(),
        ),  # filter keypoints by 2D boolean mask, same count per row
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
            ),
            np.array([[True, False, True]]),
            _create_key_points(
                xy=[[[0, 1], [4, 5]]],
                confidence=[[0.8, 0.6]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # filter keypoints by 2D boolean mask, single object
        (
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5]],
                    [[10, 11], [12, 13], [14, 15]],
                ],
                confidence=[
                    [0.8, 0.2, 0.6],
                    [0.1, 0.2, 0.3],
                ],
                class_id=[0, 1],
            ),
            np.array([[True, False, True], [False, False, False]]),
            None,
            pytest.raises(ValueError, match="different numbers of True values"),
        ),  # 2D boolean mask with different counts per row raises ValueError
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                class_id=[0],
            ),
            np.array([[True, False, True]]),
            _create_key_points(
                xy=[[[0, 1], [4, 5]]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # 2D boolean mask with confidence=None — no confidence array in result
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
            ),
            np.array([[True, False]]),
            None,
            pytest.raises(ValueError, match="column count"),
        ),  # 2D boolean mask column count mismatch raises ValueError
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
            ),
            np.array([[True, False, True], [True, False, True]]),
            None,
            pytest.raises(ValueError, match="row count"),
        ),  # 2D boolean mask row count mismatch raises ValueError
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3]], [[4, 5], [6, 7]]],
                confidence=[[0.8, 0.2], [0.6, 0.9]],
                class_id=[0, 1],
            ),
            np.array([[False, False], [False, False]]),
            KeyPoints(
                xy=np.zeros((2, 0, 2), dtype=np.float32),
                keypoint_confidence=np.zeros((2, 0), dtype=np.float32),
                class_id=np.array([0, 1]),
            ),
            DoesNotRaise(),
        ),  # all-False 2D mask — all rows select 0 keypoints, equal counts → ok
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
            ),
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
            ).keypoint_confidence
            > 0.5,
            _create_key_points(
                xy=[[[0, 1], [4, 5]]],
                confidence=[[0.8, 0.6]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # kp[kp.confidence > 0.5] — single-object canonical use case
    ],
)
def test_key_points_getitem(key_points, index, expected_result, exception):
    with exception:
        result = key_points[index]
        assert result == expected_result


def test_key_points_select_returns_subset() -> None:
    """Select returns a typed KeyPoints subset for row indexes."""
    result = KEY_POINTS.select([0, 2])

    assert result == _create_key_points(
        xy=[
            [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
            [[20, 21], [22, 23], [24, 25], [26, 27], [28, 29]],
        ],
        confidence=[
            [0.8, 0.2, 0.6, 0.1, 0.5],
            [0.1, 0.6, 0.8, 0.2, 0.7],
        ],
        class_id=[0, 2],
    )


def test_key_points_get_data_returns_data_value() -> None:
    """Get data returns the stored data value or None."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]]],
        confidence=[[0.8, 0.2]],
        class_id=[0],
    )
    key_points["custom_data"] = ["value1"]

    assert key_points.get_data("custom_data") == ["value1"]
    assert key_points.get_data("missing") is None


KEY_POINTS_WITH_DET_CONF = _create_key_points(
    xy=[
        [[0, 1], [2, 3], [4, 5]],
        [[10, 11], [12, 13], [14, 15]],
        [[20, 21], [22, 23], [24, 25]],
    ],
    confidence=[
        [0.8, 0.2, 0.6],
        [0.7, 0.9, 0.3],
        [0.1, 0.6, 0.8],
    ],
    class_id=[0, 1, 0],
    detection_confidence=[0.95, 0.40, 0.85],
)


@pytest.mark.parametrize(
    ("key_points", "index", "expected_result"),
    [
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.detection_confidence > 0.5,
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5]],
                    [[20, 21], [22, 23], [24, 25]],
                ],
                confidence=[[0.8, 0.2, 0.6], [0.1, 0.6, 0.8]],
                class_id=[0, 0],
                detection_confidence=[0.95, 0.85],
            ),
            id="filter-by-detection-confidence-threshold",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.class_id == 0,
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5]],
                    [[20, 21], [22, 23], [24, 25]],
                ],
                confidence=[[0.8, 0.2, 0.6], [0.1, 0.6, 0.8]],
                class_id=[0, 0],
                detection_confidence=[0.95, 0.85],
            ),
            id="filter-by-class-id",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.class_id == 1,
            _create_key_points(
                xy=[[[10, 11], [12, 13], [14, 15]]],
                confidence=[[0.7, 0.9, 0.3]],
                class_id=[1],
                detection_confidence=[0.40],
            ),
            id="filter-by-class-id-single-result",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            (KEY_POINTS_WITH_DET_CONF.detection_confidence > 0.5)
            & (KEY_POINTS_WITH_DET_CONF.class_id == 0),
            _create_key_points(
                xy=[
                    [[0, 1], [2, 3], [4, 5]],
                    [[20, 21], [22, 23], [24, 25]],
                ],
                confidence=[[0.8, 0.2, 0.6], [0.1, 0.6, 0.8]],
                class_id=[0, 0],
                detection_confidence=[0.95, 0.85],
            ),
            id="filter-by-detection-confidence-and-class-id",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.detection_confidence > 0.99,
            KeyPoints(
                xy=np.zeros((0, 3, 2), dtype=np.float32),
                keypoint_confidence=np.zeros((0, 3), dtype=np.float32),
                detection_confidence=np.array([], dtype=np.float32),
                class_id=np.array([], dtype=int),
            ),
            id="filter-all-out-returns-empty",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.class_id == 99,
            KeyPoints(
                xy=np.zeros((0, 3, 2), dtype=np.float32),
                keypoint_confidence=np.zeros((0, 3), dtype=np.float32),
                detection_confidence=np.array([], dtype=np.float32),
                class_id=np.array([], dtype=int),
            ),
            id="filter-by-nonexistent-class-returns-empty",
        ),
        pytest.param(
            KeyPoints.empty(),
            np.array([], dtype=bool),
            KeyPoints.empty(),
            id="filter-empty-keypoints-stays-empty",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            KEY_POINTS_WITH_DET_CONF.detection_confidence > 0.0,
            KEY_POINTS_WITH_DET_CONF,
            id="filter-keeps-all-when-all-pass",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            np.int64(0),
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
                detection_confidence=[0.95],
            ),
            id="np-integer-scalar-with-det-conf",
        ),
        pytest.param(
            KEY_POINTS_WITH_DET_CONF,
            np.array(0),
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.2, 0.6]],
                class_id=[0],
                detection_confidence=[0.95],
            ),
            id="0d-ndarray-with-det-conf",
        ),
    ],
)
def test_key_points_getitem_detection_level(key_points, index, expected_result):
    """Detection-level filtering mirrors Detections API patterns."""
    result = key_points[index]
    assert result == expected_result


class TestKeyPointsVisible:
    """Tests for the `visible` mask field on KeyPoints."""

    def test_visible_defaults_to_none(self):
        kp = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            confidence=[[0.9, 0.8]],
            class_id=[0],
        )
        assert kp.visible is None

    def test_visible_set_from_confidence_threshold(self):
        kp = _create_key_points(
            xy=[[[0, 1], [2, 3], [4, 5]]],
            confidence=[[0.9, 0.1, 0.6]],
            class_id=[0],
        )
        kp.visible = kp.keypoint_confidence > 0.5
        expected = np.array([[True, False, True]])
        np.testing.assert_array_equal(kp.visible, expected)

    def test_visible_preserved_on_skeleton_filter(self):
        kp = _create_key_points(
            xy=[
                [[0, 1], [2, 3]],
                [[10, 11], [12, 13]],
            ],
            confidence=[[0.9, 0.1], [0.3, 0.8]],
            class_id=[0, 1],
            detection_confidence=[0.95, 0.40],
            visible=[[True, False], [False, True]],
        )
        result = kp[kp.detection_confidence > 0.5]
        assert result.visible is not None
        np.testing.assert_array_equal(result.visible, np.array([[True, False]]))

    def test_visible_preserved_on_int_index(self):
        kp = _create_key_points(
            xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]]],
            confidence=[[0.9, 0.1], [0.3, 0.8]],
            class_id=[0, 1],
            visible=[[True, False], [False, True]],
        )
        result = kp[0]
        assert result.visible is not None
        np.testing.assert_array_equal(result.visible, np.array([[True, False]]))

    def test_visible_preserved_on_anchor_slice(self):
        kp = _create_key_points(
            xy=[[[0, 1], [2, 3], [4, 5]]],
            confidence=[[0.9, 0.1, 0.6]],
            class_id=[0],
            visible=[[True, False, True]],
        )
        result = kp[:, [0, 2]]
        assert result.visible is not None
        np.testing.assert_array_equal(result.visible, np.array([[True, True]]))

    def test_visible_preserved_on_2d_bool_mask(self):
        kp = _create_key_points(
            xy=[
                [[0, 1], [2, 3], [4, 5]],
                [[10, 11], [12, 13], [14, 15]],
            ],
            confidence=[[0.9, 0.1, 0.6], [0.7, 0.2, 0.8]],
            class_id=[0, 1],
            visible=[[True, False, True], [True, False, True]],
        )
        mask = np.array([[True, False, True], [True, False, True]])
        result = kp[mask]
        assert result.visible is not None
        np.testing.assert_array_equal(
            result.visible, np.array([[True, True], [True, True]])
        )

    def test_visible_none_stays_none_on_filter(self):
        kp = _create_key_points(
            xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]]],
            class_id=[0, 1],
        )
        result = kp[0]
        assert result.visible is None

    def test_equality_with_visible(self):
        kp1 = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            class_id=[0],
            visible=[[True, False]],
        )
        kp2 = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            class_id=[0],
            visible=[[True, False]],
        )
        kp3 = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            class_id=[0],
            visible=[[False, True]],
        )
        assert kp1 == kp2
        assert kp1 != kp3

    def test_equality_visible_none_vs_set(self):
        kp1 = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            class_id=[0],
        )
        kp2 = _create_key_points(
            xy=[[[0, 1], [2, 3]]],
            class_id=[0],
            visible=[[True, True]],
        )
        assert kp1 != kp2


def test_key_points_empty():
    """Test the creation and behavior of an empty KeyPoints object."""
    empty_key_points = KeyPoints.empty()
    assert len(empty_key_points) == 0
    assert empty_key_points.is_empty()
    assert empty_key_points.xy.shape == (0, 0, 2)


def test_key_points_is_empty():
    """Test the is_empty method for KeyPoints objects."""
    empty_key_points = KeyPoints.empty()
    assert empty_key_points.is_empty()

    non_empty_key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]]],
        confidence=[[0.8, 0.9]],
        class_id=[0],
    )
    assert not non_empty_key_points.is_empty()


def test_key_points_zero_length_slice_is_empty():
    """A filtered KeyPoints object with zero rows is empty."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]]],
        confidence=[[0.8, 0.9]],
        class_id=[0],
    )

    assert key_points[np.array([], dtype=int)].is_empty()


def test_key_points_setitem():
    """Test the __setitem__ method for KeyPoints objects."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]]],
        confidence=[[0.8, 0.9]],
        class_id=[0],
    )

    key_points["custom_data"] = ["value1"]
    assert "custom_data" in key_points.data
    assert np.array_equal(key_points.data["custom_data"], np.array(["value1"]))

    with pytest.raises(TypeError, match=r"Value must be a np\.ndarray or a list"):
        key_points["invalid_data"] = 123


@pytest.mark.parametrize(
    ("key_points", "expected_xyxy", "expected_confidence", "expected_class_id"),
    [
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.9, 0.7]],
                class_id=[0],
            ),
            np.array([[0, 1, 4, 5]], dtype=np.float32),
            np.array([0.8], dtype=np.float32),
            np.array([0]),
        ),
        (
            _create_key_points(
                xy=[[[0, 0], [2, 3], [4, 5]]],
                confidence=[[0.8, 0.9, 0.7]],
                class_id=[0],
            ),
            np.array([[2, 3, 4, 5]], dtype=np.float32),
            np.array([0.8], dtype=np.float32),
            np.array([0]),
        ),
    ],
)
def test_key_points_as_detections(
    key_points, expected_xyxy, expected_confidence, expected_class_id
):
    """Test the as_detections method for KeyPoints objects."""
    detections = key_points.as_detections()
    assert len(detections) == len(expected_xyxy)
    assert np.array_equal(detections.xyxy, expected_xyxy)
    assert np.allclose(detections.confidence, expected_confidence)
    assert np.array_equal(detections.class_id, expected_class_id)


def test_key_points_as_detections_empty():
    """Test the as_detections method for empty KeyPoints objects."""
    empty_key_points = KeyPoints.empty()
    empty_detections = empty_key_points.as_detections()
    assert empty_detections.is_empty()


def test_key_points_as_detections_ignores_missing_keypoints():
    """A [0, 0] keypoint is treated as missing and excluded from the box."""
    key_points = _create_key_points(
        xy=[[[0, 0], [10, 20], [30, 40]]],
        confidence=[[0.0, 0.8, 0.6]],
        class_id=[0],
    )

    detections = key_points.as_detections()

    assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))


def test_key_points_as_detections_uses_detection_confidence():
    """detection_confidence is preferred over the keypoint-confidence mean."""
    key_points = _create_key_points(
        xy=[[[10, 20], [30, 40]]],
        confidence=[[0.1, 0.2]],
        class_id=[0],
        detection_confidence=[0.95],
    )

    detections = key_points.as_detections()

    assert np.allclose(detections.confidence, np.array([0.95], dtype=np.float32))


def test_key_points_as_detections_selected_keypoint_indices():
    """Only the selected keypoints contribute to the bounding box."""
    key_points = _create_key_points(
        xy=[[[0, 0], [10, 20], [30, 40], [100, 100]]],
        confidence=[[0.5, 0.8, 0.6, 0.9]],
        class_id=[0],
    )

    detections = key_points.as_detections(selected_keypoint_indices=[1, 2])

    assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))


def test_key_points_as_detections_confidence_over_selected_indices():
    """Confidence mean uses only the selected keypoint columns, not all."""
    key_points = _create_key_points(
        xy=[[[0, 0], [10, 20], [30, 40], [100, 100]]],
        confidence=[[0.5, 0.8, 0.6, 0.9]],
        class_id=[0],
    )

    detections = key_points.as_detections(selected_keypoint_indices=[1, 2])

    expected_confidence = np.mean([0.8, 0.6], dtype=np.float32)
    assert np.isclose(detections.confidence[0], expected_confidence)


def test_key_points_as_detections_mixed_valid_invalid_batch():
    """Batch with one all-zero skeleton: invalid skeleton gets box zeroed."""
    key_points = _create_key_points(
        xy=[[[0, 0], [0, 0]], [[10, 20], [30, 40]]],
        confidence=[[0.0, 0.0], [0.8, 0.6]],
        class_id=[0, 1],
    )

    detections = key_points.as_detections()

    # Only the skeleton with at least one valid keypoint survives
    assert len(detections) == 1
    assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))


def test_key_points_getitem_empty_list():
    """Selecting with an empty list returns an empty KeyPoints, like Detections."""
    key_points = _create_key_points(
        xy=[[[1, 2], [3, 4]], [[5, 6], [7, 8]]],
        class_id=[0, 1],
    )

    result = key_points[[]]

    assert len(result) == 0
    assert result.is_empty()


@pytest.mark.parametrize(
    "selected_keypoint_indices",
    [
        pytest.param(np.array([0, 1]), id="numpy-array"),
        pytest.param(iter([0, 1]), id="generator"),
        pytest.param((0, 1), id="tuple"),
    ],
)
def test_key_points_as_detections_index_container_types(selected_keypoint_indices):
    """selected_keypoint_indices accepts any Iterable[int] container type."""
    key_points = _create_key_points(
        xy=[[[10, 10], [20, 20], [30, 15]]],
        class_id=[0],
    )

    detections = key_points.as_detections(
        selected_keypoint_indices=selected_keypoint_indices,
    )

    assert np.array_equal(detections.xyxy, np.array([[10, 10, 20, 20]]))


def test_key_points_as_detections_empty_indices_selects_all():
    """An empty selected_keypoint_indices behaves like None (selects all)."""
    key_points = _create_key_points(
        xy=[[[10, 10], [20, 20], [30, 15]]],
        confidence=[[0.1, 0.3, 0.5]],
        class_id=[0],
        detection_confidence=[0.9],
    )
    key_points["custom_data"] = ["person"]

    all_selected = key_points.as_detections()
    empty_list_selected = key_points.as_detections(selected_keypoint_indices=[])

    assert np.array_equal(all_selected.xyxy, empty_list_selected.xyxy)
    assert np.array_equal(all_selected.confidence, empty_list_selected.confidence)
    assert np.array_equal(all_selected.class_id, empty_list_selected.class_id)
    assert np.array_equal(
        all_selected.data["custom_data"], empty_list_selected.data["custom_data"]
    )


@pytest.mark.parametrize(
    ("xy", "expected_xyxy"),
    [
        pytest.param(
            [[[10, 20], [30, 20]]],
            np.array([[10, 20, 30, 20]]),
            id="collinear-keypoints",
        ),
        pytest.param(
            [[[15, 25]]],
            np.array([[15, 25, 15, 25]]),
            id="single-keypoint",
        ),
    ],
)
def test_key_points_as_detections_keeps_degenerate_skeletons(xy, expected_xyxy):
    """A skeleton with valid keypoints keeps its box even when the area is zero."""
    key_points = _create_key_points(xy=xy, class_id=[0])

    detections = key_points.as_detections()

    assert len(detections) == 1
    assert np.array_equal(detections.xyxy, expected_xyxy)


def test_key_points_as_detections_filters_invalid_and_aligns_degenerate_metadata():
    """Invalid skeletons are removed while valid degenerate metadata stays aligned."""
    key_points = _create_key_points(
        xy=[
            [[0, 0], [0, 0]],
            [[np.nan, 5], [0, 0]],
            [[np.inf, 5], [0, 0]],
            [[15, 25], [np.nan, 50]],
            [[10, 20], [30, 20]],
        ],
        class_id=[0, 1, 2, 3, 4],
        detection_confidence=[0.1, 0.2, 0.3, 0.4, 0.5],
    )
    key_points["custom_data"] = ["zero", "nan", "inf", "point", "line"]

    detections = key_points.as_detections()

    assert np.array_equal(
        detections.xyxy,
        np.array([[15, 25, 15, 25], [10, 20, 30, 20]], dtype=np.float32),
    )
    assert np.array_equal(detections.class_id, np.array([3, 4]))
    assert np.array_equal(detections.confidence, np.array([0.4, 0.5], dtype=np.float32))
    assert np.array_equal(detections.data["custom_data"], np.array(["point", "line"]))


def test_key_points_as_detections_with_data():
    """Test the as_detections method preserves data."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3], [4, 5]]],
        confidence=[[0.8, 0.9, 0.7]],
        class_id=[0],
    )
    key_points["custom_data"] = ["value1"]
    detections = key_points.as_detections()
    assert "custom_data" in detections.data
    assert np.array_equal(detections.data["custom_data"], np.array(["value1"]))


@pytest.mark.parametrize(
    ("key_points_list", "expected_result", "exception"),
    [
        (
            [],
            KeyPoints.empty(),
            DoesNotRaise(),
        ),  # empty list
        (
            [KeyPoints.empty(), KeyPoints.empty()],
            KeyPoints.empty(),
            DoesNotRaise(),
        ),  # only empty KeyPoints
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    confidence=[[0.9, 0.8]],
                    class_id=[0],
                ),
            ],
            _create_key_points(
                xy=[[[10, 10], [20, 20]]],
                confidence=[[0.9, 0.8]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # single KeyPoints
        (
            [
                KeyPoints.empty(),
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    confidence=[[0.9, 0.8]],
                    class_id=[0],
                ),
                KeyPoints.empty(),
            ],
            _create_key_points(
                xy=[[[10, 10], [20, 20]]],
                confidence=[[0.9, 0.8]],
                class_id=[0],
            ),
            DoesNotRaise(),
        ),  # empty KeyPoints are ignored
        (
            [
                KeyPoints(xy=np.array([[[10, 10], [20, 20]]], dtype=np.float32)),
                KeyPoints(xy=np.array([[[30, 30], [40, 40]]], dtype=np.float32)),
            ],
            KeyPoints(
                xy=np.array(
                    [[[10, 10], [20, 20]], [[30, 30], [40, 40]]], dtype=np.float32
                )
            ),
            DoesNotRaise(),
        ),  # xy only; all optional fields None
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    confidence=[[0.9, 0.8]],
                    class_id=[0],
                    detection_confidence=[0.95],
                    visible=[[True, False]],
                    data={"class_name": ["person"]},
                ),
                _create_key_points(
                    xy=[[[30, 30], [40, 40]], [[50, 50], [60, 60]]],
                    confidence=[[0.7, 0.6], [0.5, 0.4]],
                    class_id=[1, 2],
                    detection_confidence=[0.85, 0.75],
                    visible=[[True, True], [False, True]],
                    data={"class_name": ["dog", "cat"]},
                ),
            ],
            _create_key_points(
                xy=[
                    [[10, 10], [20, 20]],
                    [[30, 30], [40, 40]],
                    [[50, 50], [60, 60]],
                ],
                confidence=[[0.9, 0.8], [0.7, 0.6], [0.5, 0.4]],
                class_id=[0, 1, 2],
                detection_confidence=[0.95, 0.85, 0.75],
                visible=[[True, False], [True, True], [False, True]],
                data={"class_name": ["person", "dog", "cat"]},
            ),
            DoesNotRaise(),
        ),  # all fields populated on every input
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    confidence=[[0.9, 0.8]],
                    class_id=[0],
                ),
                KeyPoints(xy=np.array([[[30, 30], [40, 40]]], dtype=np.float32)),
            ],
            None,
            pytest.raises(ValueError, match="All or none of the 'class_id'"),
        ),  # class_id set on one input and None on the other
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    visible=[[True, False]],
                ),
                KeyPoints(xy=np.array([[[30, 30], [40, 40]]], dtype=np.float32)),
            ],
            None,
            pytest.raises(ValueError, match="All or none of the 'visible'"),
        ),  # visible set on one input and None on the other
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    detection_confidence=[0.9],
                ),
                KeyPoints(xy=np.array([[[30, 30], [40, 40]]], dtype=np.float32)),
            ],
            None,
            pytest.raises(
                ValueError, match="All or none of the 'detection_confidence'"
            ),
        ),  # detection_confidence set on one input and None on the other
        (
            [
                KeyPoints(xy=np.array([[[10, 10], [20, 20]]], dtype=np.float32)),
                KeyPoints(
                    xy=np.array([[[10, 10], [20, 20], [30, 30]]], dtype=np.float32)
                ),
            ],
            None,
            pytest.raises(ValueError, match="same number of keypoints"),
        ),  # mismatched keypoint counts per skeleton
        (
            [
                KeyPoints(xy=np.array([[[10, 10], [20, 20]]], dtype=np.float32)),
                KeyPoints(
                    xy=np.array([[[30, 30, 0.9], [40, 40, 0.8]]], dtype=np.float32)
                ),
            ],
            None,
            pytest.raises(ValueError, match=r"got depths \[2, 3\]"),
        ),  # mismatched coordinate depths per skeleton
        (
            [
                _create_key_points(
                    xy=[[[10, 10], [20, 20]]],
                    data={"class_name": ["person"]},
                ),
                _create_key_points(
                    xy=[[[30, 30], [40, 40]]],
                    data={"tracker_id": [7]},
                ),
            ],
            None,
            pytest.raises(ValueError, match="same keys to merge"),
        ),  # data dictionaries with mismatched keys
        pytest.param(
            [
                KeyPoints(xy=np.zeros((2, 0, 2), dtype=np.float32)),
                KeyPoints(xy=np.zeros((3, 0, 2), dtype=np.float32)),
            ],
            KeyPoints(xy=np.zeros((5, 0, 2), dtype=np.float32)),
            DoesNotRaise(),
            id="zero-keypoints-merge-succeeds",
        ),
        pytest.param(
            [
                KeyPoints(xy=np.zeros((2, 0, 2), dtype=np.float32)),
                KeyPoints(xy=np.zeros((1, 2, 2), dtype=np.float32)),
            ],
            None,
            pytest.raises(ValueError, match="same number of keypoints"),
            id="zero-vs-nonzero-keypoints-mismatch",
        ),
    ],
)
def test_key_points_merge(
    key_points_list: list[KeyPoints],
    expected_result: KeyPoints | None,
    exception: Exception,
) -> None:
    """Test KeyPoints.merge field merging, mismatches, and zero-keypoint cases."""
    with exception:
        result = KeyPoints.merge(key_points_list=key_points_list)
        assert result == expected_result, f"Expected: {expected_result}, Got: {result}"


def test_key_points_iteration():
    """Test the iteration over KeyPoints objects."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]], [[4, 5], [6, 7]]],
        confidence=[[0.8, 0.9], [0.7, 0.6]],
        class_id=[0, 1],
    )

    iterations = 0
    for i, (xy, kp_confidence, class_id, data) in enumerate(key_points):
        iterations += 1
        assert xy.shape == (2, 2)
        assert kp_confidence.shape == (2,)
        assert class_id in [0, 1]
        assert isinstance(data, dict)
    assert iterations == 2


def test_key_points_iteration_no_confidence():
    """Test the iteration over KeyPoints objects without confidence."""
    key_points_no_conf = _create_key_points(
        xy=[[[0, 1], [2, 3]]],
        confidence=None,
        class_id=[0],
    )
    for xy, kp_confidence, class_id, data in key_points_no_conf:
        assert kp_confidence is None


def test_key_points_merge_then_with_nms_deduplicates_overlapping_detections():
    """Test merge-then-NMS removes duplicated overlapping skeletons."""
    key_points_list = [
        _create_key_points(
            xy=[[[100, 100], [200, 200]], [[400, 400], [500, 500]]],
            detection_confidence=[0.9, 0.6],
            class_id=[0, 0],
        ),
        _create_key_points(
            xy=[[[100, 100], [200, 200]], [[700, 700], [800, 800]]],
            detection_confidence=[0.85, 0.7],
            class_id=[0, 0],
        ),
    ]

    merged = KeyPoints.merge(key_points_list)
    result = merged.with_nms(threshold=0.5, class_agnostic=False)

    assert len(merged) == 4
    assert len(result) == 3
    assert len(result) < len(merged)
    np.testing.assert_allclose(
        np.sort(result.detection_confidence),
        np.array([0.6, 0.7, 0.9], dtype=np.float32),
    )


@pytest.mark.parametrize(
    ("key_points1", "key_points2", "expected_equal"),
    [
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            True,
        ),
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[1]
            ),
            False,
        ),
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            _create_key_points(
                xy=[[[0, 1], [2, 4]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            False,
        ),
        (
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
            ),
            _create_key_points(
                xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.8]], class_id=[0]
            ),
            False,
        ),
    ],
)
def test_key_points_equality(key_points1, key_points2, expected_equal):
    """Test the equality comparison for KeyPoints objects."""
    status = key_points1 == key_points2
    assert status is expected_equal


def test_key_points_equality_with_data():
    """Test the equality comparison for KeyPoints objects with data."""
    key_points1 = _create_key_points(
        xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
    )
    key_points2 = _create_key_points(
        xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
    )
    key_points2["custom"] = ["value"]
    assert key_points1 != key_points2


@pytest.mark.parametrize(
    ("inference_results", "expected_key_points"),
    [
        (
            {
                "predictions": [
                    {
                        "class_id": 1,
                        "class": "person",
                        "keypoints": [
                            {"x": 100, "y": 150, "confidence": 0.9},
                            {"x": 120, "y": 160, "confidence": 0.85},
                        ],
                    }
                ]
            },
            _create_key_points(
                xy=[[[100.0, 150.0], [120.0, 160.0]]],
                confidence=[[0.9, 0.85]],
                class_id=[1],
                data={"class_name": np.array(["person"])},
            ),
        ),
        ({"predictions": []}, KeyPoints.empty()),
    ],
)
def test_from_inference_input(inference_results, expected_key_points):
    """Test the from_inference method with valid input."""
    key_points = KeyPoints.from_inference(inference_results)
    assert key_points == expected_key_points


def test_from_inference_invalid_input():
    """Test the from_inference method with invalid input."""
    key_points = _create_key_points(
        xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0]
    )
    with pytest.raises(
        ValueError, match=r"from_inference\(\) operates on a single result at a time.*"
    ):
        KeyPoints.from_inference([key_points])


@pytest.mark.parametrize(
    ("yolo_nas_results", "expected_key_points"),
    [
        (
            _FakeYoloNasKeyPointResults(
                _FakeYoloNasKeyPoint(
                    poses=[[[100.0, 150.0, 0.9], [120.0, 160.0, 0.85]]],
                    labels=[1],
                ),
            ),
            _create_key_points(
                xy=[[[100.0, 150.0], [120.0, 160.0]]],
                confidence=[[0.9, 0.85]],
                class_id=[1],
            ),
        ),
        (
            _FakeYoloNasKeyPointResults(
                _FakeYoloNasKeyPoint(
                    poses=[],
                ),
            ),
            KeyPoints.empty(),
        ),
    ],
)
def test_from_yolo_nas_input(yolo_nas_results, expected_key_points):
    """Test the from_yolo_nas method with valid input."""
    key_points = KeyPoints.from_yolo_nas(yolo_nas_results)
    assert key_points == expected_key_points


@pytest.mark.parametrize(
    ("mediapipe_results", "resolution_wh", "expected_key_points"),
    [
        (
            _FakeMediapipeResults(
                pose_landmarks=_FakeMediapipePose(
                    landmarks=[
                        _FakeMediapipeLandmark(0.5, 0.75, 0.9),
                        _FakeMediapipeLandmark(0.6, 0.8, 0.85),
                    ]
                )
            ),
            (200, 200),
            _create_key_points(
                xy=[[[100.0, 150.0], [120.0, 160.0]]],
                confidence=[[0.9, 0.85]],
                class_id=None,
            ),
        ),
        (
            _FakeMediapipeResults(
                pose_landmarks=[
                    [
                        _FakeMediapipeLandmark(0.5, 0.75, 0.9),
                        _FakeMediapipeLandmark(0.6, 0.8, 0.85),
                    ]
                ]
            ),
            (200, 200),
            _create_key_points(
                xy=[[[100.0, 150.0], [120.0, 160.0]]],
                confidence=[[0.9, 0.85]],
                class_id=None,
            ),
        ),
    ],
)
def test_from_mediapipe_input(mediapipe_results, resolution_wh, expected_key_points):
    """Test the from_mediapipe method with valid input."""
    key_points = KeyPoints.from_mediapipe(
        mediapipe_results, resolution_wh=resolution_wh
    )
    assert key_points == expected_key_points


def test_from_mediapipe_unknown_result_type_raises() -> None:
    """Unsupported Mediapipe result objects raise a descriptive ValueError."""
    with pytest.raises(ValueError, match="Unsupported MediaPipe result"):
        KeyPoints.from_mediapipe(object(), resolution_wh=(100, 100))


class TestDeprecatedConfidenceConstructor:
    """Tests for backward-compatible `confidence=` kwarg in KeyPoints()."""

    def test_constructor_accepts_and_warns_on_deprecated_confidence_kwarg(self):
        """Deprecated confidence= warns and maps value to keypoint_confidence."""
        from supervision.utils.internal import SupervisionWarnings

        xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
        confidence = np.array([[0.9, 0.8]], dtype=np.float32)

        with pytest.warns(SupervisionWarnings, match="deprecated since"):
            key_points = KeyPoints(xy=xy, confidence=confidence)

        np.testing.assert_array_equal(key_points.keypoint_confidence, confidence)
        assert key_points.xy is xy

    @pytest.mark.parametrize(
        "kwargs",
        [
            pytest.param(
                {"confidence": None, "keypoint_confidence": None},
                id="confidence-first",
            ),
            pytest.param(
                {"keypoint_confidence": None, "confidence": None},
                id="keypoint-confidence-first",
            ),
        ],
    )
    def test_constructor_rejects_both_confidence_and_keypoint_confidence(
        self, kwargs: dict
    ):
        """ValueError raised regardless of kwarg order when both are passed."""
        xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
        confidence_arr = np.array([[0.9, 0.8]], dtype=np.float32)
        actual_kwargs = {k: confidence_arr for k in kwargs}

        with pytest.raises(ValueError, match="Cannot pass both"):
            KeyPoints(xy=xy, **actual_kwargs)

    def test_constructor_normal_keypoint_confidence_path(self):
        """Normal keypoint_confidence= path works and emits no deprecation warning."""
        import warnings

        xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
        kp_conf = np.array([[0.9, 0.8]], dtype=np.float32)

        with warnings.catch_warnings():
            warnings.simplefilter("error")
            kp = KeyPoints(xy=xy, keypoint_confidence=kp_conf)

        np.testing.assert_array_equal(kp.keypoint_confidence, kp_conf)
        assert kp.data == {}

    def test_constructor_confidence_none_does_not_warn(self):
        """Explicit confidence=None is silently ignored — no warning emitted."""
        import warnings

        xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)

        with warnings.catch_warnings():
            warnings.simplefilter("error")
            kp = KeyPoints(xy=xy, confidence=None)

        assert kp.keypoint_confidence is None

    def test_constructor_data_none_defaults_to_empty_dict(self):
        """Explicit data=None normalizes to empty dict, not None."""
        xy = np.array([[[1.0, 2.0]]], dtype=np.float32)

        assert KeyPoints(xy=xy).data == {}
        assert KeyPoints(xy=xy, data=None).data == {}

    def test_keypoints_init_covers_all_dataclass_fields(self):
        """Custom __init__ must assign every dataclass field — guards against drift."""
        import dataclasses
        import inspect

        field_names = {f.name for f in dataclasses.fields(KeyPoints)}
        init_params = set(inspect.signature(KeyPoints.__init__).parameters) - {
            "self",
            "confidence",
        }
        assert field_names == init_params, (
            f"Field/init drift: {field_names.symmetric_difference(init_params)}"
        )


@pytest.mark.parametrize(
    ("key_points", "threshold", "class_agnostic", "expected_result"),
    [
        pytest.param(
            KeyPoints.empty(),
            0.5,
            True,
            KeyPoints.empty(),
            id="empty",
        ),
        pytest.param(
            _create_key_points(
                xy=[[[10, 20], [30, 40]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            0.5,
            False,
            _create_key_points(
                xy=[[[10, 20], [30, 40]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="single-skeleton",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[10, 10], [20, 20]],
                    [[500, 500], [600, 600]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            0.5,
            False,
            _create_key_points(
                xy=[
                    [[10, 10], [20, 20]],
                    [[500, 500], [600, 600]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            id="two-non-overlapping",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[150, 150], [250, 250]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            0.9,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[150, 150], [250, 250]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            id="two-overlapping-below-threshold",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[[[100, 100], [200, 200]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="two-overlapping-above-threshold",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                    [[500, 500], [600, 600]],
                ],
                detection_confidence=[0.9, 0.7, 0.8],
                class_id=[0, 0, 0],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[500, 500], [600, 600]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            id="three-skeletons-two-overlap",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            id="class-aware-different-classes-kept",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            0.3,
            True,
            _create_key_points(
                xy=[[[100, 100], [200, 200]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="class-agnostic-suppresses-across-classes",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[0, 0], [100, 100], [200, 200]],
                    [[0, 0], [110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[[[0, 0], [100, 100], [200, 200]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="zero-keypoints-excluded-from-bbox",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200], [0, 0], [0, 0]],
                    [[0, 0], [0, 0], [110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200], [0, 0], [0, 0]],
                    [[0, 0], [0, 0], [110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            id="multi-skeleton-schema-class-aware",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200], [0, 0], [0, 0]],
                    [[0, 0], [0, 0], [110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 1],
            ),
            0.3,
            True,
            _create_key_points(
                xy=[[[100, 100], [200, 200], [0, 0], [0, 0]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="multi-skeleton-schema-class-agnostic",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[0, 0], [0, 0]],
                    [[100, 100], [200, 200]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            0.5,
            False,
            _create_key_points(
                xy=[
                    [[0, 0], [0, 0]],
                    [[100, 100], [200, 200]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            id="all-zero-skeleton-passes-through",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
                visible=[[True, False], [False, True]],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
                visible=[[True, False], [False, True]],
            ),
            id="visible-mask-excludes-keypoints-from-bbox",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [0, 0]],
                    [[300, 300], [0, 0]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            0.3,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [0, 0]],
                    [[300, 300], [0, 0]],
                ],
                detection_confidence=[0.9, 0.8],
                class_id=[0, 0],
            ),
            id="single-valid-keypoint-zero-area-bbox",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            0.0,
            False,
            _create_key_points(
                xy=[[[100, 100], [200, 200]]],
                detection_confidence=[0.9],
                class_id=[0],
            ),
            id="threshold-zero-suppresses-any-overlap",
        ),
        pytest.param(
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            1.0,
            False,
            _create_key_points(
                xy=[
                    [[100, 100], [200, 200]],
                    [[110, 110], [210, 210]],
                ],
                detection_confidence=[0.9, 0.7],
                class_id=[0, 0],
            ),
            id="threshold-one-keeps-all",
        ),
    ],
)
def test_with_nms(key_points, threshold, class_agnostic, expected_result):
    """NMS filters overlapping keypoint skeletons."""
    result = key_points.with_nms(threshold=threshold, class_agnostic=class_agnostic)
    assert result == expected_result


@pytest.mark.parametrize(
    ("key_points", "threshold", "class_agnostic", "match"),
    [
        pytest.param(
            _create_key_points(
                xy=[[[10, 20], [30, 40]]],
                class_id=[0],
            ),
            0.5,
            False,
            "detection_confidence",
            id="no-detection-confidence",
        ),
        pytest.param(
            _create_key_points(
                xy=[[[10, 20], [30, 40]]],
                detection_confidence=[0.9],
            ),
            0.5,
            False,
            "class_id",
            id="no-class-id-class-aware",
        ),
        pytest.param(
            _create_key_points(
                xy=[[[10, 20], [30, 40]]],
                class_id=[0],
            ),
            0.5,
            True,
            "detection_confidence",
            id="no-detection-confidence-class-agnostic",
        ),
    ],
)
def test_with_nms_raises(key_points, threshold, class_agnostic, match):
    """NMS raises when required fields are missing."""
    with pytest.raises(ValueError, match=match):
        key_points.with_nms(threshold=threshold, class_agnostic=class_agnostic)
