"""Tests for the visualization overlay pipeline."""

import tempfile
from pathlib import Path

import numpy as np
import pytest

# Skip if cv2 not installed
cv2 = pytest.importorskip("cv2")

pytestmark = pytest.mark.visualization


# ============================================================================
# Skeleton Drawing Tests
# ============================================================================


class TestSkeletonDrawing:
    """Tests for skeleton drawing functions."""

    def _make_frame(self, h=480, w=640):
        """Create a blank black frame."""
        return np.zeros((h, w, 3), dtype=np.uint8)

    def test_draw_mediapipe_skeleton_basic(self):
        """draw_mediapipe_skeleton modifies frame pixels."""
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        original_sum = frame.sum()

        # Normalized 0-1 keypoints (33 joints)
        rng = np.random.default_rng(42)
        kpts = rng.uniform(0.2, 0.8, (33, 3)).astype(np.float32)
        confidence = np.ones(33, dtype=np.float32)

        draw_mediapipe_skeleton(frame, kpts, confidence)
        assert frame.sum() > original_sum, "Frame should have pixels drawn on it"

    def test_draw_mediapipe_with_hands(self):
        """draw_mediapipe_skeleton draws body + both hands."""
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        rng = np.random.default_rng(42)
        kpts = rng.uniform(0.2, 0.8, (33, 3)).astype(np.float32)
        lh = rng.uniform(0.1, 0.4, (21, 3)).astype(np.float32)
        rh = rng.uniform(0.6, 0.9, (21, 3)).astype(np.float32)

        draw_mediapipe_skeleton(frame, kpts, hands_l=lh, hands_r=rh)
        assert frame.sum() > 0

    def test_draw_mediapipe_pixel_coords(self):
        """Handles pixel-coordinate keypoints (values > 2)."""
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame(480, 640)
        rng = np.random.default_rng(42)
        # Pixel coords
        kpts = np.zeros((33, 3), dtype=np.float32)
        kpts[:, 0] = rng.uniform(100, 540, 33)
        kpts[:, 1] = rng.uniform(100, 380, 33)

        draw_mediapipe_skeleton(frame, kpts)
        assert frame.sum() > 0

    def test_draw_mediapipe_low_confidence_hidden(self):
        """Joints with confidence < 0.1 are not drawn."""
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        rng = np.random.default_rng(42)
        kpts = rng.uniform(0.2, 0.8, (33, 3)).astype(np.float32)
        confidence = np.zeros(33, dtype=np.float32)  # All zero confidence

        draw_mediapipe_skeleton(frame, kpts, confidence)
        # With zero confidence, nothing should be drawn (connections skipped)
        # Some edge connections may still draw if endpoints aren't both checked,
        # but the main point is that most drawing is suppressed
        # We just verify it doesn't crash

    def test_draw_wilor_hands(self):
        """draw_wilor_hands draws hand detections."""
        from sltk.visualization.skeleton import draw_wilor_hands

        frame = self._make_frame()
        original_sum = frame.sum()

        # 2 hand detections, 21 joints each, pixel coords
        rng = np.random.default_rng(42)
        kpts_2d = np.zeros((2, 21, 2), dtype=np.float32)
        kpts_2d[0] = rng.uniform(100, 300, (21, 2))
        kpts_2d[1] = rng.uniform(300, 500, (21, 2))
        right_flags = np.array([False, True])

        draw_wilor_hands(frame, kpts_2d, right_flags)
        assert frame.sum() > original_sum

    def test_draw_nlf_body(self):
        """draw_nlf_body draws SMPL-X skeleton."""
        from sltk.visualization.skeleton import draw_nlf_body

        frame = self._make_frame()
        original_sum = frame.sum()

        # 1 detection, 55 joints
        rng = np.random.default_rng(42)
        joints2d = rng.uniform(100, 400, (1, 55, 2)).astype(np.float32)

        draw_nlf_body(frame, joints2d)
        assert frame.sum() > original_sum

    def test_draw_on_various_resolutions(self):
        """Drawing works on different frame sizes."""
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        rng = np.random.default_rng(42)
        kpts = rng.uniform(0.2, 0.8, (33, 3)).astype(np.float32)

        for h, w in [(240, 320), (720, 1280), (1080, 1920)]:
            frame = self._make_frame(h, w)
            draw_mediapipe_skeleton(frame, kpts)
            assert frame.sum() > 0, f"Should draw on {w}x{h} frame"

    def test_draw_empty_detections(self):
        """Empty detection arrays don't crash."""
        from sltk.visualization.skeleton import draw_nlf_body, draw_wilor_hands

        frame = self._make_frame()
        draw_wilor_hands(frame, np.zeros((0, 21, 2), dtype=np.float32))
        draw_nlf_body(frame, np.zeros((0, 55, 2), dtype=np.float32))
        assert frame.sum() == 0  # Nothing drawn


# ============================================================================
# Pipeline Tests
# ============================================================================


class TestPipeline:
    """Tests for the overlay video generation pipeline."""

    @pytest.fixture
    def test_video(self, tmp_path):
        """Create a short test video."""
        video_path = tmp_path / "test.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        for i in range(10):
            frame = np.full((240, 320, 3), i * 25, dtype=np.uint8)
            writer.write(frame)
        writer.release()
        return video_path

    @pytest.fixture
    def mediapipe_h5(self, tmp_path):
        """Create a test MediaPipe H5 file."""
        import h5py

        h5_path = tmp_path / "features" / "test_mediapipe.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        rng = np.random.default_rng(42)
        with h5py.File(h5_path, "w") as f:
            # Dense format: poses[frame] = (33, 3)
            f.create_dataset("poses", data=rng.uniform(0.2, 0.8, (10, 33, 3)).astype(np.float32))
            f.create_dataset("confidence", data=np.ones((10, 33), dtype=np.float32))
            frame_idx = np.stack([np.arange(10), np.ones(10, dtype=np.int32)], axis=1)
            f.create_dataset("frame_idx", data=frame_idx)
            f.attrs["fps"] = 25.0
            f.attrs["format"] = "mediapipe"
        return h5_path

    @pytest.fixture
    def wilor_h5(self, tmp_path):
        """Create a test WiLoR H5 file."""
        import h5py

        h5_path = tmp_path / "features" / "test_wilor.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        rng = np.random.default_rng(42)
        # Sparse: 15 detections across 10 frames
        n_dets = 15
        with h5py.File(h5_path, "w") as f:
            kpts = rng.uniform(50, 270, (n_dets, 21, 2)).astype(np.float32)
            f.create_dataset("kpts_2d", data=kpts)
            f.create_dataset("right", data=rng.choice([True, False], n_dets))
            # frame_idx: varying detections per frame
            frame_idx = np.zeros((10, 2), dtype=np.int32)
            idx = 0
            for fr in range(10):
                count = 1 if fr % 3 != 0 else 2
                if idx + count > n_dets:
                    count = max(0, n_dets - idx)
                frame_idx[fr] = [idx, count]
                idx += count
            f.create_dataset("frame_idx", data=frame_idx)
            f.attrs["fps"] = 25.0
            f.attrs["format"] = "wilor"
        return h5_path

    def test_generate_mediapipe_overlay(self, test_video, mediapipe_h5, tmp_path):
        """Full pipeline: generates overlay video from MediaPipe data."""
        from sltk.visualization.pipeline import generate_overlay_video

        output = tmp_path / "overlays" / "test_mediapipe_skeleton.mp4"
        progress_calls = []

        result = generate_overlay_video(
            video_path=test_video,
            h5_path=mediapipe_h5,
            output_path=output,
            viz_type="mediapipe",
            progress_callback=lambda c, t: progress_calls.append((c, t)),
        )

        assert result.exists()
        assert result.stat().st_size > 0
        assert len(progress_calls) == 10
        assert progress_calls[-1] == (10, 10)

    def test_generate_wilor_overlay(self, test_video, wilor_h5, tmp_path):
        """Full pipeline: generates overlay video from WiLoR data."""
        from sltk.visualization.pipeline import generate_overlay_video

        output = tmp_path / "overlays" / "test_wilor_skeleton.mp4"
        result = generate_overlay_video(
            video_path=test_video,
            h5_path=wilor_h5,
            output_path=output,
            viz_type="wilor",
        )
        assert result.exists()
        assert result.stat().st_size > 0

    def test_invalid_viz_type(self, test_video, mediapipe_h5, tmp_path):
        """Invalid viz_type raises ValueError."""
        from sltk.visualization.pipeline import generate_overlay_video

        with pytest.raises(ValueError, match="Unknown viz_type"):
            generate_overlay_video(
                video_path=test_video,
                h5_path=mediapipe_h5,
                output_path=tmp_path / "out.mp4",
                viz_type="invalid",
            )

    def test_missing_video(self, mediapipe_h5, tmp_path):
        """Missing video file raises FileNotFoundError."""
        from sltk.visualization.pipeline import generate_overlay_video

        with pytest.raises(FileNotFoundError):
            generate_overlay_video(
                video_path=tmp_path / "nonexistent.mp4",
                h5_path=mediapipe_h5,
                output_path=tmp_path / "out.mp4",
                viz_type="mediapipe",
            )

    def test_missing_h5(self, test_video, tmp_path):
        """Missing H5 file raises FileNotFoundError."""
        from sltk.visualization.pipeline import generate_overlay_video

        with pytest.raises(FileNotFoundError):
            generate_overlay_video(
                video_path=test_video,
                h5_path=tmp_path / "nonexistent.h5",
                output_path=tmp_path / "out.mp4",
                viz_type="mediapipe",
            )

    def test_output_directory_created(self, test_video, mediapipe_h5, tmp_path):
        """Output directory is created if it doesn't exist."""
        from sltk.visualization.pipeline import generate_overlay_video

        output = tmp_path / "deep" / "nested" / "dir" / "output.mp4"
        result = generate_overlay_video(
            video_path=test_video,
            h5_path=mediapipe_h5,
            output_path=output,
            viz_type="mediapipe",
        )
        assert result.exists()


# ============================================================================
# API Endpoint Tests
# ============================================================================


# Skip API tests if fastapi not installed
fastapi = pytest.importorskip("fastapi")


class TestVisualizationAPI:
    """Tests for the visualization API endpoints."""

    @pytest.fixture
    def client(self):
        from fastapi.testclient import TestClient  # noqa: I001

        from sltk.api.main import app

        return TestClient(app)

    @pytest.fixture
    def temp_dir(self):
        from sltk.api import dependencies as api_deps

        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            original_roots = api_deps.ALLOWED_ROOTS.copy()
            api_deps.ALLOWED_ROOTS.append(tmpdir_path.resolve())
            yield tmpdir_path
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)

    def test_check_no_overlay(self, client, temp_dir):
        """Check endpoint returns exists=False when no overlay."""
        h5_path = temp_dir / "features" / "test_mediapipe.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        import h5py

        with h5py.File(h5_path, "w") as f:
            f.attrs["format"] = "mediapipe"

        response = client.get(
            "/api/visualization/check",
            params={
                "video_path": str(temp_dir / "test.mp4"),
                "h5_path": str(h5_path),
                "viz_type": "mediapipe",
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["exists"] is False

    def test_check_with_cached_overlay(self, client, temp_dir):
        """Check endpoint returns exists=True when cached overlay is present."""
        import h5py

        h5_path = temp_dir / "features" / "test_mediapipe.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        with h5py.File(h5_path, "w") as f:
            f.attrs["format"] = "mediapipe"

        # Create fake overlay file that's newer than H5
        # _output_path_for uses h5_path.parent / "overlays"
        overlay_dir = temp_dir / "features" / "overlays"
        overlay_dir.mkdir(parents=True, exist_ok=True)
        overlay_path = overlay_dir / "test_mediapipe_mediapipe_skeleton.mp4"
        overlay_path.write_bytes(b"fake video data")

        response = client.get(
            "/api/visualization/check",
            params={
                "video_path": str(temp_dir / "test.mp4"),
                "h5_path": str(h5_path),
                "viz_type": "mediapipe",
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["exists"] is True
        assert data["output_path"] == str(overlay_path)

    def test_generate_returns_cached(self, client, temp_dir):
        """Generate endpoint returns cached result immediately."""
        import h5py

        video_path = temp_dir / "test.mp4"
        video_path.write_bytes(b"fake")
        h5_path = temp_dir / "features" / "test_wilor.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        with h5py.File(h5_path, "w") as f:
            f.attrs["format"] = "wilor"

        # Create cached overlay (now uses h5_path.parent / "overlays")
        overlay_dir = temp_dir / "features" / "overlays"
        overlay_dir.mkdir(parents=True, exist_ok=True)
        overlay_path = overlay_dir / "test_wilor_wilor_skeleton.mp4"
        overlay_path.write_bytes(b"cached overlay")

        response = client.post(
            "/api/visualization/generate",
            json={
                "video_path": str(video_path),
                "h5_path": str(h5_path),
                "viz_type": "wilor",
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "completed"
        assert data["job_id"] == "cached"

    def test_status_not_found(self, client):
        """Status endpoint returns 404 for unknown job."""
        response = client.get("/api/visualization/status/nonexistent")
        assert response.status_code == 404

    def test_generate_starts_job(self, client, temp_dir):
        """Generate endpoint starts a background job when no cache."""
        import h5py

        video_path = temp_dir / "test.mp4"
        video_path.write_bytes(b"fake video")
        h5_path = temp_dir / "features" / "test_mediapipe.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        with h5py.File(h5_path, "w") as f:
            rng = np.random.default_rng(42)
            f.create_dataset("poses", data=rng.uniform(0, 1, (10, 33, 3)).astype(np.float32))
            frame_idx = np.stack([np.arange(10), np.ones(10, dtype=np.int32)], axis=1)
            f.create_dataset("frame_idx", data=frame_idx)
            f.attrs["format"] = "mediapipe"

        response = client.post(
            "/api/visualization/generate",
            json={
                "video_path": str(video_path),
                "h5_path": str(h5_path),
                "viz_type": "mediapipe",
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["status"] in ("pending", "running", "completed", "failed")
        assert data["job_id"] != "cached"


# ============================================================================
# Round 1: Bug-Exposing Edge Case Tests
# ============================================================================


class TestSkeletonEdgeCases:
    """Tests that probe edge cases and potential bugs in skeleton drawing."""

    def _make_frame(self, h=480, w=640):
        """Create a blank black frame."""
        return np.zeros((h, w, 3), dtype=np.uint8)

    # ------------------------------------------------------------------
    # BUG 1: NaN keypoints crash int() conversion
    # ------------------------------------------------------------------
    def test_draw_mediapipe_nan_keypoints_does_not_crash(self):
        """NaN keypoints should be skipped gracefully, not crash.

        Bug: to_pixels() calls result.max() which returns NaN when all values
        are NaN. NaN < 2.0 evaluates to False, so the code skips normalization.
        Then _draw_connections calls int(NaN) which raises ValueError in numpy.
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        kpts = np.full((33, 3), np.nan, dtype=np.float64)

        # This should NOT crash -- NaN keypoints should be handled gracefully
        draw_mediapipe_skeleton(frame, kpts)
        # Frame should remain unmodified (no valid keypoints to draw)
        assert frame.sum() == 0, "NaN keypoints should not produce any drawn pixels"

    # ------------------------------------------------------------------
    # BUG 2: Inf keypoints crash int() conversion
    # ------------------------------------------------------------------
    def test_draw_mediapipe_inf_keypoints_does_not_crash(self):
        """Inf keypoints should be skipped gracefully, not crash.

        Bug: to_pixels() uses max() heuristic. np.inf > 2.0 is True,
        so pixel-coord path is taken. Then int(np.inf) causes OverflowError.
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        kpts = np.full((33, 3), np.inf, dtype=np.float64)

        # This should NOT crash
        draw_mediapipe_skeleton(frame, kpts)

    # ------------------------------------------------------------------
    # BUG 3: Mixed NaN/valid data in to_pixels heuristic
    # ------------------------------------------------------------------
    def test_draw_mediapipe_partial_nan_skips_bad_joints(self):
        """Frame with some NaN joints should still draw the valid ones.

        Bug: np.nanmax vs np.max -- if even one joint is NaN, the max()
        heuristic may misbehave. With mostly normalized values (0-1) and
        a few NaN, max() returns NaN, which means NaN < 2.0 is False,
        so normalization is skipped and the 0-1 values are treated as
        pixel coords (tiny dots near origin, effectively invisible).
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        rng = np.random.default_rng(42)
        kpts = rng.uniform(0.2, 0.8, (33, 3)).astype(np.float64)
        # Introduce NaN for 5 joints
        kpts[10:15] = np.nan

        draw_mediapipe_skeleton(frame, kpts)
        # The valid joints should still be visible at scaled pixel coords.
        # Bug: with NaN present, max() returns NaN, normalization is skipped,
        # so 0.2-0.8 values are treated as pixel coords and drawn at ~(0,0).
        assert frame.sum() > 0, "Valid joints should still be drawn even with some NaN"

    # ------------------------------------------------------------------
    # BUG 4: WiLoR kpts_2d with 3 coordinates (x, y, confidence)
    # ------------------------------------------------------------------
    def test_draw_wilor_hands_3d_keypoints(self):
        """WiLoR keypoints with shape (N, 21, 3) should work.

        Some extractors produce kpts_2d with a third column (confidence or z).
        draw_wilor_hands expects (N, 21, 2) but _draw_connections reads
        keypoints[i][0] and keypoints[i][1], so it works -- but we should
        verify it does not index out of bounds or misinterpret the data.
        """
        from sltk.visualization.skeleton import draw_wilor_hands

        frame = self._make_frame()
        rng = np.random.default_rng(42)
        # Shape (2, 21, 3) -- third column is confidence
        kpts_3col = np.zeros((2, 21, 3), dtype=np.float32)
        kpts_3col[:, :, :2] = rng.uniform(100, 400, (2, 21, 2))
        kpts_3col[:, :, 2] = rng.uniform(0.5, 1.0, (2, 21))
        right_flags = np.array([False, True])

        draw_wilor_hands(frame, kpts_3col, right_flags)
        assert frame.sum() > 0, "Should draw correctly even with 3-column keypoints"

    # ------------------------------------------------------------------
    # BUG 5: to_pixels heuristic wrong for small pixel values
    # ------------------------------------------------------------------
    def test_draw_mediapipe_small_pixel_coords_treated_as_normalized(self):
        """Pixel coords where max < 2.0 are wrongly treated as normalized.

        Bug: to_pixels uses `result.max() < 2.0` to decide if coords are
        normalized or pixel. A signer at the very top-left corner of a large
        frame (e.g., all joint pixel coords between 0-1.9) will be wrongly
        treated as normalized 0-1 and scaled up to huge pixel values.

        This is a real data scenario: a small hand detection in the upper-left
        corner of a 1920x1080 frame where all joint pixels are < 2.
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame(1080, 1920)
        # All joints have pixel coords between 0.5 and 1.9 (top-left corner)
        kpts = np.full((33, 2), 1.5, dtype=np.float32)  # pixel (1.5, 1.5)

        draw_mediapipe_skeleton(frame, kpts)

        # Bug: because max(1.5) < 2.0, coords are treated as normalized.
        # 1.5 * 1920 = 2880 (off-screen!) and 1.5 * 1080 = 1620 (off-screen!)
        # The joints should be drawn at pixel (1, 1), near top-left corner.
        # Instead they're drawn off-screen, so frame is unchanged.
        # Let's verify the CORRECT behavior: something should be drawn near (1,1).
        # Check that the top-left 10x10 region has some drawn pixels:
        top_left_region = frame[:10, :10]
        assert top_left_region.sum() > 0, (
            "Joints at pixel (1.5, 1.5) should be drawn near top-left, "
            "but the normalization heuristic wrongly scales them off-screen"
        )

    # ------------------------------------------------------------------
    # BUG 6: NLF body docstring vs code mismatch for hand joint indices
    # ------------------------------------------------------------------
    def test_nlf_body_joint_index_mapping(self):
        """NLF body draws all 55 joints correctly.

        Bug investigation: The docstring says joints (22-36) are left hand,
        but the code reads joints[25:40] (15 joints starting at 25). This
        means joints 22-24 are NEVER drawn. If those are important body
        joints, they're silently lost. This test verifies by placing unique
        markers at joints 22-24 and checking if they appear.
        """
        from sltk.visualization.skeleton import draw_nlf_body

        frame = self._make_frame()
        joints = np.zeros((1, 55, 2), dtype=np.float32)

        # Place joints 22-24 at a unique visible location (center of frame)
        joints[0, 22] = [320, 240]
        joints[0, 23] = [321, 241]
        joints[0, 24] = [322, 242]

        # All other joints at (0,0) - will be skipped by zero-check
        draw_nlf_body(frame, joints)

        # If joints 22-24 are drawn, there should be pixels near (320, 240)
        center_region = frame[230:250, 310:330]
        assert center_region.sum() > 0, (
            "Joints 22-24 should be drawn but they fall in the gap between "
            "body_joints[:22] and left_hand[25:40] -- they are silently skipped"
        )


class TestPipelineEdgeCases:
    """Tests that probe edge cases in the overlay video pipeline."""

    @pytest.fixture
    def test_video(self, tmp_path):
        """Create a short 10-frame test video at 320x240."""
        video_path = tmp_path / "test.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        for i in range(10):
            frame = np.full((240, 320, 3), i * 25, dtype=np.uint8)
            writer.write(frame)
        writer.release()
        return video_path

    @pytest.fixture
    def long_test_video(self, tmp_path):
        """Create a 20-frame test video (longer than typical H5 data)."""
        video_path = tmp_path / "long_test.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        for i in range(20):
            frame = np.full((240, 320, 3), i * 12, dtype=np.uint8)
            writer.write(frame)
        writer.release()
        return video_path

    # ------------------------------------------------------------------
    # BUG 7: Video has more frames than H5 frame_idx for WiLoR
    # ------------------------------------------------------------------
    def test_wilor_video_longer_than_h5_data(self, long_test_video, tmp_path):
        """Pipeline handles video with more frames than H5 frame_idx.

        The video has 20 frames but frame_idx only covers 10 frames.
        The pipeline should render the first 10 frames with overlays
        and the remaining 10 as plain video frames.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        h5_path = tmp_path / "features" / "test_wilor.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        rng = np.random.default_rng(42)
        n_dets = 10
        with h5py.File(h5_path, "w") as f:
            f.create_dataset(
                "kpts_2d", data=rng.uniform(50, 270, (n_dets, 21, 2)).astype(np.float32)
            )
            f.create_dataset("right", data=rng.choice([True, False], n_dets))
            # frame_idx only covers 10 frames (1 detection per frame)
            frame_idx = np.stack([np.arange(10), np.ones(10, dtype=np.int32)], axis=1)
            f.create_dataset("frame_idx", data=frame_idx)

        output = tmp_path / "overlays" / "test_wilor_overlay.mp4"
        progress_calls = []

        result = generate_overlay_video(
            video_path=long_test_video,
            h5_path=h5_path,
            output_path=output,
            viz_type="wilor",
            progress_callback=lambda c, t: progress_calls.append((c, t)),
        )

        assert result.exists(), "Output video should be created"
        # Should have 20 progress callbacks (one per video frame)
        assert (
            len(progress_calls) == 20
        ), f"Expected 20 progress callbacks for 20-frame video, got {len(progress_calls)}"

    # ------------------------------------------------------------------
    # BUG 8: MediaPipe dense format ignores frame_idx (sparse data)
    # ------------------------------------------------------------------
    def test_mediapipe_sparse_h5_misses_frames(self, test_video, tmp_path):
        """MediaPipe renderer does not use frame_idx for sparse data.

        Bug: _render_mediapipe accesses poses[frame_num] directly,
        treating the data as dense (one pose per frame). If the H5 file
        uses sparse format (poses stored only for frames with detections,
        with frame_idx mapping), the renderer draws the wrong pose on
        the wrong frame.

        This test creates a sparse MediaPipe H5 where:
        - Only frames 0, 5, 9 have detections (3 entries in poses)
        - frame_idx maps these to the correct frame numbers
        The pipeline should only draw on frames 0, 5, 9.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        h5_path = tmp_path / "features" / "test_mediapipe_sparse.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        rng = np.random.default_rng(42)
        # Only 3 pose entries for a 10-frame video
        with h5py.File(h5_path, "w") as f:
            f.create_dataset("poses", data=rng.uniform(0.2, 0.8, (3, 33, 3)).astype(np.float32))
            f.create_dataset("confidence", data=np.ones((3, 33), dtype=np.float32))
            # Sparse frame_idx: frames 0, 5, 9 have 1 detection each
            frame_idx = np.array([[0, 1], [1, 1], [2, 1]], dtype=np.int32)
            # But this is wrong -- frame_idx should be indexed by frame number
            # The real issue: _render_mediapipe IGNORES frame_idx entirely
            # and uses poses[frame_num], so frame 1 gets poses[1] which is
            # actually the detection for frame 5.
            f.create_dataset("frame_idx", data=frame_idx)

        output = tmp_path / "overlays" / "test_mediapipe_sparse.mp4"
        result = generate_overlay_video(
            video_path=test_video,
            h5_path=h5_path,
            output_path=output,
            viz_type="mediapipe",
        )

        assert result.exists()
        # The pipeline should render all 10 frames without error.
        # The deeper bug (frame misalignment) would need frame-by-frame
        # inspection to prove, but at minimum it should not crash when
        # len(poses) < total_frames.

    # ------------------------------------------------------------------
    # BUG 9: Negative frame_idx values
    # ------------------------------------------------------------------
    def test_negative_frame_idx_values(self, test_video, tmp_path):
        """Negative start_idx in frame_idx should not cause array underflow.

        Bug: get_frame_detections checks start_idx < 0 and returns (-1,-1).
        But what if start_idx is negative AND count is positive?
        The check `start_idx < 0` catches this, but numpy negative indexing
        could cause silent corruption if the check were ever bypassed.

        This test ensures the pipeline doesn't crash with negative frame_idx.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        h5_path = tmp_path / "features" / "test_wilor_neg.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)

        rng = np.random.default_rng(42)
        with h5py.File(h5_path, "w") as f:
            f.create_dataset("kpts_2d", data=rng.uniform(50, 270, (5, 21, 2)).astype(np.float32))
            f.create_dataset("right", data=rng.choice([True, False], 5))
            # Corrupt frame_idx with negative start indices
            frame_idx = np.array(
                [
                    [-1, 1],
                    [0, 1],
                    [-5, 2],
                    [1, 1],
                    [2, 1],
                    [3, 1],
                    [4, 0],
                    [-1, 0],
                    [4, 1],
                    [-999, 3],
                ],
                dtype=np.int32,
            )
            f.create_dataset("frame_idx", data=frame_idx)

        output = tmp_path / "overlays" / "test_wilor_neg.mp4"

        # Should not crash -- negative indices should be gracefully skipped
        result = generate_overlay_video(
            video_path=test_video,
            h5_path=h5_path,
            output_path=output,
            viz_type="wilor",
        )
        assert result.exists()

    # ------------------------------------------------------------------
    # BUG 10: _reencode_h264 failure leaves temp file behind
    # ------------------------------------------------------------------
    def test_reencode_h264_failure_cleans_temp_file(self, tmp_path):
        """When ffmpeg fails, the temp file should be cleaned up.

        Bug: _reencode_h264 catches CalledProcessError and calls
        tmp.unlink(missing_ok=True). But what if ffmpeg writes a
        partial/corrupt file? The original mp4v file should remain intact.
        """
        from unittest.mock import patch

        from sltk.visualization.pipeline import _reencode_h264

        # Create a fake source file
        source = tmp_path / "overlay.mp4"
        source.write_bytes(b"original mp4v content")
        original_content = source.read_bytes()

        # Mock ffmpeg to fail
        with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
            with patch(
                "subprocess.run",
                side_effect=Exception("ffmpeg segfault"),
            ):
                # _reencode_h264 catches (CalledProcessError, TimeoutExpired, OSError)
                # but a generic Exception is NOT caught -- this would propagate!
                # Let's test with the actual caught exceptions:
                pass

        # Test with CalledProcessError (which IS caught)
        import subprocess

        with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
            with patch(
                "subprocess.run",
                side_effect=subprocess.CalledProcessError(1, "ffmpeg"),
            ):
                _reencode_h264(source)

        # Original file should be untouched
        assert source.exists(), "Original file should remain after ffmpeg failure"
        assert (
            source.read_bytes() == original_content
        ), "Original file content should be untouched after ffmpeg failure"
        # Temp file should be cleaned up
        tmp_file = source.with_suffix(".tmp.mp4")
        assert not tmp_file.exists(), "Temp file should be cleaned up after failure"

    # ------------------------------------------------------------------
    # BUG 11: _reencode_h264 with generic Exception (not caught)
    # ------------------------------------------------------------------
    def test_reencode_h264_uncaught_exception_type(self, tmp_path):
        """_reencode_h264 only catches specific exceptions, not all.

        Bug: The except clause catches (CalledProcessError, TimeoutExpired, OSError)
        but other exceptions (e.g., PermissionError on tmp file, or a
        RuntimeError from a weird ffmpeg wrapper) would propagate up and
        crash the pipeline. This test documents that behavior.
        """
        from unittest.mock import patch

        from sltk.visualization.pipeline import _reencode_h264

        source = tmp_path / "overlay.mp4"
        source.write_bytes(b"original mp4v content")

        # PermissionError is a subclass of OSError, so it IS caught
        # But RuntimeError is NOT caught
        with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
            with patch(
                "subprocess.run",
                side_effect=RuntimeError("unexpected ffmpeg wrapper error"),
            ):
                with pytest.raises(RuntimeError, match="unexpected"):
                    _reencode_h264(source)

        # The temp file may be left behind since the exception wasn't caught
        # (no cleanup in the except block for this path)

    # ------------------------------------------------------------------
    # BUG 12: Progress callback receives wrong total when video is shorter
    # ------------------------------------------------------------------
    def test_progress_callback_total_matches_actual_frames(self, tmp_path):
        """Progress callback total_frames should match actual frames read.

        Bug: total_frames comes from cv2.CAP_PROP_FRAME_COUNT which can be
        inaccurate for some codecs. The pipeline uses this for the loop bound,
        but cap.read() may return fewer frames. The progress callback
        reports (current, total_frames) where total_frames could be wrong.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        # Create a 5-frame video
        video_path = tmp_path / "short.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        for i in range(5):
            frame = np.full((240, 320, 3), 128, dtype=np.uint8)
            writer.write(frame)
        writer.release()

        h5_path = tmp_path / "features" / "short_mp.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        rng = np.random.default_rng(42)
        with h5py.File(h5_path, "w") as f:
            f.create_dataset("poses", data=rng.uniform(0.2, 0.8, (5, 33, 3)).astype(np.float32))

        output = tmp_path / "overlays" / "short_overlay.mp4"
        progress_calls = []

        generate_overlay_video(
            video_path=video_path,
            h5_path=h5_path,
            output_path=output,
            viz_type="mediapipe",
            progress_callback=lambda c, t: progress_calls.append((c, t)),
        )

        # All progress calls should have the same total
        totals = {t for _, t in progress_calls}
        assert len(totals) == 1, f"Total frames should be consistent, got: {totals}"

        # The final call should have current == total
        if progress_calls:
            last_current, last_total = progress_calls[-1]
            assert last_current == last_total, (
                f"Last progress call should have current==total, "
                f"got current={last_current}, total={last_total}"
            )


class TestVisualizationAPIEdgeCases:
    """Tests for API edge cases in visualization endpoints."""

    @pytest.fixture
    def client(self):
        from fastapi.testclient import TestClient  # noqa: I001

        from sltk.api.main import app

        return TestClient(app)

    @pytest.fixture
    def temp_dir(self):
        from sltk.api import dependencies as api_deps

        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            original_roots = api_deps.ALLOWED_ROOTS.copy()
            api_deps.ALLOWED_ROOTS.append(tmpdir_path.resolve())
            yield tmpdir_path
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)

    # ------------------------------------------------------------------
    # BUG 13: Duplicate pending jobs should be detected
    # ------------------------------------------------------------------
    def test_duplicate_job_detection_for_pending_jobs(self, client, temp_dir):
        """Two rapid generate requests for the same output should return the same job.

        Previously, VizJob started with output_path=None so the dedup check
        `job.output_path == str(output_path)` never matched. Fixed by setting
        output_path at job creation time.
        """
        from unittest.mock import patch

        import h5py

        from sltk.api.routers import visualization as viz_router

        # Reset job state
        with viz_router._jobs_lock:
            viz_router._jobs.clear()

        video_path = temp_dir / "test.mp4"
        video_path.write_bytes(b"fake video")
        h5_path = temp_dir / "features" / "test_wilor.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        with h5py.File(h5_path, "w") as f:
            f.attrs["format"] = "wilor"

        payload = {
            "video_path": str(video_path),
            "h5_path": str(h5_path),
            "viz_type": "wilor",
        }

        # Prevent background task from actually running so job stays pending
        with patch.object(viz_router, "_run_overlay_job"):
            resp1 = client.post("/api/visualization/generate", json=payload)
            assert resp1.status_code == 200
            job1 = resp1.json()

            resp2 = client.post("/api/visualization/generate", json=payload)
            assert resp2.status_code == 200
            job2 = resp2.json()

        # They should be the same job (duplicate detection worked)
        assert job1["job_id"] == job2["job_id"], (
            f"Expected same job_id for duplicate requests, "
            f"got {job1['job_id']} vs {job2['job_id']}."
        )

    # ------------------------------------------------------------------
    # BUG 14: check_overlay does not validate video_path
    # ------------------------------------------------------------------
    def test_check_overlay_ignores_video_path_validation(self, client, temp_dir):
        """Check endpoint does not validate video_path at all.

        Bug: check_overlay validates h5_path via validate_path() but
        video_path is just accepted as a string and never validated.
        An attacker could pass any video_path string.
        """
        import h5py

        h5_path = temp_dir / "features" / "test_mp.h5"
        h5_path.parent.mkdir(parents=True, exist_ok=True)
        with h5py.File(h5_path, "w") as f:
            f.attrs["format"] = "mediapipe"

        # Pass a completely bogus video path -- should it be validated?
        response = client.get(
            "/api/visualization/check",
            params={
                "video_path": "/etc/shadow",  # clearly malicious
                "h5_path": str(h5_path),
                "viz_type": "mediapipe",
            },
        )
        # Currently returns 200 with exists=False (video_path is never checked)
        # This is a design issue but not necessarily a security bug since
        # check_overlay only reads h5_path.stat() and overlay file existence.
        assert response.status_code == 200

    # ------------------------------------------------------------------
    # BUG 15: _output_path_for goes two levels up from h5_path
    # ------------------------------------------------------------------
    def test_output_path_convention_with_flat_h5(self, client, temp_dir):
        """_output_path_for uses h5_path.parent.parent which may escape.

        Bug: If the H5 file is at the root of temp_dir (not in a
        features/ subdirectory), then parent.parent goes ABOVE temp_dir.
        The overlay directory would be created outside the expected tree.
        """
        from sltk.api.routers.visualization import _output_path_for

        # H5 at root of temp_dir (no features/ subdirectory)
        h5_path = temp_dir / "test.h5"
        output = _output_path_for(h5_path, "mediapipe")

        # parent.parent of temp_dir/test.h5 is temp_dir's parent!
        # The overlay would be written OUTSIDE the workspace directory
        assert output.parent.is_relative_to(temp_dir), (
            f"Output path {output} escapes the workspace directory {temp_dir}. "
            f"h5_path.parent.parent = {h5_path.parent.parent}"
        )


# ============================================================================
# Round 3: Deeper Edge Case Tests
# ============================================================================


class TestRound3EdgeCases:
    """Round 3 edge-case tests targeting bugs that may still exist after the
    Round 2 fixes to normalization, NLF indexing, job dedup, and output paths.

    Focus areas:
    - Large/overflow coordinate values crashing cv2
    - MediaPipe renderer ignoring frame_idx (sparse data misalignment)
    - Cached VizJob losing total_frames
    - SMPLX extra joints (52-54) silently dropped
    - Thread-safety TOCTOU in job dedup
    """

    def _make_frame(self, h=480, w=640):
        """Create a blank black frame."""
        return np.zeros((h, w, 3), dtype=np.uint8)

    # ------------------------------------------------------------------
    # BUG R3-1: Large pixel coordinates crash cv2 (int > INT32_MAX)
    # ------------------------------------------------------------------
    def test_draw_mediapipe_huge_pixel_coords_does_not_crash(self):
        """Keypoints with huge finite pixel values should not crash cv2.

        Bug: _draw_connections and _draw_joints call int(keypoints[i][0])
        which produces a Python int. If the value exceeds 2^31-1 (INT32_MAX),
        cv2.line raises an error because it expects C int32.

        The isfinite() guard catches NaN/Inf but NOT huge finite values.
        A corrupted H5 file or a bad extractor could produce coords like 1e10.
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        # Keypoints with huge pixel coords (not NaN/Inf -- just very large)
        kpts = np.full((33, 2), 1e15, dtype=np.float64)

        # This SHOULD NOT crash -- huge coords should be clipped or skipped
        draw_mediapipe_skeleton(frame, kpts)

    def test_draw_wilor_huge_pixel_coords_does_not_crash(self):
        """WiLoR hand keypoints with huge values should not crash cv2.

        Bug: draw_wilor_hands passes raw coords to _draw_connections.
        There is no to_pixels() normalization or range check.
        Values >= 2^31 cause cv2.line to raise a TypeError.
        """
        from sltk.visualization.skeleton import draw_wilor_hands

        frame = self._make_frame()
        # 1 hand detection with coords > INT32_MAX
        kpts = np.full((1, 21, 2), 3e9, dtype=np.float64)
        right_flags = np.array([True])

        # This should not crash
        draw_wilor_hands(frame, kpts, right_flags)

    def test_draw_nlf_huge_pixel_coords_does_not_crash(self):
        """NLF body joints with huge values should not crash cv2.

        Bug: draw_nlf_body passes raw coords to _draw_connections.
        Values >= 2^31 produce Python ints that cv2 cannot parse.
        """
        from sltk.visualization.skeleton import draw_nlf_body

        frame = self._make_frame()
        # 1 detection, 55 joints, all with huge coords
        joints = np.full((1, 55, 2), 5e9, dtype=np.float64)

        # This should not crash
        draw_nlf_body(frame, joints)

    # ------------------------------------------------------------------
    # BUG R3-2: _render_mediapipe ignores frame_idx (sparse misalignment)
    # ------------------------------------------------------------------
    def test_mediapipe_renderer_uses_frame_idx_for_sparse_data(self):
        """MediaPipe renderer should use frame_idx to map poses to frames.

        Bug: _render_mediapipe accesses poses[frame_num] directly, treating
        data as dense (one pose per video frame). For sparse H5 files where
        frame_idx maps detection indices to frame numbers, the renderer
        draws pose[0] on video frame 0 even if frame_idx says detection 0
        belongs to frame 5.

        This test creates a sparse MediaPipe H5 with 2 poses for a 10-frame
        video. frame_idx says: frame 3 has pose at index 0, frame 7 has pose
        at index 1. All other frames should have no overlay.

        With the bug: _render_mediapipe draws poses[0] on frame 0 and
        poses[1] on frame 1, ignoring frame_idx entirely.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        tmp_path = Path(tempfile.mkdtemp())

        # Create 10-frame test video
        video_path = tmp_path / "test.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        for _ in range(10):
            frame = np.zeros((240, 320, 3), dtype=np.uint8)
            writer.write(frame)
        writer.release()

        # Create sparse MediaPipe H5: 2 detections for frames 3 and 7
        h5_path = tmp_path / "sparse_mp.h5"
        rng = np.random.default_rng(42)
        with h5py.File(h5_path, "w") as f:
            # Only 2 pose entries
            poses = rng.uniform(0.2, 0.8, (2, 33, 3)).astype(np.float32)
            f.create_dataset("poses", data=poses)
            f.create_dataset("confidence", data=np.ones((2, 33), dtype=np.float32))
            # frame_idx: 10 entries (one per frame), most with count=0
            frame_idx = np.zeros((10, 2), dtype=np.int32)
            frame_idx[3] = [0, 1]  # Frame 3: pose at index 0
            frame_idx[7] = [1, 1]  # Frame 7: pose at index 1
            f.create_dataset("frame_idx", data=frame_idx)

        output = tmp_path / "overlays" / "sparse_overlay.mp4"
        result = generate_overlay_video(
            video_path=video_path,
            h5_path=h5_path,
            output_path=output,
            viz_type="mediapipe",
        )

        # Read back the output video and check which frames have overlay.
        # mp4v codec introduces small compression artifacts, so we use a
        # threshold to distinguish "has real overlay" from "codec noise".
        cap = cv2.VideoCapture(str(result))
        frame_sums = []
        for i in range(10):
            ret, frame = cap.read()
            if not ret:
                break
            frame_sums.append(int(frame.sum()))
        cap.release()

        assert len(frame_sums) == 10, f"Expected 10 frames, got {len(frame_sums)}"

        # Frames with real overlay have pixel sums orders of magnitude larger
        # than codec-artifact-only frames. Use 10x the minimum as threshold.
        min_sum = min(frame_sums)
        threshold = max(min_sum * 10, 50000)
        frames_with_overlay = [i for i, s in enumerate(frame_sums) if s > threshold]

        # With correct frame_idx usage, only frames 3 and 7 should have overlay.
        # With the bug, _render_mediapipe uses poses[frame_num] directly:
        #   frame 0 -> poses[0] (should be frame 3's pose)
        #   frame 1 -> poses[1] (should be frame 7's pose)
        #   frames 2-9 -> no poses (len(poses)==2, so frame_num >= 2 skipped)
        # So the bug causes overlay on frames [0, 1] instead of [3, 7].
        assert 3 in frames_with_overlay, (
            f"Frame 3 should have overlay (per frame_idx[3]=[0,1]), "
            f"but frames with overlay are: {frames_with_overlay} "
            f"(sums: {[frame_sums[i] for i in frames_with_overlay]}). "
            f"This proves _render_mediapipe ignores frame_idx and uses "
            f"dense indexing poses[frame_num] instead."
        )
        assert 0 not in frames_with_overlay, (
            f"Frame 0 should NOT have overlay (frame_idx[0]=[0,0] means "
            f"no detection), but _render_mediapipe draws poses[0] on frame 0 "
            f"because it ignores frame_idx. "
            f"Frames with overlay: {frames_with_overlay}"
        )

    # ------------------------------------------------------------------
    # BUG R3-3: Cached VizJob hardcodes total_frames=0
    # ------------------------------------------------------------------
    def test_cached_vizjob_has_nonzero_total_frames(self):
        """Cached overlay response should report actual total_frames, not 0.

        Bug: When a cached overlay is found in generate_overlay, the endpoint
        returns VizJob(total_frames=0, current_frame=0). The frontend may use
        total_frames to show a progress bar or frame count -- a value of 0
        is misleading for a completed job.
        """
        pytest.importorskip("fastapi")
        import h5py
        from fastapi.testclient import TestClient

        from sltk.api import dependencies as api_deps
        from sltk.api.main import app

        client = TestClient(app)

        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            original_roots = api_deps.ALLOWED_ROOTS.copy()
            api_deps.ALLOWED_ROOTS.append(tmpdir_path.resolve())
            try:
                video_path = tmpdir_path / "test.mp4"
                video_path.write_bytes(b"fake video")
                h5_path = tmpdir_path / "features" / "test_mp.h5"
                h5_path.parent.mkdir(parents=True, exist_ok=True)
                with h5py.File(h5_path, "w") as f:
                    f.attrs["format"] = "mediapipe"

                # Create a real cached overlay video (5 frames)
                overlay_dir = tmpdir_path / "features" / "overlays"
                overlay_dir.mkdir(parents=True, exist_ok=True)
                overlay_path = overlay_dir / "test_mp_mediapipe_skeleton.mp4"
                fourcc = cv2.VideoWriter_fourcc(*"mp4v")
                vw = cv2.VideoWriter(str(overlay_path), fourcc, 25.0, (320, 240))
                for _ in range(5):
                    vw.write(np.zeros((240, 320, 3), dtype=np.uint8))
                vw.release()

                response = client.post(
                    "/api/visualization/generate",
                    json={
                        "video_path": str(video_path),
                        "h5_path": str(h5_path),
                        "viz_type": "mediapipe",
                    },
                )
                assert response.status_code == 200
                data = response.json()
                assert data["status"] == "completed"
                assert data["job_id"] == "cached"

                assert data["total_frames"] > 0, (
                    f"Cached job reports total_frames={data['total_frames']}. "
                    f"A completed overlay should report the actual frame count, "
                    f"not 0. The frontend cannot show meaningful progress info."
                )
            finally:
                api_deps.ALLOWED_ROOTS.clear()
                api_deps.ALLOWED_ROOTS.extend(original_roots)

    # ------------------------------------------------------------------
    # BUG R3-4: SMPLX joints 52-54 (jaw/eyes) silently dropped
    # ------------------------------------------------------------------
    def test_nlf_draws_all_55_joints(self):
        """NLF body should draw all 55 joints, including jaw/eyes at 52-54.

        Bug: draw_nlf_body processes joints[:22] (body), joints[22:37]
        (left hand), and joints[37:52] (right hand) = 52 joints total.
        Joints at indices 52, 53, 54 (jaw/eye extra joints from SMPL-X)
        are never drawn.

        For a 55-joint SMPL-X model, these 3 joints represent jaw and
        eye controls. While they may not need connections, they should
        at least be drawn as dots if they have valid coordinates.
        """
        from sltk.visualization.skeleton import draw_nlf_body

        frame = self._make_frame()
        joints = np.zeros((1, 55, 2), dtype=np.float32)

        # Place ONLY joints 52-54 at visible positions (center of frame)
        # All other joints are at (0,0) and will be skipped
        joints[0, 52] = [300, 200]
        joints[0, 53] = [310, 210]
        joints[0, 54] = [320, 220]

        draw_nlf_body(frame, joints)

        # If joints 52-54 are drawn, there should be pixels near (300-320, 200-220)
        region = frame[190:230, 290:330]
        assert region.sum() > 0, (
            "Joints 52-54 (jaw/eyes) should be drawn but draw_nlf_body only "
            "processes indices 0-51. The 3 extra SMPL-X joints are silently "
            "dropped and never rendered."
        )

    # ------------------------------------------------------------------
    # BUG R3-5: TOCTOU race condition in job dedup
    # ------------------------------------------------------------------
    def test_concurrent_generate_requests_produce_single_job(self):
        """Two concurrent generate requests should produce exactly one job.

        Bug: The dedup check (scanning _jobs under lock) and the job creation
        (_set_job under a separate lock acquisition) are not atomic. Between
        these two operations, another request can pass the same dedup check
        and also create a job. This is a Time-Of-Check-Time-Of-Use (TOCTOU)
        race condition.

        In the current code (visualization.py lines 118-126):
          1. Lock -> scan _jobs for matching output_path -> unlock
          2. Create new job_id
          3. Lock -> _set_job -> unlock

        Between step 1 and step 3, another thread can also pass step 1
        and both create separate jobs for the same output.
        """
        import threading
        from unittest.mock import patch

        pytest.importorskip("fastapi")
        import h5py
        from fastapi.testclient import TestClient

        from sltk.api import dependencies as api_deps
        from sltk.api.main import app
        from sltk.api.routers import visualization as viz_router

        # Reset job state
        with viz_router._jobs_lock:
            viz_router._jobs.clear()

        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            original_roots = api_deps.ALLOWED_ROOTS.copy()
            api_deps.ALLOWED_ROOTS.append(tmpdir_path.resolve())
            try:
                video_path = tmpdir_path / "test.mp4"
                video_path.write_bytes(b"fake video")
                h5_path = tmpdir_path / "features" / "test_wilor.h5"
                h5_path.parent.mkdir(parents=True, exist_ok=True)
                with h5py.File(h5_path, "w") as f:
                    f.attrs["format"] = "wilor"

                payload = {
                    "video_path": str(video_path),
                    "h5_path": str(h5_path),
                    "viz_type": "wilor",
                }

                results = [None, None]
                barrier = threading.Barrier(2)

                # Patch _run_overlay_job to prevent actual work
                # and add a delay after the dedup check to widen the race window
                original_set_job = viz_router._set_job

                call_count = {"n": 0}

                def slow_set_job(job_id, job):
                    call_count["n"] += 1
                    import time

                    time.sleep(0.05)  # Widen the race window
                    original_set_job(job_id, job)

                def send_request(idx):
                    client = TestClient(app)
                    barrier.wait()  # Synchronize start
                    resp = client.post("/api/visualization/generate", json=payload)
                    results[idx] = resp.json()

                with patch.object(viz_router, "_run_overlay_job"):
                    with patch.object(viz_router, "_set_job", side_effect=slow_set_job):
                        t1 = threading.Thread(target=send_request, args=(0,))
                        t2 = threading.Thread(target=send_request, args=(1,))
                        t1.start()
                        t2.start()
                        t1.join(timeout=10)
                        t2.join(timeout=10)

                assert (
                    results[0] is not None and results[1] is not None
                ), "Both requests should complete"

                # Both should return the SAME job_id (dedup should work)
                job_id_1 = results[0]["job_id"]
                job_id_2 = results[1]["job_id"]
                assert job_id_1 == job_id_2, (
                    f"TOCTOU race: two concurrent requests created separate "
                    f"jobs ({job_id_1} vs {job_id_2}) for the same output. "
                    f"The dedup check and job creation are not atomic."
                )
            finally:
                api_deps.ALLOWED_ROOTS.clear()
                api_deps.ALLOWED_ROOTS.extend(original_roots)

    # ------------------------------------------------------------------
    # BUG R3-6: to_pixels with all-zero coords scales unnecessarily
    # ------------------------------------------------------------------
    def test_to_pixels_all_zero_coords_not_double_scaled(self):
        """All-zero keypoints should produce all-zero pixel coords.

        When all keypoints are (0, 0), to_pixels sees max_val=0.0 <= 1.0,
        treats them as normalized, and multiplies by (w, h). Result is still
        (0, 0) since 0 * anything = 0. So no crash. But this means there is
        NO way to distinguish between "no detection" (zeros) and "normalized
        detection at origin" (also zeros). The zero-check in _draw_connections
        skips both cases, so nothing is drawn.

        This test documents the behavior: all-zero input = nothing drawn.
        (This test is expected to PASS -- it documents a known limitation.)
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        kpts = np.zeros((33, 3), dtype=np.float32)

        draw_mediapipe_skeleton(frame, kpts)
        assert (
            frame.sum() == 0
        ), "All-zero keypoints should produce no drawing (zero-check skips them)"

    # ------------------------------------------------------------------
    # BUG R3-7: to_pixels negative normalized coords drawn off-screen
    # ------------------------------------------------------------------
    def test_to_pixels_negative_normalized_coords_partially_visible(self):
        """Centered-normalization coords (e.g., [-0.5, 0.5]) should work.

        Some pose formats use centered normalization where coords range
        from -1 to +1 (center of frame = 0). to_pixels checks
        max(abs(values)) <= 1.0, which is True for this range, so it
        scales by (w, h). But negative coords become negative pixels
        (e.g., -0.5 * 640 = -320), which are off-screen.

        The positive-half coords should still be visible. This test
        verifies that at least SOME joints are drawn when half are
        in positive normalized range.
        """
        from sltk.visualization.skeleton import draw_mediapipe_skeleton

        frame = self._make_frame()
        rng = np.random.default_rng(42)
        # Centered normalization: half negative, half positive
        kpts = rng.uniform(-0.3, 0.8, (33, 3)).astype(np.float64)

        draw_mediapipe_skeleton(frame, kpts)

        # Joints with positive coords should be visible. The negative
        # ones draw off-screen (silently ignored by cv2). No crash.
        # But the position is WRONG: the user expected centered coords
        # to be mapped relative to frame center, not frame origin.
        # e.g., coord 0.0 should map to frame center (320, 240),
        # but actually maps to pixel (0, 0) which is skipped.
        # This is a semantic bug in the normalization heuristic.
        #
        # For now, we just verify no crash and some pixels drawn.
        assert frame.sum() > 0, "Joints with positive normalized coords should be visible"

    # ------------------------------------------------------------------
    # BUG R3-8: Pipeline with zero-length video produces empty output
    # ------------------------------------------------------------------
    def test_pipeline_zero_frame_video(self, tmp_path):
        """Pipeline should handle a zero-frame video gracefully.

        A zero-frame video is degenerate but should not crash. The output
        file should exist (possibly empty). cv2.CAP_PROP_FRAME_COUNT
        returns 0, so the loop never executes.
        """
        import h5py

        from sltk.visualization.pipeline import generate_overlay_video

        # Create a zero-frame video by creating and immediately releasing writer
        video_path = tmp_path / "empty.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, (320, 240))
        writer.release()

        h5_path = tmp_path / "empty.h5"
        with h5py.File(h5_path, "w") as f:
            rng = np.random.default_rng(42)
            f.create_dataset(
                "poses",
                data=rng.uniform(0.2, 0.8, (0, 33, 3)).astype(np.float32),
            )

        output = tmp_path / "overlays" / "empty_overlay.mp4"

        # Should not crash on zero-frame video. May raise RuntimeError
        # if cv2.VideoCapture fails to open the empty file, or if
        # cv2.VideoWriter can't write zero frames.
        try:
            result = generate_overlay_video(
                video_path=video_path,
                h5_path=h5_path,
                output_path=output,
                viz_type="mediapipe",
            )
            # If it succeeds, output should exist
            assert result.exists()
        except RuntimeError as e:
            # Acceptable: "Cannot open video" for a zero-frame file
            assert "Cannot open video" in str(e) or "Cannot create" in str(e)

    # ------------------------------------------------------------------
    # BUG R3-9: _reencode_h264 exception for non-OSError subclasses
    # ------------------------------------------------------------------
    def test_reencode_h264_leaks_temp_on_unexpected_exception(self):
        """_reencode_h264 leaves temp file behind for unexpected exceptions.

        Bug: The except clause catches (CalledProcessError, TimeoutExpired,
        OSError) and cleans up the temp file. But if subprocess.run raises
        a different exception type (e.g., RuntimeError, ValueError), the
        temp file is never cleaned up AND the exception propagates.

        This means generate_overlay_video can fail with an unhandled
        exception, leaving a .tmp.mp4 file on disk.
        """
        import subprocess
        from unittest.mock import patch

        from sltk.visualization.pipeline import _reencode_h264

        tmp_path = Path(tempfile.mkdtemp())
        source = tmp_path / "overlay.mp4"
        source.write_bytes(b"original content")
        tmp_file = source.with_suffix(".tmp.mp4")

        with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
            # Make subprocess.run create the temp file then raise RuntimeError
            def failing_run(*args, **kwargs):
                # Simulate ffmpeg creating the temp file before crashing
                tmp_file.write_bytes(b"partial data")
                raise RuntimeError("unexpected crash")

            with patch("subprocess.run", side_effect=failing_run):
                with pytest.raises(RuntimeError, match="unexpected crash"):
                    _reencode_h264(source)

        # The temp file should be cleaned up, but it is NOT
        # because RuntimeError is not caught by the except clause
        assert not tmp_file.exists(), (
            f"Temp file {tmp_file} should be cleaned up after unexpected "
            f"exception, but _reencode_h264 only catches "
            f"(CalledProcessError, TimeoutExpired, OSError)"
        )
