"""
Unit tests for pyWebLayout.concrete.functional module.
Tests the LinkText, ButtonText, and FormFieldText classes.
"""

import unittest
import numpy as np
from unittest.mock import Mock, patch

from pyWebLayout.concrete.functional import (
    LinkText, ButtonText, FormFieldText,
    create_link_text, create_button_text, create_form_field_text
)
from pyWebLayout.abstract.functional import (
    Link, Button, FormField, LinkType, FormFieldType
)
from pyWebLayout.style import Font, TextDecoration


class TestLinkText(unittest.TestCase):
    """Test cases for the LinkText class"""

    def setUp(self):
        """Set up test fixtures"""
        self.font = Font(
            font_path=None,  # Use default font
            font_size=12,
            colour=(0, 0, 0)
        )
        self.callback = Mock()

        # Create different types of links
        self.internal_link = Link("chapter1", LinkType.INTERNAL, self.callback)
        self.external_link = Link(
            "https://example.com",
            LinkType.EXTERNAL,
            self.callback)
        self.api_link = Link("/api/settings", LinkType.API, self.callback)
        self.function_link = Link("toggle_theme", LinkType.FUNCTION, self.callback)

        # Create a mock ImageDraw.Draw object
        self.mock_draw = Mock()

    def test_link_text_initialization_internal(self):
        """Test initialization of internal link text"""
        link_text = "Go to Chapter 1"
        renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)

        self.assertEqual(renderable._link, self.internal_link)
        self.assertEqual(renderable.text, link_text)
        self.assertFalse(renderable._hovered)
        self.assertEqual(renderable._callback, self.internal_link.execute)

        # Check that the font has underline decoration and blue color
        self.assertEqual(renderable.style.decoration, TextDecoration.UNDERLINE)
        self.assertEqual(renderable.style.colour, (0, 0, 200))

    def test_link_text_initialization_external(self):
        """Test initialization of external link text"""
        link_text = "Visit Example"
        renderable = LinkText(self.external_link, link_text, self.font, self.mock_draw)

        self.assertEqual(renderable._link, self.external_link)
        # External links should have darker blue color
        self.assertEqual(renderable.style.colour, (0, 0, 180))

    def test_link_text_initialization_api(self):
        """Test initialization of API link text"""
        link_text = "Settings"
        renderable = LinkText(self.api_link, link_text, self.font, self.mock_draw)

        self.assertEqual(renderable._link, self.api_link)
        # API links should have red color
        self.assertEqual(renderable.style.colour, (150, 0, 0))

    def test_link_text_initialization_function(self):
        """Test initialization of function link text"""
        link_text = "Toggle Theme"
        renderable = LinkText(self.function_link, link_text, self.font, self.mock_draw)

        self.assertEqual(renderable._link, self.function_link)
        # Function links should have green color
        self.assertEqual(renderable.style.colour, (0, 120, 0))

    def test_link_property(self):
        """Test link property accessor"""
        link_text = "Test Link"
        renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)

        self.assertEqual(renderable.link, self.internal_link)

    def test_set_hovered(self):
        """Test setting hover state"""
        link_text = "Hover Test"
        renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)

        self.assertFalse(renderable._hovered)

        renderable.set_hovered(True)
        self.assertTrue(renderable._hovered)

        renderable.set_hovered(False)
        self.assertFalse(renderable._hovered)

    def test_render_normal_state(self):
        """Test rendering in normal state"""
        link_text = "Test Link"
        renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Parent render should be called
            mock_parent_render.assert_called_once()
            # Should not draw highlight when not hovered
            self.mock_draw.rectangle.assert_not_called()

    def test_in_object(self):
        """Test in_object method"""
        link_text = "Test Link"
        renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
        renderable.set_origin(np.array([10, 20]))

        # Mock width property
        renderable._width = 80

        # Point inside link - origin is at baseline (10, 20), so test at baseline Y
        self.assertTrue(renderable.in_object((15, 20)))

        # Point outside link
        self.assertFalse(renderable.in_object((200, 200)))

    def test_factory_function(self):
        """Test the create_link_text factory function"""
        link_text = "Factory Test"
        renderable = create_link_text(
            self.internal_link,
            link_text,
            self.font,
            self.mock_draw)

        self.assertIsInstance(renderable, LinkText)
        self.assertEqual(renderable.text, link_text)
        self.assertEqual(renderable.link, self.internal_link)


class TestButtonText(unittest.TestCase):
    """Test cases for the ButtonText class"""

    def setUp(self):
        """Set up test fixtures"""
        self.font = Font(
            font_path=None,  # Use default font
            font_size=12,
            colour=(255, 255, 255)
        )
        self.callback = Mock()
        self.button = Button("Click Me", self.callback)
        self.mock_draw = Mock()

    def test_button_text_initialization(self):
        """Test basic button text initialization"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        self.assertEqual(renderable._button, self.button)
        self.assertEqual(renderable.text, "Click Me")
        self.assertFalse(renderable._pressed)
        self.assertFalse(renderable._hovered)
        self.assertEqual(renderable._callback, self.button.execute)
        self.assertEqual(renderable._padding, (4, 8, 4, 8))

    def test_button_text_with_custom_padding(self):
        """Test button text initialization with custom padding"""
        custom_padding = (8, 12, 8, 12)

        renderable = ButtonText(
            self.button, self.font, self.mock_draw,
            padding=custom_padding
        )

        self.assertEqual(renderable._padding, custom_padding)

    def test_button_property(self):
        """Test button property accessor"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        self.assertEqual(renderable.button, self.button)

    def test_set_pressed(self):
        """Test setting pressed state"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        self.assertFalse(renderable._pressed)

        renderable.set_pressed(True)
        self.assertTrue(renderable._pressed)

        renderable.set_pressed(False)
        self.assertFalse(renderable._pressed)

    def test_set_hovered(self):
        """Test setting hover state"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        self.assertFalse(renderable._hovered)

        renderable.set_hovered(True)
        self.assertTrue(renderable._hovered)

        renderable.set_hovered(False)
        self.assertFalse(renderable._hovered)

    def test_size_property(self):
        """Test size property includes padding"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        # The size should be padded size, not just text size
        # Since we handle mocks in __init__, use the padded values directly
        expected_width = renderable._padded_width
        expected_height = renderable._padded_height

        np.testing.assert_array_equal(
            renderable.size, np.array([expected_width, expected_height]))

    def test_render_normal_state(self):
        """Test rendering in normal state"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should draw rounded rectangle for button background
            self.mock_draw.rounded_rectangle.assert_called_once()
            # Parent render should be called for text
            mock_parent_render.assert_called_once()

    def test_render_disabled_state(self):
        """Test rendering disabled button"""
        disabled_button = Button("Disabled", self.callback, enabled=False)
        renderable = ButtonText(disabled_button, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should still draw button background
            self.mock_draw.rounded_rectangle.assert_called_once()
            mock_parent_render.assert_called_once()

    def test_in_object_with_padding(self):
        """Test in_object method considers padding"""
        renderable = ButtonText(self.button, self.font, self.mock_draw)
        renderable.set_origin(np.array([10, 20]))

        # Point inside button (including padding)
        self.assertTrue(renderable.in_object((15, 25)))

        # Point outside button
        self.assertFalse(renderable.in_object((200, 200)))

    def test_factory_function(self):
        """Test the create_button_text factory function"""
        custom_padding = (6, 10, 6, 10)
        renderable = create_button_text(
            self.button, self.font, self.mock_draw, custom_padding)

        self.assertIsInstance(renderable, ButtonText)
        self.assertEqual(renderable.text, "Click Me")
        self.assertEqual(renderable.button, self.button)
        self.assertEqual(renderable._padding, custom_padding)


class TestFormFieldText(unittest.TestCase):
    """Test cases for the FormFieldText class"""

    def setUp(self):
        """Set up test fixtures"""
        self.font = Font(
            font_path=None,  # Use default font
            font_size=12,
            colour=(0, 0, 0)
        )

        # Create different types of form fields
        self.text_field = FormField("username", FormFieldType.TEXT, "Username")
        self.password_field = FormField("password", FormFieldType.PASSWORD, "Password")
        self.textarea_field = FormField(
            "description", FormFieldType.TEXTAREA, "Description")
        self.select_field = FormField("country", FormFieldType.SELECT, "Country")

        self.mock_draw = Mock()

    def test_form_field_text_initialization(self):
        """Test initialization of form field text"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        self.assertEqual(renderable._field, self.text_field)
        self.assertEqual(renderable.text, "Username")
        self.assertFalse(renderable._focused)
        self.assertEqual(renderable._field_height, 24)

    def test_form_field_text_with_custom_height(self):
        """Test form field text with custom field height"""
        custom_height = 40
        renderable = FormFieldText(
            self.text_field,
            self.font,
            self.mock_draw,
            custom_height)

        self.assertEqual(renderable._field_height, custom_height)

    def test_field_property(self):
        """Test field property accessor"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        self.assertEqual(renderable.field, self.text_field)

    def test_set_focused(self):
        """Test setting focus state"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        self.assertFalse(renderable._focused)

        renderable.set_focused(True)
        self.assertTrue(renderable._focused)

        renderable.set_focused(False)
        self.assertFalse(renderable._focused)

    def test_size_includes_field_area(self):
        """Test size property includes field area"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        # Size should include label height + gap + field height
        expected_height = renderable._style.font_size + 5 + renderable._field_height
        expected_width = renderable._field_width  # Use the calculated field width

        np.testing.assert_array_equal(
            renderable.size, np.array([expected_width, expected_height]))

    def test_render_text_field(self):
        """Test rendering text field"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should render label
            mock_parent_render.assert_called_once()
            # Should draw field background rectangle
            self.mock_draw.rectangle.assert_called_once()

    def test_render_field_with_value(self):
        """Test rendering field with value"""
        self.text_field.value = "john_doe"
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should render label
            mock_parent_render.assert_called_once()
            # Should draw field background and value text
            self.mock_draw.rectangle.assert_called_once()
            self.mock_draw.text.assert_called_once()

    def test_render_password_field(self):
        """Test rendering password field with masked value"""
        self.password_field.value = "secret123"
        renderable = FormFieldText(self.password_field, self.font, self.mock_draw)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should render label and field
            mock_parent_render.assert_called_once()
            self.mock_draw.rectangle.assert_called_once()
            # Should render masked text
            self.mock_draw.text.assert_called_once()
            # Check that the text call used masked characters
            call_args = self.mock_draw.text.call_args[0]
            self.assertEqual(call_args[1], "•" * len("secret123"))

    def test_render_focused_field(self):
        """Test rendering focused field"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
        renderable.set_focused(True)

        # Mock the parent Text render method
        with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
            renderable.render()

            # Should render with focus styling
            mock_parent_render.assert_called_once()
            self.mock_draw.rectangle.assert_called_once()

    def test_handle_click_inside_field(self):
        """Test clicking inside field area"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        # Click inside field area (below label)
        field_area_y = renderable._style.font_size + 5 + 10  # Within field area
        field_area_point = (15, field_area_y)
        result = renderable.handle_click(field_area_point)

        # Should return True and set focused
        self.assertTrue(result)
        self.assertTrue(renderable._focused)

    def test_handle_click_outside_field(self):
        """Test clicking outside field area"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)

        # Click outside field area
        outside_point = (200, 200)
        result = renderable.handle_click(outside_point)

        # Should return False and not set focused
        self.assertFalse(result)
        self.assertFalse(renderable._focused)

    def test_in_object(self):
        """Test in_object method"""
        renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
        renderable.set_origin(np.array([10, 20]))

        # Point inside field (including label and input area)
        self.assertTrue(renderable.in_object((15, 25)))

        # Point outside field
        self.assertFalse(renderable.in_object((200, 200)))

    def test_factory_function(self):
        """Test the create_form_field_text factory function"""
        custom_height = 30
        renderable = create_form_field_text(
            self.text_field, self.font, self.mock_draw, custom_height)

        self.assertIsInstance(renderable, FormFieldText)
        self.assertEqual(renderable.text, "Username")
        self.assertEqual(renderable.field, self.text_field)
        self.assertEqual(renderable._field_height, custom_height)


class TestInteractionCallbacks(unittest.TestCase):
    """Test cases for interaction functionality"""

    def setUp(self):
        """Set up test fixtures"""
        self.font = Font(font_size=12, colour=(0, 0, 0))
        self.mock_draw = Mock()
        self.callback_result = "callback_executed"

        # Link callback: receives (location, point, **params)
        def link_callback(location, point, **params):
            return "callback_executed"
        self.link_callback = link_callback

        # Button callback: receives (point, **params)
        def button_callback(point, **params):
            return "callback_executed"
        self.button_callback = button_callback

    def test_link_text_interaction(self):
        """Test that LinkText properly handles interaction"""
        # Use a FUNCTION link type which calls the callback, not INTERNAL which
        # returns location
        link = Link("test_function", LinkType.FUNCTION, self.link_callback)
        renderable = LinkText(link, "Test Link", self.font, self.mock_draw)

        # Simulate interaction
        result = renderable.interact(np.array([10, 10]))

        # Should execute the link's callback
        self.assertEqual(result, self.callback_result)

    def test_button_text_interaction(self):
        """Test that ButtonText properly handles interaction"""
        button = Button("Test Button", self.button_callback)
        renderable = ButtonText(button, self.font, self.mock_draw)

        # Simulate interaction
        result = renderable.interact(np.array([10, 10]))

        # Should execute the button's callback
        self.assertEqual(result, self.callback_result)


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