"""Tests for draw_debug in python_pkg.puzzle_solver.parse_image."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from python_pkg.puzzle_solver.parse_image import (
    _assign_teleporter_and_kl_groups,
    draw_debug,
)

CV2 = "python_pkg.puzzle_solver.parse_image.cv2"


# ── draw_debug ───────────────────────────────────────────────────────


class TestDrawDebug:
    @patch(CV2)
    def test_image_not_found_returns_early(self, mock_cv2: MagicMock) -> None:
        mock_cv2.imread.return_value = None
        draw_debug("nofile.png", {"squares": []}, "out.png")
        mock_cv2.imwrite.assert_not_called()

    @patch(CV2)
    def test_draws_normal_square(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "normal",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        mock_cv2.rectangle.assert_called_once()
        mock_cv2.putText.assert_called_once()
        mock_cv2.imwrite.assert_called_once_with("out.png", mock_img)

    @patch(CV2)
    def test_draws_portal_with_arrows(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "portal",
                    "side": "left",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        # label should be "<" for left
        args = mock_cv2.putText.call_args
        assert args[0][1] == "<"

    @patch(CV2)
    def test_draws_portal_right_arrow(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "portal",
                    "side": "right",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        args = mock_cv2.putText.call_args
        assert args[0][1] == ">"

    @patch(CV2)
    def test_draws_portal_up_arrow(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "portal",
                    "side": "up",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        args = mock_cv2.putText.call_args
        assert args[0][1] == "^"

    @patch(CV2)
    def test_draws_portal_down_arrow(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "portal",
                    "side": "down",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        args = mock_cv2.putText.call_args
        assert args[0][1] == "v"

    @patch(CV2)
    def test_portal_no_side_uses_o(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "portal",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        args = mock_cv2.putText.call_args
        assert args[0][1] == "O"

    @patch(CV2)
    def test_unknown_type_fallback_colour(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {
                    "type": "nonexistent_type",
                    "_pixel_bbox": [10, 20, 30, 40],
                },
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        # Should use fallback colour (128, 128, 128)
        rect_args = mock_cv2.rectangle.call_args
        assert rect_args[0][3] == (128, 128, 128)

    @patch(CV2)
    def test_multiple_squares(self, mock_cv2: MagicMock) -> None:
        mock_img = MagicMock()
        mock_cv2.imread.return_value = mock_img
        puzzle: dict[str, Any] = {
            "squares": [
                {"type": "player", "_pixel_bbox": [0, 0, 10, 10]},
                {"type": "goal", "_pixel_bbox": [20, 20, 10, 10]},
            ],
        }
        draw_debug("img.png", puzzle, "out.png")
        assert mock_cv2.rectangle.call_count == 2
        assert mock_cv2.putText.call_count == 2


# ── _assign_teleporter_and_kl_groups: inner p2-in-used branch ────────


class TestTeleporterInnerUsedSkip:
    def test_inner_loop_skips_already_used_p2(self) -> None:
        """Line 338: inner continue when p2 already in used set.

        Teleporters ordered so that after A pairs with C (skipping B),
        B's inner loop encounters the already-used C before finding D.
        """
        classified: dict[tuple[int, int], dict[str, Any]] = {
            (0, 0): {"type": "teleporter", "antenna_sides": ["up"]},
            (1, 0): {"type": "teleporter", "antenna_sides": ["down"]},
            (2, 0): {"type": "teleporter", "antenna_sides": ["up"]},
            (3, 0): {"type": "teleporter", "antenna_sides": ["down"]},
        }
        _assign_teleporter_and_kl_groups(classified)
        # (0,0) pairs with (2,0) by antenna match (both "up")
        assert classified[(0, 0)]["group"] == classified[(2, 0)]["group"]
        # (1,0) pairs with (3,0) by antenna match (both "down"),
        # after skipping already-used (2,0) in the inner loop
        assert classified[(1, 0)]["group"] == classified[(3, 0)]["group"]
        assert classified[(0, 0)]["group"] != classified[(1, 0)]["group"]
