"""
Unit tests for engine.canvas.Canvas.

Tests the core 2D rendering surface without any display dependencies.
"""

from engine.canvas import Canvas, CanvasRegion


class TestCanvasRegion:
    """Tests for CanvasRegion dataclass."""

    def test_is_valid_positive_dimensions(self):
        """Positive width and height returns True."""
        region = CanvasRegion(0, 0, 10, 5)
        assert region.is_valid() is True

    def test_is_valid_zero_width(self):
        """Zero width returns False."""
        region = CanvasRegion(0, 0, 0, 5)
        assert region.is_valid() is False

    def test_is_valid_zero_height(self):
        """Zero height returns False."""
        region = CanvasRegion(0, 0, 10, 0)
        assert region.is_valid() is False

    def test_is_valid_negative_dimensions(self):
        """Negative dimensions return False."""
        region = CanvasRegion(0, 0, -1, 5)
        assert region.is_valid() is False

    def test_rows_computes_correct_set(self):
        """rows() returns set of row indices in region."""
        region = CanvasRegion(2, 3, 4, 2)
        assert region.rows() == {3, 4}


class TestCanvas:
    """Tests for Canvas class."""

    def test_init_default_dimensions(self):
        """Default width=80, height=24."""
        canvas = Canvas()
        assert canvas.width == 80
        assert canvas.height == 24
        assert len(canvas._grid) == 24
        assert len(canvas._grid[0]) == 80

    def test_init_custom_dimensions(self):
        """Custom dimensions are set correctly."""
        canvas = Canvas(100, 50)
        assert canvas.width == 100
        assert canvas.height == 50

    def test_clear_empties_grid(self):
        """clear() resets all cells to spaces."""
        canvas = Canvas(5, 3)
        canvas.put_text(0, 0, "Hello")
        canvas.clear()
        region = canvas.get_region(0, 0, 5, 3)
        assert all(all(cell == " " for cell in row) for row in region)

    def test_clear_marks_entire_canvas_dirty(self):
        """clear() marks entire canvas as dirty."""
        canvas = Canvas(10, 5)
        canvas.clear()
        dirty = canvas.get_dirty_regions()
        assert len(dirty) == 1
        assert dirty[0].x == 0 and dirty[0].y == 0
        assert dirty[0].width == 10 and dirty[0].height == 5

    def test_put_text_single_char(self):
        """put_text writes a single character at position."""
        canvas = Canvas(10, 5)
        canvas.put_text(3, 2, "X")
        assert canvas._grid[2][3] == "X"

    def test_put_text_multiple_chars(self):
        """put_text writes multiple characters in a row."""
        canvas = Canvas(10, 5)
        canvas.put_text(2, 1, "ABC")
        assert canvas._grid[1][2] == "A"
        assert canvas._grid[1][3] == "B"
        assert canvas._grid[1][4] == "C"

    def test_put_text_ignores_overflow_right(self):
        """Characters beyond width are ignored."""
        canvas = Canvas(5, 5)
        canvas.put_text(3, 0, "XYZ")
        assert canvas._grid[0][3] == "X"
        assert canvas._grid[0][4] == "Y"
        # Z would be at index 5, which is out of bounds

    def test_put_text_ignores_overflow_bottom(self):
        """Rows beyond height are ignored."""
        canvas = Canvas(5, 3)
        canvas.put_text(0, 5, "test")
        # Row 5 doesn't exist, nothing should be written
        assert all(cell == " " for row in canvas._grid for cell in row)

    def test_put_text_marks_dirty_region(self):
        """put_text marks the written area as dirty."""
        canvas = Canvas(10, 5)
        canvas.put_text(2, 1, "Hello")
        dirty = canvas.get_dirty_regions()
        assert len(dirty) == 1
        assert dirty[0].x == 2 and dirty[0].y == 1
        assert dirty[0].width == 5 and dirty[0].height == 1

    def test_put_text_empty_string_no_dirty(self):
        """Empty string does not create dirty region."""
        canvas = Canvas(10, 5)
        canvas.put_text(0, 0, "")
        assert not canvas.is_dirty()

    def test_put_region_single_cell(self):
        """put_region writes a single cell correctly."""
        canvas = Canvas(5, 5)
        content = [["X"]]
        canvas.put_region(2, 2, content)
        assert canvas._grid[2][2] == "X"

    def test_put_region_multiple_rows(self):
        """put_region writes multiple rows correctly."""
        canvas = Canvas(10, 10)
        content = [["A", "B"], ["C", "D"]]
        canvas.put_region(1, 1, content)
        assert canvas._grid[1][1] == "A"
        assert canvas._grid[1][2] == "B"
        assert canvas._grid[2][1] == "C"
        assert canvas._grid[2][2] == "D"

    def test_put_region_partial_out_of_bounds(self):
        """put_region clips content that extends beyond canvas bounds."""
        canvas = Canvas(5, 5)
        content = [["A", "B", "C"], ["D", "E", "F"]]
        canvas.put_region(4, 4, content)
        # Only cell (4,4) should be within bounds
        assert canvas._grid[4][4] == "A"
        # Others are out of bounds
        assert canvas._grid[4][5] == " " if 5 < 5 else True  # index 5 doesn't exist
        assert canvas._grid[5][4] == " " if 5 < 5 else True  # row 5 doesn't exist

    def test_put_region_marks_dirty(self):
        """put_region marks dirty region covering written area (clipped)."""
        canvas = Canvas(10, 10)
        content = [["A", "B", "C"], ["D", "E", "F"]]
        canvas.put_region(2, 2, content)
        dirty = canvas.get_dirty_regions()
        assert len(dirty) == 1
        assert dirty[0].x == 2 and dirty[0].y == 2
        assert dirty[0].width == 3 and dirty[0].height == 2

    def test_fill_rectangle(self):
        """fill() fills a rectangular region with character."""
        canvas = Canvas(10, 10)
        canvas.fill(2, 2, 3, 2, "*")
        for y in range(2, 4):
            for x in range(2, 5):
                assert canvas._grid[y][x] == "*"

    def test_fill_entire_canvas(self):
        """fill() can fill entire canvas."""
        canvas = Canvas(5, 3)
        canvas.fill(0, 0, 5, 3, "#")
        for row in canvas._grid:
            assert all(cell == "#" for cell in row)

    def test_fill_empty_region_no_dirty(self):
        """fill with zero dimensions does not mark dirty."""
        canvas = Canvas(10, 10)
        canvas.fill(0, 0, 0, 5, "X")
        assert not canvas.is_dirty()

    def test_fill_clips_to_bounds(self):
        """fill clips to canvas boundaries."""
        canvas = Canvas(5, 5)
        canvas.fill(3, 3, 5, 5, "X")
        # Should only fill within bounds: (3,3) to (4,4)
        assert canvas._grid[3][3] == "X"
        assert canvas._grid[3][4] == "X"
        assert canvas._grid[4][3] == "X"
        assert canvas._grid[4][4] == "X"
        # Out of bounds should remain spaces
        assert canvas._grid[5] if 5 < 5 else True  # row 5 doesn't exist

    def test_get_region_extracts_subgrid(self):
        """get_region returns correct rectangular subgrid."""
        canvas = Canvas(10, 10)
        for y in range(10):
            for x in range(10):
                canvas._grid[y][x] = chr(ord("A") + (x % 26))
        region = canvas.get_region(2, 3, 4, 2)
        assert len(region) == 2
        assert len(region[0]) == 4
        assert region[0][0] == "C"  # (2,3) = 'C'
        assert region[1][2] == "E"  # (4,4) = 'E'

    def test_get_region_out_of_bounds_returns_spaces(self):
        """get_region pads out-of-bounds areas with spaces."""
        canvas = Canvas(5, 5)
        canvas.put_text(0, 0, "HELLO")
        # Region overlapping right edge: cols 3-4 inside, col5+ outside
        region = canvas.get_region(3, 0, 5, 2)
        assert region[0][0] == "L"
        assert region[0][1] == "O"
        assert region[0][2] == " "  # col5 out of bounds
        assert all(cell == " " for cell in region[1])

    def test_get_region_flat_returns_lines(self):
        """get_region_flat returns list of joined strings."""
        canvas = Canvas(10, 5)
        canvas.put_text(0, 0, "FIRST")
        canvas.put_text(0, 1, "SECOND")
        flat = canvas.get_region_flat(0, 0, 6, 2)
        assert flat == ["FIRST ", "SECOND"]

    def test_mark_dirty_manual(self):
        """mark_dirty() can be called manually to mark arbitrary region."""
        canvas = Canvas(10, 10)
        canvas.mark_dirty(5, 5, 3, 2)
        dirty = canvas.get_dirty_regions()
        assert len(dirty) == 1
        assert dirty[0] == CanvasRegion(5, 5, 3, 2)

    def test_get_dirty_rows_union(self):
        """get_dirty_rows() returns union of all dirty row indices."""
        canvas = Canvas(10, 10)
        canvas.put_text(0, 0, "A")  # row 0
        canvas.put_text(0, 2, "B")  # row 2
        canvas.mark_dirty(0, 1, 1, 1)  # row 1
        rows = canvas.get_dirty_rows()
        assert rows == {0, 1, 2}

    def test_is_dirty_after_operations(self):
        """is_dirty() returns True after any modifying operation."""
        canvas = Canvas(10, 10)
        assert not canvas.is_dirty()
        canvas.put_text(0, 0, "X")
        assert canvas.is_dirty()
        _ = canvas.get_dirty_regions()  # resets
        assert not canvas.is_dirty()

    def test_resize_same_size_no_change(self):
        """resize with same dimensions does nothing."""
        canvas = Canvas(10, 5)
        canvas.put_text(0, 0, "TEST")
        canvas.resize(10, 5)
        assert canvas._grid[0][0] == "T"

    def test_resize_larger_preserves_content(self):
        """resize to larger canvas preserves existing content."""
        canvas = Canvas(5, 3)
        canvas.put_text(1, 1, "AB")
        canvas.resize(10, 6)
        assert canvas.width == 10
        assert canvas.height == 6
        assert canvas._grid[1][1] == "A"
        assert canvas._grid[1][2] == "B"
        # New area should be spaces
        assert canvas._grid[0][0] == " "

    def test_resize_smaller_truncates(self):
        """resize to smaller canvas drops content outside new bounds."""
        canvas = Canvas(10, 5)
        canvas.put_text(8, 4, "XYZ")
        canvas.resize(5, 3)
        assert canvas.width == 5
        assert canvas.height == 3
        # Content at (8,4) should be lost
        # But content within new bounds should remain
        canvas2 = Canvas(10, 5)
        canvas2.put_text(2, 2, "HI")
        canvas2.resize(5, 3)
        assert canvas2._grid[2][2] == "H"

    def test_resize_does_not_auto_mark_dirty(self):
        """resize() does not automatically mark dirty (caller responsibility)."""
        canvas = Canvas(10, 10)
        canvas.put_text(0, 0, "A")
        _ = canvas.get_dirty_regions()  # reset
        canvas.resize(5, 5)
        # Resize doesn't mark dirty - this is current implementation
        assert not canvas.is_dirty()
