"""
Unit tests for abstract block elements.

Tests the core abstract block classes that form the foundation of the document model.
"""

import unittest
import os
import tempfile
import shutil
import threading
import time
from PIL import Image as PILImage
from pyWebLayout.abstract.block import (
    BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock, HList,
    ListStyle, ListItem, Table, TableRow, TableCell, HorizontalRule,
    Image
)
from pyWebLayout.abstract.inline import Word, LineBreak
from pyWebLayout.style import Font

# Flask server for testing URL functionality
try:
    from flask import Flask, send_file
    FLASK_AVAILABLE = True
except ImportError:
    FLASK_AVAILABLE = False


class TestBlockElements(unittest.TestCase):
    """Test cases for basic block elements."""

    def test_paragraph_creation(self):
        """Test creating and using paragraphs."""
        paragraph = Paragraph()

        self.assertEqual(paragraph.block_type, BlockType.PARAGRAPH)
        self.assertEqual(paragraph.word_count, 0)
        self.assertIsNone(paragraph.parent)

        # Add words
        font = Font()
        word1 = Word("Hello", font)
        word2 = Word("World", font)

        paragraph.add_word(word1)
        paragraph.add_word(word2)

        self.assertEqual(paragraph.word_count, 2)

        # Test word iteration
        words = list(paragraph.words_iter())
        self.assertEqual(len(words), 2)
        self.assertEqual(words[0][1].text, "Hello")
        self.assertEqual(words[1][1].text, "World")

    def test_heading_levels(self):
        """Test heading creation with different levels."""
        h1 = Heading(HeadingLevel.H1)
        h3 = Heading(HeadingLevel.H3)
        h6 = Heading(HeadingLevel.H6)

        self.assertEqual(h1.level, HeadingLevel.H1)
        self.assertEqual(h3.level, HeadingLevel.H3)
        self.assertEqual(h6.level, HeadingLevel.H6)

        self.assertEqual(h1.block_type, BlockType.HEADING)

        # Test level modification
        h1.level = HeadingLevel.H2
        self.assertEqual(h1.level, HeadingLevel.H2)

    def test_quote_nesting(self):
        """Test blockquote with nested content."""
        quote = Quote()

        # Add nested paragraphs
        p1 = Paragraph()
        p2 = Paragraph()

        quote.add_block(p1)
        quote.add_block(p2)

        self.assertEqual(p1.parent, quote)
        self.assertEqual(p2.parent, quote)

        # Test block iteration
        blocks = list(quote.blocks())
        self.assertEqual(len(blocks), 2)
        self.assertEqual(blocks[0], p1)
        self.assertEqual(blocks[1], p2)

    def test_code_block(self):
        """Test code block functionality."""
        code = CodeBlock("python")

        self.assertEqual(code.language, "python")
        self.assertEqual(code.line_count, 0)

        # Add code lines
        code.add_line("def hello():")
        code.add_line("    print('Hello!')")

        self.assertEqual(code.line_count, 2)

        # Test line iteration
        lines = list(code.lines())
        self.assertEqual(len(lines), 2)
        self.assertEqual(lines[0][1], "def hello():")
        self.assertEqual(lines[1][1], "    print('Hello!')")

        # Test language modification
        code.language = "javascript"
        self.assertEqual(code.language, "javascript")

    def test_list_creation(self):
        """Test list creation and item management."""
        # Unordered list
        ul = HList(ListStyle.UNORDERED)
        self.assertEqual(ul.style, ListStyle.UNORDERED)
        self.assertEqual(ul.item_count, 0)

        # Add list items
        item1 = ListItem()
        item2 = ListItem()

        ul.add_item(item1)
        ul.add_item(item2)

        self.assertEqual(ul.item_count, 2)
        self.assertEqual(item1.parent, ul)
        self.assertEqual(item2.parent, ul)

        # Test item iteration
        items = list(ul.items())
        self.assertEqual(len(items), 2)

        # Test list style change
        ul.style = ListStyle.ORDERED
        self.assertEqual(ul.style, ListStyle.ORDERED)

    def test_definition_list(self):
        """Test definition list with terms."""
        dl = HList(ListStyle.DEFINITION)

        # Add definition items with terms
        dt1 = ListItem(term="Python")
        dt2 = ListItem(term="JavaScript")

        dl.add_item(dt1)
        dl.add_item(dt2)

        self.assertEqual(dt1.term, "Python")
        self.assertEqual(dt2.term, "JavaScript")

        # Test term modification
        dt1.term = "Python 3"
        self.assertEqual(dt1.term, "Python 3")

    def test_table_structure(self):
        """Test table, row, and cell structure."""
        table = Table(caption="Test Table")

        self.assertEqual(table.caption, "Test Table")
        self.assertEqual(table.row_count["total"], 0)

        # Create rows and cells
        header_row = TableRow()
        data_row = TableRow()

        # Header cells
        h1 = TableCell(is_header=True)
        h2 = TableCell(is_header=True)
        header_row.add_cell(h1)
        header_row.add_cell(h2)

        # Data cells
        d1 = TableCell(is_header=False)
        d2 = TableCell(is_header=False, colspan=2)
        data_row.add_cell(d1)
        data_row.add_cell(d2)

        # Add rows to table
        table.add_row(header_row, "header")
        table.add_row(data_row, "body")

        # Test structure
        self.assertEqual(table.row_count["header"], 1)
        self.assertEqual(table.row_count["body"], 1)
        self.assertEqual(table.row_count["total"], 2)

        # Test cell properties
        self.assertTrue(h1.is_header)
        self.assertFalse(d1.is_header)
        self.assertEqual(d2.colspan, 2)
        self.assertEqual(d2.rowspan, 1)  # Default

        # Test row cell count
        self.assertEqual(header_row.cell_count, 2)
        self.assertEqual(data_row.cell_count, 2)

    def test_table_sections(self):
        """Test table header, body, and footer sections."""
        table = Table()

        # Add rows to different sections
        header = TableRow()
        body1 = TableRow()
        body2 = TableRow()
        footer = TableRow()

        table.add_row(header, "header")
        table.add_row(body1, "body")
        table.add_row(body2, "body")
        table.add_row(footer, "footer")

        # Test section iteration
        header_rows = list(table.header_rows())
        body_rows = list(table.body_rows())
        footer_rows = list(table.footer_rows())

        self.assertEqual(len(header_rows), 1)
        self.assertEqual(len(body_rows), 2)
        self.assertEqual(len(footer_rows), 1)

        # Test all_rows iteration
        all_rows = list(table.all_rows())
        self.assertEqual(len(all_rows), 4)

        # Check section labels
        sections = [section for section, row in all_rows]
        self.assertEqual(sections, ["header", "body", "body", "footer"])

    def test_image_loading(self):
        """Test image element properties."""
        # Test with basic properties
        img = Image("test.jpg", "Test image", 100, 200)

        self.assertEqual(img.source, "test.jpg")
        self.assertEqual(img.alt_text, "Test image")
        self.assertEqual(img.width, 100)
        self.assertEqual(img.height, 200)

        # Test property modification
        img.source = "new.png"
        img.alt_text = "New image"
        img.width = 150
        img.height = 300

        self.assertEqual(img.source, "new.png")
        self.assertEqual(img.alt_text, "New image")
        self.assertEqual(img.width, 150)
        self.assertEqual(img.height, 300)

        # Test dimensions tuple
        self.assertEqual(img.get_dimensions(), (150, 300))

    def test_aspect_ratio_calculation(self):
        """Test image aspect ratio calculations."""
        # Test with specified dimensions
        img = Image("test.jpg", width=400, height=200)
        self.assertEqual(img.get_aspect_ratio(), 2.0)  # 400/200

        # Test with only one dimension
        img2 = Image("test.jpg", width=300)
        self.assertIsNone(img2.get_aspect_ratio())  # No height specified

        # Test scaled dimensions
        scaled = img.calculate_scaled_dimensions(max_width=200, max_height=150)
        # Should scale down proportionally
        self.assertEqual(scaled[0], 200)  # Width limited by max_width
        self.assertEqual(scaled[1], 100)  # Height scaled proportionally

    def test_simple_elements(self):
        """Test simple block elements."""
        hr = HorizontalRule()
        br = LineBreak()

        self.assertEqual(hr.block_type, BlockType.HORIZONTAL_RULE)
        self.assertEqual(br.block_type, BlockType.LINE_BREAK)

        # These elements have no additional properties
        self.assertIsNone(hr.parent)
        self.assertIsNone(br.parent)


class TestImagePIL(unittest.TestCase):
    """Test cases for Image class with PIL functionality."""

    @classmethod
    def setUpClass(cls):
        """Set up temporary directory and test images."""
        cls.temp_dir = tempfile.mkdtemp()
        cls.sample_image_path = "tests/data/sample_image.jpg"

        # Create test images in different formats
        cls._create_test_images()

        # Start Flask server for URL testing if Flask is available
        if FLASK_AVAILABLE:
            cls._start_flask_server()

    @classmethod
    def tearDownClass(cls):
        """Clean up temporary directory and stop Flask server."""
        shutil.rmtree(cls.temp_dir, ignore_errors=True)

        if FLASK_AVAILABLE and hasattr(cls, 'flask_thread'):
            cls.flask_server_running = False
            cls.flask_thread.join(timeout=2)

    @classmethod
    def _create_test_images(cls):
        """Create test images in different formats."""
        # Load the sample image
        if os.path.exists(cls.sample_image_path):
            with PILImage.open(cls.sample_image_path) as img:
                cls.original_size = img.size

                # Save in different formats
                cls.jpg_path = os.path.join(cls.temp_dir, "test.jpg")
                cls.png_path = os.path.join(cls.temp_dir, "test.png")
                cls.bmp_path = os.path.join(cls.temp_dir, "test.bmp")
                cls.gif_path = os.path.join(cls.temp_dir, "test.gif")

                img.save(cls.jpg_path, "JPEG")
                img.save(cls.png_path, "PNG")
                img.save(cls.bmp_path, "BMP")

                # Convert to RGB for GIF (GIF doesn't support transparency from RGBA)
                rgb_img = img.convert("RGB")
                rgb_img.save(cls.gif_path, "GIF")
        else:
            # Create a simple test image if sample doesn't exist
            cls.original_size = (100, 100)
            test_img = PILImage.new("RGB", cls.original_size, (255, 0, 0))

            cls.jpg_path = os.path.join(cls.temp_dir, "test.jpg")
            cls.png_path = os.path.join(cls.temp_dir, "test.png")
            cls.bmp_path = os.path.join(cls.temp_dir, "test.bmp")
            cls.gif_path = os.path.join(cls.temp_dir, "test.gif")

            test_img.save(cls.jpg_path, "JPEG")
            test_img.save(cls.png_path, "PNG")
            test_img.save(cls.bmp_path, "BMP")
            test_img.save(cls.gif_path, "GIF")

    @classmethod
    def _start_flask_server(cls):
        """Start a Flask server for URL testing."""
        import urllib.request
        import urllib.error

        cls.flask_app = Flask(__name__)
        cls.flask_port = 5555  # Use a specific port for testing
        cls.flask_server_running = True

        @cls.flask_app.route('/test.jpg')
        def serve_test_image():
            return send_file(cls.jpg_path, mimetype='image/jpeg')

        @cls.flask_app.route('/health')
        def health_check():
            return 'OK', 200

        def run_flask():
            cls.flask_app.run(host='127.0.0.1', port=cls.flask_port, debug=False,
                              use_reloader=False, threaded=True)

        cls.flask_thread = threading.Thread(target=run_flask, daemon=True)
        cls.flask_thread.start()

        # Wait for server to be ready with health check
        max_wait = 5  # Maximum 5 seconds
        wait_interval = 0.1  # Check every 100ms
        elapsed = 0

        while elapsed < max_wait:
            try:
                with urllib.request.urlopen(f'http://127.0.0.1:{cls.flask_port}/health', timeout=1) as response:
                    if response.status == 200:
                        break
            except (urllib.error.URLError, ConnectionRefusedError, OSError):
                pass
            time.sleep(wait_interval)
            elapsed += wait_interval

    def test_image_url_detection(self):
        """Test URL detection functionality."""
        img = Image()

        # Test URL detection
        self.assertTrue(img._is_url("http://example.com/image.jpg"))
        self.assertTrue(img._is_url("https://example.com/image.png"))
        self.assertTrue(img._is_url("ftp://example.com/image.gif"))

        # Test non-URL detection
        self.assertFalse(img._is_url("image.jpg"))
        self.assertFalse(img._is_url("/path/to/image.png"))
        self.assertFalse(img._is_url("../relative/path.gif"))
        self.assertFalse(img._is_url(""))

    def test_load_local_image_jpg(self):
        """Test loading local JPG image."""
        img = Image(self.jpg_path)

        file_path, pil_img = img.load_image_data()

        self.assertIsNotNone(pil_img)
        self.assertEqual(file_path, self.jpg_path)
        self.assertEqual(pil_img.size, self.original_size)
        self.assertEqual(img.width, self.original_size[0])
        self.assertEqual(img.height, self.original_size[1])

    def test_load_local_image_png(self):
        """Test loading local PNG image."""
        img = Image(self.png_path)

        file_path, pil_img = img.load_image_data()

        self.assertIsNotNone(pil_img)
        self.assertEqual(file_path, self.png_path)
        self.assertEqual(pil_img.size, self.original_size)

    def test_load_local_image_bmp(self):
        """Test loading local BMP image."""
        img = Image(self.bmp_path)

        file_path, pil_img = img.load_image_data()

        self.assertIsNotNone(pil_img)
        self.assertEqual(file_path, self.bmp_path)
        self.assertEqual(pil_img.size, self.original_size)

    def test_load_local_image_gif(self):
        """Test loading local GIF image."""
        img = Image(self.gif_path)

        file_path, pil_img = img.load_image_data()

        self.assertIsNotNone(pil_img)
        self.assertEqual(file_path, self.gif_path)
        self.assertEqual(pil_img.size, self.original_size)

    def test_load_nonexistent_image(self):
        """Test loading non-existent image."""
        img = Image("nonexistent.jpg")

        file_path, pil_img = img.load_image_data()

        self.assertIsNone(pil_img)
        self.assertIsNone(file_path)

    def test_load_empty_source(self):
        """Test loading with empty source."""
        img = Image("")

        file_path, pil_img = img.load_image_data()

        self.assertIsNone(pil_img)
        self.assertIsNone(file_path)

    def test_auto_update_dimensions(self):
        """Test automatic dimension updating."""
        img = Image(self.jpg_path, width=50, height=50)  # Wrong initial dimensions

        # Test with auto-update enabled (default)
        file_path, pil_img = img.load_image_data(auto_update_dimensions=True)

        self.assertEqual(img.width, self.original_size[0])
        self.assertEqual(img.height, self.original_size[1])

    def test_no_auto_update_dimensions(self):
        """Test loading without automatic dimension updating."""
        original_width, original_height = 50, 50
        img = Image(self.jpg_path, width=original_width, height=original_height)

        # Test with auto-update disabled
        file_path, pil_img = img.load_image_data(auto_update_dimensions=False)

        self.assertEqual(img.width, original_width)  # Should remain unchanged
        self.assertEqual(img.height, original_height)  # Should remain unchanged

    def test_get_image_info(self):
        """Test getting detailed image information."""
        img = Image(self.jpg_path)

        info = img.get_image_info()

        self.assertIsInstance(info, dict)
        self.assertIn('format', info)
        self.assertIn('mode', info)
        self.assertIn('size', info)
        self.assertIn('width', info)
        self.assertIn('height', info)

        self.assertEqual(info['size'], self.original_size)
        self.assertEqual(info['width'], self.original_size[0])
        self.assertEqual(info['height'], self.original_size[1])

    def test_get_image_info_different_formats(self):
        """Test getting image info for different formats."""
        formats_and_paths = [
            ('JPEG', self.jpg_path),
            ('PNG', self.png_path),
            ('BMP', self.bmp_path),
            ('GIF', self.gif_path),
        ]

        for expected_format, path in formats_and_paths:
            with self.subTest(format=expected_format):
                img = Image(path)
                info = img.get_image_info()

                self.assertEqual(info['format'], expected_format)
                self.assertEqual(info['size'], self.original_size)

    def test_get_image_info_nonexistent(self):
        """Test getting image info for non-existent image."""
        img = Image("nonexistent.jpg")

        info = img.get_image_info()

        self.assertEqual(info, {})

    @unittest.skipUnless(FLASK_AVAILABLE, "Flask not available for URL testing")
    def test_load_image_from_url(self):
        """Test loading image from URL."""
        url = f"http://127.0.0.1:{self.flask_port}/test.jpg"
        img = Image(url)

        file_path, pil_img = img.load_image_data()

        self.assertIsNotNone(pil_img)
        self.assertIsNotNone(file_path)
        self.assertTrue(file_path.endswith('.tmp'))  # Should be a temp file
        self.assertEqual(pil_img.size, self.original_size)

        # Check that dimensions were updated
        self.assertEqual(img.width, self.original_size[0])
        self.assertEqual(img.height, self.original_size[1])

    @unittest.skipUnless(FLASK_AVAILABLE, "Flask not available for URL testing")
    def test_get_image_info_from_url(self):
        """Test getting image info from URL."""
        url = f"http://127.0.0.1:{self.flask_port}/test.jpg"
        img = Image(url)

        info = img.get_image_info()

        self.assertIsInstance(info, dict)
        self.assertEqual(info['format'], 'JPEG')
        self.assertEqual(info['size'], self.original_size)

    def test_load_invalid_url(self):
        """Test loading from invalid URL."""
        img = Image("http://nonexistent.domain/image.jpg")

        file_path, pil_img = img.load_image_data()

        self.assertIsNone(pil_img)
        self.assertIsNone(file_path)

    def test_multiple_loads_cleanup(self):
        """Test that multiple loads don't leave temp files."""
        img = Image(self.jpg_path)

        # Load multiple times
        for _ in range(3):
            file_path, pil_img = img.load_image_data()
            self.assertIsNotNone(pil_img)

    def test_original_sample_image(self):
        """Test loading the original sample image if it exists."""
        if os.path.exists(self.sample_image_path):
            img = Image(self.sample_image_path)

            file_path, pil_img = img.load_image_data()

            self.assertIsNotNone(pil_img)
            self.assertEqual(file_path, self.sample_image_path)

            # Test that we can get image info
            info = img.get_image_info()
            self.assertIsInstance(info, dict)
            self.assertIn('format', info)
            self.assertIn('size', info)


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