"""ADR-117 P2 tests — BoundingBox + PersonPose + PoseEstimate bindings.

Run with: cd python && .venv/Scripts/python -m pytest tests/test_pose.py -v
"""

from __future__ import annotations

import pytest

from wifi_densepose import (
    BoundingBox,
    Keypoint,
    KeypointType,
    PersonPose,
    PoseEstimate,
)


# ─── BoundingBox ─────────────────────────────────────────────────────


def test_bounding_box_construct() -> None:
    bb = BoundingBox(0.1, 0.2, 0.5, 0.7)
    assert bb.x_min == pytest.approx(0.1)
    assert bb.y_min == pytest.approx(0.2)
    assert bb.x_max == pytest.approx(0.5)
    assert bb.y_max == pytest.approx(0.7)


def test_bounding_box_dimensions() -> None:
    bb = BoundingBox(0.0, 0.0, 4.0, 3.0)
    assert bb.width == pytest.approx(4.0)
    assert bb.height == pytest.approx(3.0)
    assert bb.area == pytest.approx(12.0)
    assert bb.center == pytest.approx((2.0, 1.5))


def test_bounding_box_from_center() -> None:
    bb = BoundingBox.from_center(2.0, 3.0, 4.0, 6.0)
    assert bb.x_min == pytest.approx(0.0)
    assert bb.y_min == pytest.approx(0.0)
    assert bb.x_max == pytest.approx(4.0)
    assert bb.y_max == pytest.approx(6.0)


def test_bounding_box_iou_no_overlap() -> None:
    a = BoundingBox(0.0, 0.0, 1.0, 1.0)
    b = BoundingBox(2.0, 2.0, 3.0, 3.0)
    assert a.iou(b) == pytest.approx(0.0)


def test_bounding_box_iou_full_overlap() -> None:
    a = BoundingBox(0.0, 0.0, 1.0, 1.0)
    b = BoundingBox(0.0, 0.0, 1.0, 1.0)
    assert a.iou(b) == pytest.approx(1.0)


def test_bounding_box_iou_partial() -> None:
    a = BoundingBox(0.0, 0.0, 10.0, 10.0)
    b = BoundingBox(5.0, 5.0, 15.0, 15.0)
    # intersection 25, union 175 → 1/7
    assert a.iou(b) == pytest.approx(25.0 / 175.0)


def test_bounding_box_eq() -> None:
    assert BoundingBox(1, 2, 3, 4) == BoundingBox(1, 2, 3, 4)
    assert BoundingBox(1, 2, 3, 4) != BoundingBox(1, 2, 3, 5)


def test_bounding_box_repr() -> None:
    bb = BoundingBox(0.1, 0.2, 0.5, 0.7)
    assert "BoundingBox" in repr(bb)
    assert "x_min=0.1" in repr(bb)


# ─── PersonPose ──────────────────────────────────────────────────────


def test_person_pose_empty() -> None:
    p = PersonPose()
    assert p.id is None
    assert p.visible_keypoint_count == 0
    assert p.bounding_box is None
    assert p.confidence == 0.0


def test_person_pose_set_get_keypoint() -> None:
    p = PersonPose()
    kp = Keypoint(KeypointType.Nose, 0.5, 0.3, 0.95)
    p.set_keypoint(kp)
    got = p.get_keypoint(KeypointType.Nose)
    assert got is not None
    assert got.x == pytest.approx(0.5)
    assert got.confidence == pytest.approx(0.95)


def test_person_pose_get_missing_returns_none() -> None:
    p = PersonPose()
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.5, 0.3, 0.95))
    assert p.get_keypoint(KeypointType.LeftWrist) is None


def test_person_pose_visible_count() -> None:
    p = PersonPose()
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.0, 0.0, 0.9))  # visible
    p.set_keypoint(Keypoint(KeypointType.LeftEar, 0.0, 0.0, 0.2))  # invisible
    p.set_keypoint(Keypoint(KeypointType.RightEar, 0.0, 0.0, 0.8))  # visible
    assert p.visible_keypoint_count == 2


def test_person_pose_visible_keypoints_list() -> None:
    p = PersonPose()
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.0, 0.0, 0.9))
    p.set_keypoint(Keypoint(KeypointType.LeftEar, 0.0, 0.0, 0.2))
    vis = p.visible_keypoints()
    assert len(vis) == 1
    assert vis[0].keypoint_type == KeypointType.Nose


def test_person_pose_keypoints_dict_excludes_missing() -> None:
    p = PersonPose()
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.0, 0.0, 0.9))
    p.set_keypoint(Keypoint(KeypointType.LeftWrist, 0.5, 0.5, 0.6))
    d = p.keypoints()
    assert KeypointType.Nose in d
    assert KeypointType.LeftWrist in d
    assert KeypointType.RightAnkle not in d
    assert len(d) == 2


def test_person_pose_set_id() -> None:
    p = PersonPose()
    p.set_id(7)
    assert p.id == 7


def test_person_pose_set_bounding_box() -> None:
    p = PersonPose()
    bb = BoundingBox(0.1, 0.1, 0.5, 0.9)
    p.set_bounding_box(bb)
    assert p.bounding_box == bb


def test_person_pose_compute_bbox_returns_none_when_empty() -> None:
    p = PersonPose()
    assert p.compute_bounding_box() is None


def test_person_pose_compute_bbox_from_keypoints() -> None:
    p = PersonPose()
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.0, 0.0, 0.95))
    p.set_keypoint(Keypoint(KeypointType.RightAnkle, 1.0, 2.0, 0.95))
    bb = p.compute_bounding_box()
    assert bb is not None
    # bbox should span both keypoints
    assert bb.x_min <= 0.0
    assert bb.y_min <= 0.0
    assert bb.x_max >= 1.0
    assert bb.y_max >= 2.0
    # also stored
    assert p.bounding_box is not None


def test_person_pose_set_confidence_validation() -> None:
    p = PersonPose()
    p.set_confidence(0.85)
    assert p.confidence == pytest.approx(0.85)
    with pytest.raises(ValueError):
        p.set_confidence(1.5)


def test_person_pose_repr() -> None:
    p = PersonPose()
    p.set_id(3)
    p.set_keypoint(Keypoint(KeypointType.Nose, 0.0, 0.0, 0.9))
    r = repr(p)
    assert "PersonPose" in r
    assert "id=Some(3)" in r or "id=3" in r


# ─── PoseEstimate ────────────────────────────────────────────────────


def test_pose_estimate_construct_empty() -> None:
    e = PoseEstimate([], 0.5, 1.0, "test-v0")
    assert e.person_count == 0
    assert not e.has_detections
    assert e.confidence == pytest.approx(0.5)
    assert e.latency_ms == pytest.approx(1.0)
    assert e.model_version == "test-v0"


def test_pose_estimate_construct_with_persons() -> None:
    p1 = PersonPose()
    p1.set_id(1)
    p1.set_confidence(0.8)
    p2 = PersonPose()
    p2.set_id(2)
    p2.set_confidence(0.9)
    e = PoseEstimate([p1, p2], 0.85, 5.2, "v0.7.0")
    assert e.person_count == 2
    assert e.has_detections
    assert e.confidence == pytest.approx(0.85)


def test_pose_estimate_highest_confidence_person() -> None:
    p1 = PersonPose()
    p1.set_confidence(0.5)
    p2 = PersonPose()
    p2.set_confidence(0.95)
    p3 = PersonPose()
    p3.set_confidence(0.7)
    e = PoseEstimate([p1, p2, p3], 0.85, 5.2, "v0.7.0")
    best = e.highest_confidence_person()
    assert best is not None
    assert best.confidence == pytest.approx(0.95)


def test_pose_estimate_highest_confidence_returns_none_when_empty() -> None:
    e = PoseEstimate([], 0.5, 1.0, "test")
    assert e.highest_confidence_person() is None


def test_pose_estimate_metadata_strings_nonempty() -> None:
    e = PoseEstimate([], 0.5, 1.0, "test")
    assert isinstance(e.id, str)
    assert isinstance(e.timestamp, str)
    assert e.id  # non-empty
    assert e.timestamp  # non-empty


def test_pose_estimate_confidence_validation() -> None:
    with pytest.raises(ValueError):
        PoseEstimate([], 1.5, 0.0, "test")


def test_pose_estimate_repr_contains_counts() -> None:
    e = PoseEstimate([], 0.5, 2.3, "v0.7.0")
    r = repr(e)
    assert "PoseEstimate" in r
    assert "v0.7.0" in r


def test_build_features_marks_p2_complete() -> None:
    import wifi_densepose

    assert "p2-keypoint-bindings" in wifi_densepose.__build_features__
    assert "p2-pose-bindings" in wifi_densepose.__build_features__
