import importlib
import sys
from contextlib import ExitStack as DoesNotRaise

import pytest

from supervision.draw.color import Color, ColorPalette, unify_to_bgr


@pytest.mark.parametrize(
    ("color_hex", "expected_result", "exception"),
    [
        ("fff", Color.WHITE, DoesNotRaise()),
        ("#fff", Color.WHITE, DoesNotRaise()),
        ("ffffff", Color.WHITE, DoesNotRaise()),
        ("#ffffff", Color.WHITE, DoesNotRaise()),
        ("f00", Color.RED, DoesNotRaise()),
        ("0f0", Color.GREEN, DoesNotRaise()),
        ("00f", Color.BLUE, DoesNotRaise()),
        ("#808000", Color(r=128, g=128, b=0), DoesNotRaise()),
        # RGBA hex codes (4 digits)
        ("f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
        ("#f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
        ("ffff", Color(r=255, g=255, b=255, a=255), DoesNotRaise()),
        ("f008", Color(r=255, g=0, b=0, a=136), DoesNotRaise()),
        # RGBA hex codes (8 digits)
        ("ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
        ("#ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
        ("ffffff00", Color(r=255, g=255, b=255, a=0), DoesNotRaise()),
        ("00ff00ff", Color(r=0, g=255, b=0, a=255), DoesNotRaise()),
        # Invalid hex codes
        ("", None, pytest.raises(ValueError, match="Invalid length of color hash")),
        ("00", None, pytest.raises(ValueError, match="Invalid length of color hash")),
        (
            "00000",
            None,
            pytest.raises(ValueError, match="Invalid length of color hash"),
        ),
        (
            "0000000",
            None,
            pytest.raises(ValueError, match="Invalid length of color hash"),
        ),
        (
            "000000000",
            None,
            pytest.raises(ValueError, match="Invalid length of color hash"),
        ),
        ("ffg", None, pytest.raises(ValueError, match="Invalid characters in color")),
        ("fffg", None, pytest.raises(ValueError, match="Invalid characters in color")),
        (
            "ff00ffgg",
            None,
            pytest.raises(ValueError, match="Invalid characters in color"),
        ),
    ],
)
def test_color_from_hex(
    color_hex, expected_result: Color | None, exception: Exception
) -> None:
    """
    Verify that Color.from_hex correctly parses various hex string formats.

    Scenario: Creating a `Color` object from various hex string formats (3-, 4-, 6-,
    and 8-digit, with/without # prefix).
    Expected: Correct RGBA values are parsed, and invalid hex strings raise
        `ValueError`. This allows users to define colors using familiar web formats
        including alpha.
    """
    with exception:
        result = Color.from_hex(color_hex=color_hex)
        assert result == expected_result


@pytest.mark.parametrize(
    ("color", "expected_result", "exception"),
    [
        (Color.WHITE, "#ffffff", DoesNotRaise()),
        (Color.BLACK, "#000000", DoesNotRaise()),
        (Color.RED, "#ff0000", DoesNotRaise()),
        (Color.GREEN, "#00ff00", DoesNotRaise()),
        (Color.BLUE, "#0000ff", DoesNotRaise()),
        (Color(r=128, g=128, b=0), "#808000", DoesNotRaise()),
        # With alpha channel
        (Color(r=255, g=0, b=255, a=128), "#ff00ff80", DoesNotRaise()),
        (Color(r=255, g=255, b=255, a=255), "#ffffff", DoesNotRaise()),
        (Color(r=0, g=255, b=0, a=0), "#00ff0000", DoesNotRaise()),
        (Color(r=128, g=128, b=0, a=200), "#808000c8", DoesNotRaise()),
    ],
)
def test_color_as_hex(
    color: Color, expected_result: str | None, exception: Exception
) -> None:
    """
    Verify that Color.as_hex correctly converts Color objects to hex strings.

    Scenario: Converting a `Color` object back to a hex string.
    Expected: 6-digit hex (#RRGGBB) when alpha is 255, 8-digit hex (#RRGGBBAA)
    otherwise, ensuring round-trip consistency for color definitions.
    """
    with exception:
        result = color.as_hex()
        assert result == expected_result


@pytest.mark.parametrize(
    ("color_tuple", "expected_result", "exception"),
    [
        ((255, 255, 255), Color.WHITE, DoesNotRaise()),
        ((0, 0, 0), Color.BLACK, DoesNotRaise()),
        ((255, 0, 0), Color.RED, DoesNotRaise()),
        ((0, 255, 0), Color.GREEN, DoesNotRaise()),
        ((0, 0, 255), Color.BLUE, DoesNotRaise()),
        ((128, 128, 0), Color(r=128, g=128, b=0), DoesNotRaise()),
        (
            (300, 0, 0),
            None,
            pytest.raises(ValueError, match=r"RGB values must be in range.*300, 0, 0"),
        ),  # R out of range
        (
            (0, -10, 0),
            None,
            pytest.raises(ValueError, match=r"RGB values must be in range.*0, -10, 0"),
        ),  # G out of range
        (
            (0, 0, 500),
            None,
            pytest.raises(ValueError, match=r"RGB values must be in range.*0, 0, 500"),
        ),  # B out of range
        (
            (300, -10, 500),
            None,
            pytest.raises(
                ValueError, match=r"RGB values must be in range.*300, -10, 500"
            ),
        ),  # All out of range
    ],
)
def test_color_from_rgb_tuple(
    color_tuple: tuple[int, int, int],
    expected_result: Color | None,
    exception: Exception,
) -> None:
    with exception:
        result = Color.from_rgb_tuple(color_tuple=color_tuple)
        assert result == expected_result


@pytest.mark.parametrize(
    ("color_tuple", "expected_result", "exception"),
    [
        ((255, 255, 255), Color.WHITE, DoesNotRaise()),
        ((0, 0, 0), Color.BLACK, DoesNotRaise()),
        ((0, 0, 255), Color.RED, DoesNotRaise()),  # BGR format
        ((0, 255, 0), Color.GREEN, DoesNotRaise()),  # BGR format
        ((255, 0, 0), Color.BLUE, DoesNotRaise()),  # BGR format
        ((0, 128, 128), Color(r=128, g=128, b=0), DoesNotRaise()),  # BGR format
        (
            (300, 0, 0),
            None,
            pytest.raises(ValueError, match=r"BGR values must be in range.*300, 0, 0"),
        ),  # B out of range
        (
            (0, -10, 0),
            None,
            pytest.raises(ValueError, match=r"BGR values must be in range.*0, -10, 0"),
        ),  # G out of range
        (
            (0, 0, 500),
            None,
            pytest.raises(ValueError, match=r"BGR values must be in range.*0, 0, 500"),
        ),  # R out of range
        (
            (300, -10, 500),
            None,
            pytest.raises(
                ValueError, match=r"BGR values must be in range.*300, -10, 500"
            ),
        ),  # All out of range
    ],
)
def test_color_from_bgr_tuple(
    color_tuple: tuple[int, int, int],
    expected_result: Color | None,
    exception: Exception,
) -> None:
    with exception:
        result = Color.from_bgr_tuple(color_tuple=color_tuple)
        assert result == expected_result


@pytest.mark.parametrize(
    ("color_tuple", "expected_result", "exception"),
    [
        ((255, 255, 0, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
        ((0, 255, 255, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
        ((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
        (
            (300, 0, 0, 128),
            None,
            pytest.raises(
                ValueError, match=r"RGBA values must be in range.*300, 0, 0, 128"
            ),
        ),
        (
            (0, 0, 0, 300),
            None,
            pytest.raises(
                ValueError, match=r"RGBA values must be in range.*0, 0, 0, 300"
            ),
        ),
    ],
)
def test_color_from_rgba_tuple(
    color_tuple: tuple[int, int, int, int],
    expected_result: Color | None,
    exception: Exception,
) -> None:
    with exception:
        result = Color.from_rgba_tuple(color_tuple=color_tuple)
        assert result == expected_result


@pytest.mark.parametrize(
    ("color_tuple", "expected_result", "exception"),
    [
        ((0, 255, 255, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
        ((255, 255, 0, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
        ((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
        (
            (300, 0, 0, 128),
            None,
            pytest.raises(
                ValueError, match=r"BGRA values must be in range.*300, 0, 0, 128"
            ),
        ),
        (
            (0, 0, 0, 300),
            None,
            pytest.raises(
                ValueError, match=r"BGRA values must be in range.*0, 0, 0, 300"
            ),
        ),
    ],
)
def test_color_from_bgra_tuple(
    color_tuple: tuple[int, int, int, int],
    expected_result: Color | None,
    exception: Exception,
) -> None:
    with exception:
        result = Color.from_bgra_tuple(color_tuple=color_tuple)
        assert result == expected_result


@pytest.mark.parametrize(
    "kwargs",
    [
        pytest.param({"r": -1, "g": 0, "b": 0}, id="red-negative"),
        pytest.param({"r": 0, "g": 256, "b": 0}, id="green-too-large"),
        pytest.param({"r": 0, "g": 0, "b": 300}, id="blue-too-large"),
        pytest.param({"r": 0, "g": 0, "b": 0, "a": -1}, id="alpha-negative"),
    ],
)
def test_color_constructor_rejects_out_of_range_channels(
    kwargs: dict[str, int],
) -> None:
    """Direct Color construction rejects channels outside the byte range."""
    with pytest.raises(ValueError, match="Color values must be in range"):
        Color(**kwargs)


@pytest.mark.parametrize(
    ("color", "expected_result", "exception"),
    [
        (Color(r=255, g=255, b=0, a=128), (255, 255, 0, 128), DoesNotRaise()),
        (Color(r=0, g=255, b=255, a=255), (0, 255, 255, 255), DoesNotRaise()),
        (Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
    ],
)
def test_color_as_rgba(
    color: Color,
    expected_result: tuple[int, int, int, int] | None,
    exception: Exception,
) -> None:
    with exception:
        result = color.as_rgba()
        assert result == expected_result


@pytest.mark.parametrize(
    ("color", "expected_result", "exception"),
    [
        (Color(r=255, g=255, b=0, a=128), (0, 255, 255, 128), DoesNotRaise()),
        (Color(r=0, g=255, b=255, a=255), (255, 255, 0, 255), DoesNotRaise()),
        (Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
    ],
)
def test_color_as_bgra(
    color: Color,
    expected_result: tuple[int, int, int, int] | None,
    exception: Exception,
) -> None:
    with exception:
        result = color.as_bgra()
        assert result == expected_result


@pytest.mark.parametrize(
    ("color", "expected_repr"),
    [
        (Color(r=255, g=255, b=255), "Color(r=255, g=255, b=255)"),
        (Color(r=0, g=0, b=0), "Color(r=0, g=0, b=0)"),
        (Color(r=255, g=0, b=255, a=255), "Color(r=255, g=0, b=255)"),
        (Color(r=255, g=0, b=255, a=128), "Color(r=255, g=0, b=255, a=128)"),
        (Color(r=255, g=0, b=255, a=0), "Color(r=255, g=0, b=255, a=0)"),
    ],
)
def test_color_repr(color: Color, expected_repr: str) -> None:
    assert repr(color) == expected_repr


@pytest.mark.parametrize(
    ("color_a", "color_b", "expect_equal_hash"),
    [
        (Color(r=255, g=0, b=0), Color(r=255, g=0, b=0), True),
        (Color(r=255, g=0, b=0, a=255), Color(r=255, g=0, b=0), True),
        (Color(r=255, g=0, b=0, a=128), Color(r=255, g=0, b=0, a=64), False),
        (Color(r=255, g=0, b=0, a=0), Color(r=255, g=0, b=0, a=255), False),
    ],
)
def test_color_hash(color_a: Color, color_b: Color, expect_equal_hash: bool) -> None:
    assert (hash(color_a) == hash(color_b)) == expect_equal_hash
    assert (color_a == color_b) == expect_equal_hash


def test_palette_accepts_negative_indices() -> None:
    """Negative palette indices wrap like standard Python sequences."""
    palette = ColorPalette.from_hex(["#ff0000", "#00ff00", "#0000ff"])

    assert palette.by_idx(-1) == Color.from_hex("#0000ff")


def test_empty_palette_lookup_raises() -> None:
    """Empty palette lookup raises a clear API error."""
    palette = ColorPalette.from_hex([])

    with pytest.raises(ValueError, match="at least one color"):
        palette.by_idx(0)


def test_unify_to_bgr_passes_through_bgr_tuple() -> None:
    """Tuple inputs are already BGR and must not be channel-swapped."""
    assert unify_to_bgr((1, 2, 3)) == (1, 2, 3)


def test_from_matplotlib_accepts_single_color() -> None:
    """A one-color matplotlib palette is valid and must not divide by zero."""
    palette = ColorPalette.from_matplotlib("jet", 1)

    assert len(palette) == 1


def test_draw_color_import_does_not_import_matplotlib_pyplot() -> None:
    """Importing draw.color keeps pyplot lazy until from_matplotlib is called."""
    sys.modules.pop("supervision.draw.color", None)
    sys.modules.pop("matplotlib.pyplot", None)

    importlib.import_module("supervision.draw.color")

    assert "matplotlib.pyplot" not in sys.modules
