"""
Unit tests for engine.display.renderer module.

Tests ANSI parsing and PIL rendering utilities.
"""

import pytest

try:
    from PIL import Image

    PIL_AVAILABLE = True
except ImportError:
    PIL_AVAILABLE = False

from engine.display.renderer import ANSI_COLORS, parse_ansi, render_to_pil


class TestParseANSI:
    """Tests for parse_ansi function."""

    def test_plain_text(self):
        """Plain text without ANSI codes returns single token."""
        tokens = parse_ansi("Hello World")
        assert len(tokens) == 1
        assert tokens[0][0] == "Hello World"
        # Check default colors
        assert tokens[0][1] == (204, 204, 204)  # fg
        assert tokens[0][2] == (0, 0, 0)  # bg
        assert tokens[0][3] is False  # bold

    def test_empty_string(self):
        """Empty string returns single empty token."""
        tokens = parse_ansi("")
        assert tokens == [("", (204, 204, 204), (0, 0, 0), False)]

    def test_reset_code(self):
        """Reset code (ESC[0m) restores defaults."""
        tokens = parse_ansi("\x1b[31mRed\x1b[0mNormal")
        assert len(tokens) == 2
        assert tokens[0][0] == "Red"
        # Red fg should be ANSI_COLORS[1]
        assert tokens[0][1] == ANSI_COLORS[1]
        assert tokens[1][0] == "Normal"
        assert tokens[1][1] == (204, 204, 204)  # back to default

    def test_bold_code(self):
        """Bold code (ESC[1m) sets bold flag."""
        tokens = parse_ansi("\x1b[1mBold")
        assert tokens[0][3] is True

    def test_bold_off_code(self):
        """Bold off (ESC[22m) clears bold."""
        tokens = parse_ansi("\x1b[1mBold\x1b[22mNormal")
        assert tokens[0][3] is True
        assert tokens[1][3] is False

    def test_4bit_foreground_colors(self):
        """4-bit foreground colors (30-37, 90-97) work."""
        # Test normal red (31)
        tokens = parse_ansi("\x1b[31mRed")
        assert tokens[0][1] == ANSI_COLORS[1]  # color 1 = red

        # Test bright cyan (96) - maps to index 14 (bright cyan)
        tokens = parse_ansi("\x1b[96mCyan")
        assert tokens[0][1] == ANSI_COLORS[14]  # bright cyan

    def test_4bit_background_colors(self):
        """4-bit background colors (40-47, 100-107) work."""
        # Green bg = 42
        tokens = parse_ansi("\x1b[42mText")
        assert tokens[0][2] == ANSI_COLORS[2]  # color 2 = green

        # Bright magenta bg = 105
        tokens = parse_ansi("\x1b[105mText")
        assert tokens[0][2] == ANSI_COLORS[13]  # bright magenta (13)

    def test_multiple_ansi_codes_in_sequence(self):
        """Multiple codes in one escape sequence are parsed."""
        tokens = parse_ansi("\x1b[1;31;42mBold Red on Green")
        assert tokens[0][0] == "Bold Red on Green"
        assert tokens[0][3] is True  # bold
        assert tokens[0][1] == ANSI_COLORS[1]  # red fg
        assert tokens[0][2] == ANSI_COLORS[2]  # green bg

    def test_nested_ansi_sequences(self):
        """Multiple separate ANSI sequences are tokenized correctly."""
        text = "\x1b[31mRed\x1b[32mGreen\x1b[0mNormal"
        tokens = parse_ansi(text)
        assert len(tokens) == 3
        assert tokens[0][0] == "Red"
        assert tokens[1][0] == "Green"
        assert tokens[2][0] == "Normal"

    def test_interleaved_text_and_ansi(self):
        """Text before and after ANSI codes is tokenized."""
        tokens = parse_ansi("Pre\x1b[31mRedPost")
        assert len(tokens) == 2
        assert tokens[0][0] == "Pre"
        assert tokens[1][0] == "RedPost"
        assert tokens[1][1] == ANSI_COLORS[1]

    def test_all_standard_4bit_colors(self):
        """All 4-bit color indices (0-15) map to valid RGB."""
        for i in range(16):
            tokens = parse_ansi(f"\x1b[{i}mX")
            # Should be a defined color or default fg
            fg = tokens[0][1]
            valid = fg in ANSI_COLORS.values() or fg == (204, 204, 204)
            assert valid, f"Color {i} produced invalid fg {fg}"

    def test_unknown_code_ignored(self):
        """Unknown numeric codes are ignored, keep current style."""
        tokens = parse_ansi("\x1b[99mText")
        # 99 is not recognized, should keep previous state (defaults)
        assert tokens[0][1] == (204, 204, 204)


@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL not available")
class TestRenderToPIL:
    """Tests for render_to_pil function (requires PIL)."""

    def test_renders_plain_text(self):
        """Plain buffer renders as image."""
        buffer = ["Hello"]
        img = render_to_pil(buffer, width=10, height=1)
        assert isinstance(img, Image.Image)
        assert img.mode == "RGBA"

    def test_renders_with_ansi_colors(self):
        """Buffer with ANSI colors renders correctly."""
        buffer = ["\x1b[31mRed\x1b[0mNormal"]
        img = render_to_pil(buffer, width=20, height=1)
        assert isinstance(img, Image.Image)

    def test_multi_line_buffer(self):
        """Multiple lines render with correct height."""
        buffer = ["Line1", "Line2", "Line3"]
        img = render_to_pil(buffer, width=10, height=3)
        # Height should be approximately 3 * cell_height (18-2 padding)
        assert img.height > 0

    def test_clipping_to_height(self):
        """Buffer longer than height is clipped."""
        buffer = ["Line1", "Line2", "Line3", "Line4"]
        img = render_to_pil(buffer, width=10, height=2)
        # Should only render 2 lines
        assert img.height < img.width * 2  # roughly 2 lines tall

    def test_cell_dimensions_respected(self):
        """Custom cell_width and cell_height are used."""
        buffer = ["Test"]
        img = render_to_pil(buffer, width=5, height=1, cell_width=20, cell_height=25)
        assert img.width == 5 * 20
        assert img.height == 25

    def test_font_fallback_on_invalid(self):
        """Invalid font path falls back to default font."""
        buffer = ["Test"]
        # Should not crash with invalid font path
        img = render_to_pil(
            buffer, width=5, height=1, font_path="/nonexistent/font.ttf"
        )
        assert isinstance(img, Image.Image)
