"""
Unit tests for the new Page implementation to verify it meets the requirements:
1. Accepts a PageStyle that defines borders, line spacing and inter-block spacing
2. Makes an image canvas
3. Provides a method for accepting child objects
4. Provides methods for determining canvas size and border size
5. Has a method that calls render on all children
6. Has a method to query a point and determine which child it belongs to
"""
import unittest
import numpy as np
from PIL import Image
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.core.base import Renderable, Queriable


class SimpleTestRenderable(Renderable, Queriable):
    """A simple test renderable for testing the page system"""

    def __init__(self, text: str, size: tuple = (100, 50)):
        self._text = text
        self.size = size
        self._origin = np.array([0, 0])

    def render(self):
        """Render returns None - drawing is done via the page's draw object"""
        return None


class TestPageImplementation(unittest.TestCase):
    """Test cases for the Page class implementation"""

    def setUp(self):
        """Set up test fixtures"""
        self.basic_style = PageStyle(
            border_width=2,
            border_color=(255, 0, 0),
            line_spacing=8,
            inter_block_spacing=20,
            padding=(15, 15, 15, 15),
            background_color=(240, 240, 240)
        )

        self.page_size = (800, 600)

    def test_page_creation_with_style(self):
        """Test creating a page with a PageStyle"""
        page = Page(size=self.page_size, style=self.basic_style)

        self.assertEqual(page.size, self.page_size)
        self.assertEqual(page.style, self.basic_style)
        self.assertEqual(page.border_size, 2)

    def test_page_creation_without_style(self):
        """Test creating a page without a PageStyle (should use defaults)"""
        page = Page(size=self.page_size)

        self.assertEqual(page.size, self.page_size)
        self.assertIsNotNone(page.style)

    def test_page_canvas_and_content_sizes(self):
        """Test that page correctly calculates canvas and content sizes"""
        style = PageStyle(
            border_width=5,
            padding=(10, 20, 30, 40)  # top, right, bottom, left
        )

        page = Page(size=self.page_size, style=style)

        # Canvas size should be page size minus borders
        expected_canvas_size = (790, 590)  # 800-10, 600-10 (border on both sides)
        self.assertEqual(page.canvas_size, expected_canvas_size)

        # Content size should be canvas minus padding
        # 790-60, 590-40 (padding left+right, top+bottom)
        expected_content_size = (730, 550)
        self.assertEqual(page.content_size, expected_content_size)

    def test_page_add_remove_children(self):
        """Test adding and removing children from the page"""
        page = Page(size=self.page_size)

        # Initially no children
        self.assertEqual(len(page.children), 0)

        # Add children
        child1 = SimpleTestRenderable("Child 1")
        child2 = SimpleTestRenderable("Child 2")

        page.add_child(child1)
        self.assertEqual(len(page.children), 1)
        self.assertIn(child1, page.children)

        page.add_child(child2)
        self.assertEqual(len(page.children), 2)
        self.assertIn(child2, page.children)

        # Test method chaining
        child3 = SimpleTestRenderable("Child 3")
        result = page.add_child(child3)
        self.assertIs(result, page)  # Should return self for chaining
        self.assertEqual(len(page.children), 3)
        self.assertIn(child3, page.children)

        # Remove childce you’ll notice is that responses don’t stream
        # character-by-character like other providers. Instead, Claude Code
        # processes your full request before sending back the complete response.
        removed = page.remove_child(child2)
        self.assertTrue(removed)
        self.assertEqual(len(page.children), 2)
        self.assertNotIn(child2, page.children)

        # Try to remove non-existent child
        removed = page.remove_child(child2)
        self.assertFalse(removed)

        # Clear all children
        page.clear_children()
        self.assertEqual(len(page.children), 0)

    def test_page_render(self):
        """Test that page renders and creates a canvas"""
        style = PageStyle(
            border_width=2,
            border_color=(255, 0, 0),
            background_color=(255, 255, 255)
        )

        page = Page(size=(200, 150), style=style)

        # Add a child
        child = SimpleTestRenderable("Test child")
        page.add_child(child)

        # Render the page
        image = page.render()

        # Check that we got an image
        self.assertIsInstance(image, Image.Image)
        self.assertEqual(image.size, (200, 150))
        self.assertEqual(image.mode, 'RGBA')

        # Check that draw object is available
        self.assertIsNotNone(page.draw)

    def test_page_query_point(self):
        """Test querying points to find children"""
        page = Page(size=(400, 300))

        # Add children with known positions and sizes
        child1 = SimpleTestRenderable("Child 1", (100, 50))
        child2 = SimpleTestRenderable("Child 2", (80, 40))

        page.add_child(child1).add_child(child2)

        # Query points
        # Point within first child
        result = page.query_point((90, 30))
        self.assertIsNotNone(result)
        self.assertEqual(result.object, child1)

        # Point within second child
        result = page.query_point((30, 30))
        self.assertIsNotNone(result)
        self.assertEqual(result.object, child2)

        # Point outside any child - returns QueryResult with object_type "empty"
        result = page.query_point((300, 250))
        self.assertIsNotNone(result)
        self.assertEqual(result.object_type, "empty")

    def test_page_in_object(self):
        """Test that page correctly implements in_object"""
        page = Page(size=(400, 300))

        # Points within page bounds
        self.assertTrue(page.in_object((0, 0)))
        self.assertTrue(page.in_object((200, 150)))
        self.assertTrue(page.in_object((399, 299)))

        # Points outside page bounds
        self.assertFalse(page.in_object((-1, 0)))
        self.assertFalse(page.in_object((0, -1)))
        self.assertFalse(page.in_object((400, 299)))
        self.assertFalse(page.in_object((399, 300)))

    def test_page_with_borders(self):
        """Test page rendering with borders"""
        style = PageStyle(
            border_width=3,
            border_color=(128, 128, 128),
            background_color=(255, 255, 255)
        )

        page = Page(size=(100, 100), style=style)
        image = page.render()

        # Check that image was created
        self.assertIsInstance(image, Image.Image)
        self.assertEqual(image.size, (100, 100))

        # The border should be drawn but we can't easily test pixel values
        # Just verify the image exists and has the right properties

    def test_page_border_size_property(self):
        """Test that border_size property returns correct value"""
        # Test with border
        style_with_border = PageStyle(border_width=5)
        page_with_border = Page(size=self.page_size, style=style_with_border)
        self.assertEqual(page_with_border.border_size, 5)

        # Test without border
        style_no_border = PageStyle(border_width=0)
        page_no_border = Page(size=self.page_size, style=style_no_border)
        self.assertEqual(page_no_border.border_size, 0)

    def test_page_style_properties(self):
        """Test that page correctly exposes style properties"""
        page = Page(size=self.page_size, style=self.basic_style)

        # Test that style properties are accessible
        self.assertEqual(page.style.border_width, 2)
        self.assertEqual(page.style.border_color, (255, 0, 0))
        self.assertEqual(page.style.line_spacing, 8)
        self.assertEqual(page.style.inter_block_spacing, 20)
        self.assertEqual(page.style.padding, (15, 15, 15, 15))
        self.assertEqual(page.style.background_color, (240, 240, 240))

    def test_page_children_list_operations(self):
        """Test that children list behaves correctly"""
        page = Page(size=self.page_size)

        # Test that children is initially empty list
        self.assertIsInstance(page.children, list)
        self.assertEqual(len(page.children), 0)

        # Test adding multiple children
        children = [
            SimpleTestRenderable(f"Child {i}")
            for i in range(5)
        ]

        for child in children:
            page.add_child(child)

        self.assertEqual(len(page.children), 5)

        # Test that children are in the correct order
        for i, child in enumerate(page.children):
            self.assertEqual(child._text, f"Child {i}")

    def test_page_can_fit_line_boundary_checking(self):
        """Test that can_fit_line correctly checks bottom boundary"""
        # Create page with known dimensions
        # Page: 800x600, border: 40, padding: (10, 10, 10, 10)
        # Content area starts at y=50 (border + padding_top = 40 + 10)
        # Content area ends at y=550 (height - border - padding_bottom = 600 - 40 - 10)
        style = PageStyle(
            border_width=40,
            padding=(10, 10, 10, 10)
        )
        page = Page(size=(800, 600), style=style)

        # Initial y_offset should be at border + padding_top = 50
        self.assertEqual(page._current_y_offset, 50)

        # Test 1: Line that fits comfortably
        line_height = 20
        _max_y = 600 - 40 - 10  # 550
        self.assertTrue(page.can_fit_line(line_height))
        # Would end at 50 + 20 = 70, well within 550

        # Test 2: Simulate adding lines to fill the page
        # Available height: 550 - 50 = 500 pixels
        # With 20-pixel lines, we can fit 25 lines exactly
        for i in range(24):  # Add 24 lines
            self.assertTrue(page.can_fit_line(20), f"Line {i + 1} should fit")
            # Simulate adding a line by updating y_offset
            page._current_y_offset += 20

        # After 24 lines: y_offset = 50 + (24 * 20) = 530
        self.assertEqual(page._current_y_offset, 530)

        # Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at
        # boundary)
        self.assertTrue(page.can_fit_line(20))
        page._current_y_offset += 20
        self.assertEqual(page._current_y_offset, 550)

        # Test 4: Now another line should NOT fit (550 + 20 = 570 > 550)
        self.assertFalse(page.can_fit_line(20))

        # Test 5: Even a 1-pixel line should not fit (550 + 1 = 551 > 550)
        self.assertFalse(page.can_fit_line(1))

        # Test 6: Edge case - exactly at boundary, 0-height line should fit
        self.assertTrue(page.can_fit_line(0))

    def test_page_can_fit_line_with_different_styles(self):
        """Test can_fit_line with different page styles"""
        # Test with no border or padding
        style_no_border = PageStyle(border_width=0, padding=(0, 0, 0, 0))
        page_no_border = Page(size=(100, 100), style=style_no_border)

        # With no border/padding, y_offset starts at 0
        self.assertEqual(page_no_border._current_y_offset, 0)

        # Can fit a 100-pixel line exactly
        self.assertTrue(page_no_border.can_fit_line(100))

        # Cannot fit a 101-pixel line
        self.assertFalse(page_no_border.can_fit_line(101))

        # Test with large border and padding
        style_large = PageStyle(border_width=20, padding=(15, 15, 15, 15))
        page_large = Page(size=(200, 200), style=style_large)

        # y_offset starts at border + padding_top = 20 + 15 = 35
        self.assertEqual(page_large._current_y_offset, 35)

        # Max y = 200 - 20 - 15 = 165
        # Available height = 165 - 35 = 130 pixels
        self.assertTrue(page_large.can_fit_line(130))
        self.assertFalse(page_large.can_fit_line(131))


if __name__ == '__main__':
    unittest.main()
