"""
Unit tests for the query system (pixel-to-content mapping).

Tests the QueryResult, SelectionRange, and query_point functionality
across Page, Line, and Text classes.
"""

import unittest
import numpy as np
from PIL import Image, ImageDraw

from pyWebLayout.core.query import QueryResult, SelectionRange
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Text, Line
from pyWebLayout.concrete.functional import LinkText
from pyWebLayout.abstract.inline import Word
from pyWebLayout.abstract.functional import Link, LinkType
from pyWebLayout.style.page_style import PageStyle
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests


class TestQueryResult(unittest.TestCase):
    """Test QueryResult dataclass"""

    def test_init_basic(self):
        """Test basic QueryResult creation"""
        obj = object()
        result = QueryResult(
            object=obj,
            object_type="text",
            bounds=(100, 200, 50, 20)
        )

        self.assertEqual(result.object, obj)
        self.assertEqual(result.object_type, "text")
        self.assertEqual(result.bounds, (100, 200, 50, 20))
        self.assertIsNone(result.text)
        self.assertFalse(result.is_interactive)

    def test_init_with_metadata(self):
        """Test QueryResult with full metadata"""
        obj = object()
        result = QueryResult(
            object=obj,
            object_type="link",
            bounds=(100, 200, 50, 20),
            text="Click here",
            is_interactive=True,
            link_target="chapter2"
        )

        self.assertEqual(result.text, "Click here")
        self.assertTrue(result.is_interactive)
        self.assertEqual(result.link_target, "chapter2")

    def test_to_dict(self):
        """Test QueryResult serialization"""
        result = QueryResult(
            object=object(),
            object_type="link",
            bounds=(100, 200, 50, 20),
            text="Click here",
            is_interactive=True,
            link_target="chapter2"
        )

        d = result.to_dict()
        self.assertEqual(d['object_type'], "link")
        self.assertEqual(d['bounds'], (100, 200, 50, 20))
        self.assertEqual(d['text'], "Click here")
        self.assertTrue(d['is_interactive'])
        self.assertEqual(d['link_target'], "chapter2")


class TestSelectionRange(unittest.TestCase):
    """Test SelectionRange dataclass"""

    def test_init(self):
        """Test SelectionRange creation"""
        results = []
        sel_range = SelectionRange(
            start_point=(10, 20),
            end_point=(100, 30),
            results=results
        )

        self.assertEqual(sel_range.start_point, (10, 20))
        self.assertEqual(sel_range.end_point, (100, 30))
        self.assertEqual(sel_range.results, results)

    def test_text_property(self):
        """Test concatenated text extraction"""
        results = [
            QueryResult(object(), "text", (0, 0, 0, 0), text="Hello"),
            QueryResult(object(), "text", (0, 0, 0, 0), text="world"),
            QueryResult(object(), "text", (0, 0, 0, 0), text="test")
        ]

        sel_range = SelectionRange((0, 0), (100, 100), results)
        self.assertEqual(sel_range.text, "Hello world test")

    def test_bounds_list_property(self):
        """Test bounds list extraction"""
        results = [
            QueryResult(object(), "text", (10, 20, 30, 15), text="Hello"),
            QueryResult(object(), "text", (45, 20, 35, 15), text="world")
        ]

        sel_range = SelectionRange((0, 0), (100, 100), results)
        bounds = sel_range.bounds_list

        self.assertEqual(len(bounds), 2)
        self.assertEqual(bounds[0], (10, 20, 30, 15))
        self.assertEqual(bounds[1], (45, 20, 35, 15))

    def test_to_dict(self):
        """Test SelectionRange serialization"""
        results = [
            QueryResult(object(), "text", (10, 20, 30, 15), text="Hello"),
            QueryResult(object(), "text", (45, 20, 35, 15), text="world")
        ]

        sel_range = SelectionRange((10, 20), (80, 35), results)
        d = sel_range.to_dict()

        self.assertEqual(d['start'], (10, 20))
        self.assertEqual(d['end'], (80, 35))
        self.assertEqual(d['text'], "Hello world")
        self.assertEqual(d['word_count'], 2)
        self.assertEqual(len(d['bounds']), 2)


class TestTextQueryPoint(unittest.TestCase):
    """Test Text class in_object (from Queriable mixin)"""

    def setUp(self):
        ensure_consistent_font_in_tests()
        self.canvas = Image.new('RGB', (800, 600), color='white')
        self.draw = ImageDraw.Draw(self.canvas)
        self.font = create_default_test_font()

    def test_in_object_hit(self):
        """Test in_object returns True for point inside text"""
        text = Text("Hello", self.font, self.draw)
        text.set_origin(np.array([100, 100]))

        # Point inside text bounds
        # Origin is at baseline (100, 100), so test a point slightly above (at ascent/2)
        # and to the right
        self.assertTrue(text.in_object(np.array([110, 100])))

    def test_in_object_miss(self):
        """Test in_object returns False for point outside text"""
        text = Text("Hello", self.font, self.draw)
        text.set_origin(np.array([100, 100]))

        # Point outside text bounds
        self.assertFalse(text.in_object(np.array([50, 50])))
        self.assertFalse(text.in_object(np.array([200, 200])))


class TestLineQueryPoint(unittest.TestCase):
    """Test Line.query_point method"""

    def setUp(self):
        ensure_consistent_font_in_tests()
        self.canvas = Image.new('RGB', (800, 600), color='white')
        self.draw = ImageDraw.Draw(self.canvas)
        self.font = create_default_test_font()

    def test_query_point_finds_text(self):
        """Test Line.query_point finds a text object"""
        line = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.draw,
            font=self.font
        )

        # Add text objects
        word1 = Word("Hello", self.font)
        word2 = Word("world", self.font)

        line.add_word(word1)
        line.add_word(word2)
        line.render()

        # Query a point that should hit first word
        # (after rendering, text objects have positions set)
        if len(line._text_objects) > 0:
            text_obj = line._text_objects[0]
            # Origin is at baseline, so query at baseline position (Y = origin[1])
            # with X offset into the text
            point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1]))

            result = line.query_point(point)

            self.assertIsNotNone(result)
            self.assertEqual(result.object_type, "text")
            self.assertIsNotNone(result.text)

    def test_query_point_miss(self):
        """Test Line.query_point returns None for miss"""
        line = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.draw,
            font=self.font
        )

        word1 = Word("Hello", self.font)
        line.add_word(word1)
        line.render()

        # Query far outside line bounds
        result = line.query_point((10, 10))
        self.assertIsNone(result)

    def test_query_point_finds_link(self):
        """Test Line.query_point correctly identifies links"""
        line = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.draw,
            font=self.font
        )

        # Create a linked word
        from pyWebLayout.abstract.inline import LinkedWord
        linked_word = LinkedWord("Click", self.font, "chapter2", LinkType.INTERNAL)

        line.add_word(linked_word)
        line.render()

        # Query the link
        if len(line._text_objects) > 0:
            text_obj = line._text_objects[0]
            # Origin is at baseline, query at baseline Y position
            point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1]))

            result = line.query_point(point)

            self.assertIsNotNone(result)
            self.assertEqual(result.object_type, "link")
            self.assertTrue(result.is_interactive)
            self.assertEqual(result.link_target, "chapter2")


class TestPageQueryPoint(unittest.TestCase):
    """Test Page.query_point method"""

    def setUp(self):
        ensure_consistent_font_in_tests()
        self.page = Page(size=(800, 1000), style=PageStyle())
        self.font = create_default_test_font()

    def test_query_point_empty_page(self):
        """Test querying empty page returns empty result"""
        result = self.page.query_point((400, 500))

        self.assertIsNotNone(result)
        self.assertEqual(result.object_type, "empty")
        self.assertEqual(result.object, self.page)

    def test_query_point_finds_line(self):
        """Test Page.query_point traverses to Line"""
        line = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.page.draw,
            font=self.font
        )

        word = Word("Hello", self.font)
        line.add_word(word)
        line.render()

        self.page.add_child(line)

        # Query a point inside the line
        if len(line._text_objects) > 0:
            text_obj = line._text_objects[0]
            # Origin is at baseline, query at baseline Y position
            point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1]))

            result = self.page.query_point(point)

            # Should traverse Page → Line → Text
            self.assertIsNotNone(result)
            self.assertEqual(result.object_type, "text")
            self.assertEqual(result.parent_page, self.page)

    def test_query_point_multiple_lines(self):
        """Test Page.query_point with multiple lines"""
        # Add two lines at different Y positions
        line1 = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.page.draw,
            font=self.font
        )
        line2 = Line(
            spacing=(5, 10),
            origin=np.array([50, 150]),
            size=(700, 30),
            draw=self.page.draw,
            font=self.font
        )

        word1 = Word("First", self.font)
        word2 = Word("Second", self.font)

        line1.add_word(word1)
        line2.add_word(word2)

        line1.render()
        line2.render()

        self.page.add_child(line1)
        self.page.add_child(line2)

        # Query first line
        if len(line1._text_objects) > 0:
            text_obj1 = line1._text_objects[0]
            # Origin is at baseline, query at baseline Y position
            point1 = (int(text_obj1._origin[0] + 5), int(text_obj1._origin[1]))
            result1 = self.page.query_point(point1)

            self.assertIsNotNone(result1)
            self.assertEqual(result1.text, "First")

        # Query second line
        if len(line2._text_objects) > 0:
            text_obj2 = line2._text_objects[0]
            # Origin is at baseline, query at baseline Y position
            point2 = (int(text_obj2._origin[0] + 5), int(text_obj2._origin[1]))
            result2 = self.page.query_point(point2)

            self.assertIsNotNone(result2)
            self.assertEqual(result2.text, "Second")


class TestPageQueryRange(unittest.TestCase):
    """Test Page.query_range method for text selection"""

    def setUp(self):
        ensure_consistent_font_in_tests()
        self.page = Page(size=(800, 1000), style=PageStyle())
        self.font = create_default_test_font()

    def test_query_range_single_line(self):
        """Test selecting text within a single line"""
        line = Line(
            spacing=(5, 10),
            origin=np.array([50, 100]),
            size=(700, 30),
            draw=self.page.draw,
            font=self.font
        )

        # Add multiple words
        words = [Word(text, self.font) for text in ["Hello", "world", "test"]]
        for word in words:
            line.add_word(word)

        line.render()
        self.page.add_child(line)

        if len(line._text_objects) >= 2:
            # Select from first to second word
            start_text = line._text_objects[0]
            end_text = line._text_objects[1]

            # Origin is at baseline, query at baseline Y position
            start_point = (
                int(start_text._origin[0] + 5), int(start_text._origin[1]))
            end_point = (int(end_text._origin[0] + 5), int(end_text._origin[1]))

            sel_range = self.page.query_range(start_point, end_point)

            self.assertIsNotNone(sel_range)
            self.assertGreater(len(sel_range.results), 0)
            self.assertIn("Hello", sel_range.text)

    def test_query_range_invalid(self):
        """Test query_range with invalid points returns empty"""
        sel_range = self.page.query_range((10, 10), (20, 20))

        self.assertEqual(len(sel_range.results), 0)
        self.assertEqual(sel_range.text, "")


class TestPageMakeQueryResult(unittest.TestCase):
    """Test Page._make_query_result helper"""

    def setUp(self):
        ensure_consistent_font_in_tests()
        self.page = Page(size=(800, 1000), style=PageStyle())
        self.font = create_default_test_font()
        self.draw = self.page.draw

    def test_make_query_result_text(self):
        """Test packaging regular Text object"""
        text = Text("Hello", self.font, self.draw)
        text.set_origin(np.array([100, 200]))

        result = self.page._make_query_result(text, (105, 205))

        self.assertEqual(result.object_type, "text")
        self.assertEqual(result.text, "Hello")
        self.assertFalse(result.is_interactive)

    def test_make_query_result_link(self):
        """Test packaging LinkText object"""
        link = Link(location="chapter2", link_type=LinkType.INTERNAL, callback=None)
        link_text = LinkText(link, "Click here", self.font, self.draw)
        link_text.set_origin(np.array([100, 200]))

        result = self.page._make_query_result(link_text, (105, 205))

        self.assertEqual(result.object_type, "link")
        self.assertEqual(result.text, "Click here")
        self.assertTrue(result.is_interactive)
        self.assertEqual(result.link_target, "chapter2")


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