"""Integration tests for InferenceSlicer with compact_masks=True.

Verifies that with compact_masks=True:
- Masks stay as CompactMask throughout the pipeline (no dense materialisation).
- NMS is computed via RLE IoU (no resize, no dense (N,H,W) alloc).
- Final detections are pixel-identical to the compact_masks=False path.
"""

import numpy as np

import supervision as sv
from supervision.detection.compact_mask import CompactMask
from supervision.detection.core import Detections


def _fake_seg_callback(tile: np.ndarray) -> Detections:
    """Return two non-overlapping segmentation detections for any tile."""
    h, w = tile.shape[:2]
    masks = np.zeros((2, h, w), dtype=bool)
    masks[0, : h // 3, : w // 3] = True
    masks[1, h // 2 :, w // 2 :] = True
    xyxy = np.array([[0, 0, w // 3, h // 3], [w // 2, h // 2, w, h]], dtype=np.float32)
    return Detections(
        xyxy=xyxy,
        mask=masks,
        confidence=np.array([0.9, 0.8], dtype=np.float32),
        class_id=np.array([0, 1]),
    )


class TestInferenceSlicerCompactMasks:
    """Tests that compact_masks=True keeps masks in RLE form end-to-end.

    The pipeline inside InferenceSlicer goes:
      callback → CompactMask.from_dense (tile coords)
               → with_offset (full-image coords)
               → CompactMask.merge (all tiles)
               → mask_non_max_suppression → compact_mask_iou_batch (RLE IoU)

    None of those steps materialise a full (N, H, W) dense array.
    """

    def test_compact_masks_flag_converts_dense_to_compact(self) -> None:
        """Masks returned from callback are CompactMask after _run_callback."""
        image = np.zeros((200, 200, 3), dtype=np.uint8)
        slicer = sv.InferenceSlicer(
            callback=_fake_seg_callback,
            slice_wh=200,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NONE,
            compact_masks=True,
        )
        result = slicer(image)
        assert isinstance(result.mask, CompactMask), (
            f"compact_masks=True must produce a CompactMask, got {type(result.mask)}"
        )

    def test_compact_masks_false_keeps_dense(self) -> None:
        """Default (compact_masks=False) keeps dense ndarray masks."""
        image = np.zeros((200, 200, 3), dtype=np.uint8)
        slicer = sv.InferenceSlicer(
            callback=_fake_seg_callback,
            slice_wh=200,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NONE,
            compact_masks=False,
        )
        result = slicer(image)
        assert isinstance(result.mask, np.ndarray)
        assert not isinstance(result.mask, CompactMask)

    def test_compact_and_dense_pipelines_give_same_masks(self) -> None:
        """compact_masks=True and False must produce pixel-identical final masks."""
        image = np.zeros((300, 300, 3), dtype=np.uint8)

        slicer_dense = sv.InferenceSlicer(
            callback=_fake_seg_callback,
            slice_wh=150,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
            iou_threshold=0.3,
            compact_masks=False,
        )
        slicer_compact = sv.InferenceSlicer(
            callback=_fake_seg_callback,
            slice_wh=150,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
            iou_threshold=0.3,
            compact_masks=True,
        )

        det_dense = slicer_dense(image)
        det_compact = slicer_compact(image)

        assert len(det_dense) == len(det_compact)

        dense_masks = det_dense.mask
        compact_masks_arr = np.asarray(det_compact.mask)

        # Sort both by xyxy to align order (NMS order may differ).
        def _sort_key(d: Detections) -> np.ndarray:
            return d.xyxy[:, 0] * 10000 + d.xyxy[:, 1]

        order_d = np.argsort(_sort_key(det_dense))
        order_c = np.argsort(_sort_key(det_compact))

        np.testing.assert_array_equal(
            dense_masks[order_d],
            compact_masks_arr[order_c],
            err_msg="compact_masks pipeline produced different mask pixels than dense",
        )

    def test_compact_masks_preserve_pixels_outside_detector_box(self) -> None:
        """compact_masks=True crops to the full tile, so mask pixels outside the
        detection xyxy box (but inside the tile) are preserved."""
        image = np.zeros((100, 100, 3), dtype=np.uint8)

        def callback(tile: np.ndarray) -> Detections:
            h, w = tile.shape[:2]
            masks = np.zeros((1, h, w), dtype=bool)
            masks[0, 0, 0] = True
            masks[0, h - 1, w - 1] = True
            return Detections(
                xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
                mask=masks,
                confidence=np.array([0.9], dtype=np.float32),
                class_id=np.array([0]),
            )

        dense = sv.InferenceSlicer(
            callback=callback,
            slice_wh=100,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NONE,
            compact_masks=False,
        )(image)
        compact = sv.InferenceSlicer(
            callback=callback,
            slice_wh=100,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NONE,
            compact_masks=True,
        )(image)

        assert isinstance(compact.mask, CompactMask)
        np.testing.assert_array_equal(compact.mask.to_dense(), dense.mask)

    def test_nms_with_overlapping_tiles_uses_rle_iou(self) -> None:
        """With overlapping tiles, NMS must suppress duplicates using RLE IoU."""
        image = np.zeros((300, 300, 3), dtype=np.uint8)

        call_count = 0

        def counting_callback(tile: np.ndarray) -> Detections:
            nonlocal call_count
            call_count += 1
            return _fake_seg_callback(tile)

        slicer = sv.InferenceSlicer(
            callback=counting_callback,
            slice_wh=200,
            overlap_wh=100,  # heavy overlap → many duplicate detections
            overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
            iou_threshold=0.3,
            compact_masks=True,
        )
        result = slicer(image)

        assert call_count > 1, "Should have run on multiple tiles"
        assert isinstance(result.mask, CompactMask), (
            "Result mask must remain CompactMask after cross-tile NMS"
        )

    def test_no_mask_callback_unaffected(self) -> None:
        """compact_masks=True must not crash when callback returns no masks."""

        def box_only_callback(tile: np.ndarray) -> Detections:
            h, w = tile.shape[:2]
            return Detections(
                xyxy=np.array([[0, 0, w // 2, h // 2]], dtype=np.float32),
                confidence=np.array([0.9]),
                class_id=np.array([0]),
            )

        image = np.zeros((200, 200, 3), dtype=np.uint8)
        slicer = sv.InferenceSlicer(
            callback=box_only_callback,
            slice_wh=200,
            overlap_wh=0,
            overlap_filter=sv.OverlapFilter.NONE,
            compact_masks=True,
        )
        result = slicer(image)
        assert result.mask is None
