"""Tests for the Pillow-based fallback OpenCV text implementation."""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import numpy as np
import pytest

from supervision._cv2._text import _get_text_size, _put_text
from supervision._cv2.constants import (
    _FONT_HERSHEY_COMPLEX,
    _FONT_HERSHEY_COMPLEX_SMALL,
    _FONT_HERSHEY_DUPLEX,
    _FONT_HERSHEY_PLAIN,
    _FONT_HERSHEY_SCRIPT_COMPLEX,
    _FONT_HERSHEY_SCRIPT_SIMPLEX,
    _FONT_HERSHEY_SIMPLEX,
    _FONT_HERSHEY_TRIPLEX,
    _FONT_ITALIC,
)

FONT_FACES = [
    pytest.param(_FONT_HERSHEY_SIMPLEX, id="simplex"),
    pytest.param(_FONT_HERSHEY_PLAIN, id="plain"),
    pytest.param(_FONT_HERSHEY_DUPLEX, id="duplex"),
    pytest.param(_FONT_HERSHEY_COMPLEX, id="complex"),
    pytest.param(_FONT_HERSHEY_TRIPLEX, id="triplex"),
    pytest.param(_FONT_HERSHEY_COMPLEX_SMALL, id="complex-small"),
    pytest.param(_FONT_HERSHEY_SCRIPT_SIMPLEX, id="script-simplex"),
    pytest.param(_FONT_HERSHEY_SCRIPT_COMPLEX, id="script-complex"),
]


def _run_without_opencv(source: str) -> subprocess.CompletedProcess[str]:
    """Run a Python snippet with imports of cv2 blocked."""
    environment = os.environ.copy()
    source_path = str(Path(__file__).resolve().parents[2] / "src")
    environment["PYTHONPATH"] = os.pathsep.join(
        filter(None, (source_path, environment.get("PYTHONPATH")))
    )
    return subprocess.run(
        [sys.executable, "-c", source],
        capture_output=True,
        text=True,
        env=environment,
    )


class TestGetTextSize:
    """Text-metric contract of the Pillow fallback getTextSize."""

    @pytest.mark.parametrize("font_face", FONT_FACES)
    @pytest.mark.parametrize("italic", [False, True], ids=["regular", "italic"])
    def test_returns_positive_metrics_for_every_face(
        self, font_face: int, italic: bool
    ) -> None:
        """Report positive width, height, and baseline for every accepted face."""
        actual_font_face = font_face | (_FONT_ITALIC if italic else 0)

        (width, height), baseline = _get_text_size(
            "Supervision", actual_font_face, 0.75, 2
        )

        assert width > 0
        assert height > 0
        assert baseline > 0

    def test_height_is_string_independent(self) -> None:
        """Match OpenCV's contract of a content-independent line height."""
        (_, short_height), _ = _get_text_size("i", _FONT_HERSHEY_SIMPLEX, 1.0, 1)
        (_, tall_height), _ = _get_text_size("Ag|", _FONT_HERSHEY_SIMPLEX, 1.0, 1)

        assert short_height == tall_height

    def test_width_grows_with_text_length(self) -> None:
        """Report a wider box for a longer string at the same scale."""
        (short_width, _), _ = _get_text_size("ab", _FONT_HERSHEY_SIMPLEX, 1.0, 1)
        (long_width, _), _ = _get_text_size("abcdef", _FONT_HERSHEY_SIMPLEX, 1.0, 1)

        assert long_width > short_width

    def test_metrics_grow_with_scale(self) -> None:
        """Report a larger box as the font scale increases."""
        (small_width, small_height), _ = _get_text_size(
            "text", _FONT_HERSHEY_SIMPLEX, 0.5, 1
        )
        (large_width, large_height), _ = _get_text_size(
            "text", _FONT_HERSHEY_SIMPLEX, 2.0, 1
        )

        assert large_width > small_width
        assert large_height > small_height


class TestPutText:
    """Rendering behavior of the Pillow fallback putText."""

    @pytest.mark.parametrize("font_face", FONT_FACES)
    @pytest.mark.parametrize("italic", [False, True], ids=["regular", "italic"])
    def test_renders_every_font_face_in_place(
        self, font_face: int, italic: bool
    ) -> None:
        """Draw visible text in place for every accepted font and italic combo."""
        image = np.zeros((96, 256, 3), dtype=np.uint8)
        actual_font_face = font_face | (_FONT_ITALIC if italic else 0)

        result = _put_text(
            image, "Supervision", (8, 60), actual_font_face, 0.75, (255, 255, 255), 1
        )

        assert result is image
        assert np.any(image)

    def test_empty_text_leaves_image_untouched(self) -> None:
        """Leave the scene unchanged when the string is empty."""
        image = np.zeros((32, 64, 3), dtype=np.uint8)

        result = _put_text(image, "", (4, 20), _FONT_HERSHEY_SIMPLEX, 1.0, (255, 0, 0))

        assert result is image
        assert not np.any(image)

    def test_rendered_text_stays_within_reported_box(self) -> None:
        """Keep drawn pixels within the getTextSize rectangle above the baseline."""
        image = np.zeros((120, 320, 3), dtype=np.uint8)
        org = (20, 80)
        (width, height), baseline = _get_text_size(
            "person 0.87", _FONT_HERSHEY_SIMPLEX, 1.0, 2
        )

        _put_text(
            image, "person 0.87", org, _FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2
        )

        rows, columns = np.nonzero(np.any(image, axis=2))
        assert columns.min() >= org[0]
        assert columns.max() <= org[0] + width
        assert rows.min() >= org[1] - height
        assert rows.max() <= org[1] + baseline

    def test_reported_baseline_encloses_thick_stroke_descent(self) -> None:
        """Keep the baseline padding matching the stroke_width putText renders.

        A thickness of 7 (stroke_width 6) is past the point where the old
        ``thickness // 2`` baseline formula under-padded relative to the
        ``thickness - 1`` stroke_width _put_text actually uses, letting
        descender pixels fall outside the reported box.
        """
        image = np.zeros((160, 320, 3), dtype=np.uint8)
        org = (20, 100)
        (_, height), baseline = _get_text_size(
            "person 0.87", _FONT_HERSHEY_SIMPLEX, 1.0, 7
        )

        _put_text(
            image, "person 0.87", org, _FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 7
        )

        rows, _ = np.nonzero(np.any(image, axis=2))
        assert rows.min() >= org[1] - height
        assert rows.max() <= org[1] + baseline

    def test_rejects_bottom_left_origin(self) -> None:
        """Reject the unsupported inverted-axis origin mode."""
        image = np.zeros((32, 64, 3), dtype=np.uint8)

        with pytest.raises(ValueError, match="bottomLeftOrigin"):
            _put_text(
                image,
                "x",
                (4, 20),
                _FONT_HERSHEY_SIMPLEX,
                1.0,
                (255, 255, 255),
                bottomLeftOrigin=True,
            )

    def test_clips_text_at_image_boundaries(self) -> None:
        """Clip the cropped Pillow mask when the baseline starts off-canvas."""
        image = np.zeros((32, 64, 3), dtype=np.uint8)

        result = _put_text(
            image, "clip", (-8, 12), _FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255)
        )

        assert result is image
        assert np.any(image)


class TestFallbackWithoutOpenCV:
    """Production-shaped text calls with OpenCV imports blocked."""

    def test_facade_text_works_when_opencv_is_blocked(self) -> None:
        """Exercise the facade getTextSize and putText in a cv2-free subprocess."""
        completed = _run_without_opencv(
            """
import sys


class BlockCv2:
    def find_spec(self, fullname, path=None, target=None):
        if fullname == "cv2":
            raise ModuleNotFoundError("blocked for test")
        return None


sys.meta_path.insert(0, BlockCv2())

import numpy as np

from supervision import _cv2

assert _cv2.BACKEND_NAME == "fallback"
size, baseline = _cv2.getTextSize(
    "Fallback", _cv2.FONT_HERSHEY_COMPLEX | _cv2.FONT_ITALIC, 0.75, 2
)
assert size[0] > 0
assert baseline > 0
image = np.zeros((96, 256, 3), dtype=np.uint8)
result = _cv2.putText(
    image,
    "Fallback",
    (8, 60),
    _cv2.FONT_HERSHEY_COMPLEX | _cv2.FONT_ITALIC,
    0.75,
    (255, 255, 255),
    1,
)
assert result is image
assert np.any(image)
"""
        )

        assert completed.returncode == 0, completed.stderr

    def test_draw_text_consumer_uses_fallback_without_opencv(self) -> None:
        """Exercise the public draw_text consumer in a cv2-free subprocess."""
        completed = _run_without_opencv(
            """
import sys


class BlockCv2:
    def find_spec(self, fullname, path=None, target=None):
        if fullname == "cv2":
            raise ModuleNotFoundError("blocked for test")
        return None


sys.meta_path.insert(0, BlockCv2())

import numpy as np

from supervision.draw.color import Color
from supervision.draw.utils import draw_text
from supervision.geometry.core import Point

image = np.zeros((96, 256, 3), dtype=np.uint8)
result = draw_text(
    image,
    "Fallback",
    Point(80, 40),
    text_font=7,
    text_scale=0.75,
    text_color=Color.WHITE,
)
assert result is image
assert np.any(image)
"""
        )

        assert completed.returncode == 0, completed.stderr
