"""Unit tests for record builder."""

from datetime import date, datetime
from uuid import uuid4

import pytest

from src.extraction.record_builder import RecordBuilder
from src.extraction.models import (
    TableBoundary,
    HeaderInfo,
    MergeMetadata,
    ExtractedRecord,
)
from src.core.exceptions import ExtractionError


@pytest.fixture
def builder():
    """Create a RecordBuilder instance."""
    return RecordBuilder()


@pytest.fixture
def simple_boundary():
    """Create a simple table boundary."""
    return TableBoundary(
        sheet="Sheet1",
        table_id=1,
        start_row=1,
        end_row=5,
        start_col="A",
        end_col="C",
    )


@pytest.fixture
def simple_headers():
    """Create simple headers (Category, Product, Value)."""
    return {
        "A": HeaderInfo(display="Category", levels=["Category"], depth=1, column_letter="A"),
        "B": HeaderInfo(display="Product", levels=["Product"], depth=1, column_letter="B"),
        "C": HeaderInfo(display="Value", levels=["Value"], depth=1, column_letter="C"),
    }


@pytest.fixture
def multilevel_headers():
    """Create multi-level headers (Sales > Q1, Sales > Q2)."""
    return {
        "A": HeaderInfo(display="Product", levels=["Product"], depth=1, column_letter="A"),
        "B": HeaderInfo(display="Sales > Q1", levels=["Sales", "Q1"], depth=2, column_letter="B"),
        "C": HeaderInfo(display="Sales > Q2", levels=["Sales", "Q2"], depth=2, column_letter="C"),
    }


class TestExtractedRecordModel:
    """Tests for ExtractedRecord dataclass validation."""

    def test_valid_extracted_record(self):
        """Should create valid ExtractedRecord."""
        content = {"Category": "Group A", "Product": "Widget", "Value": 100}
        headers = ["Category", "Product", "Value"]
        source = {
            "document_id": str(uuid4()),
            "filename": "test.xlsx",
            "sheet": "Sheet1",
            "table_id": 1,
            "row": 2,
            "col_range": "A:C",
        }

        record = ExtractedRecord(content=content, headers=headers, _source=source)

        assert record.content == content
        assert record.headers == headers
        assert record._source == source
        assert record._merge_metadata is None

    def test_to_dict(self):
        """Should convert ExtractedRecord to dictionary."""
        content = {"Category": "Test"}
        headers = ["Category"]
        source = {
            "document_id": str(uuid4()),
            "filename": "test.xlsx",
            "sheet": "Sheet1",
            "table_id": 1,
            "row": 2,
            "col_range": "A:A",
        }
        merge_metadata = {"Category": "A2:A4"}

        record = ExtractedRecord(
            content=content,
            headers=headers,
            _source=source,
            _merge_metadata=merge_metadata,
        )

        result = record.to_dict()

        assert result["content"] == content
        assert result["headers"] == headers
        assert result["_source"] == source
        assert result["_merge_metadata"] == merge_metadata

    def test_empty_content_raises_error(self):
        """Should raise error for empty content."""
        with pytest.raises(ValueError, match="Record content cannot be empty"):
            ExtractedRecord(
                content={},
                headers=["test"],
                _source={
                    "document_id": str(uuid4()),
                    "filename": "test.xlsx",
                    "sheet": "Sheet1",
                    "table_id": 1,
                    "row": 2,
                    "col_range": "A:A",
                },
            )

    def test_empty_headers_raises_error(self):
        """Should raise error for empty headers."""
        with pytest.raises(ValueError, match="Record headers cannot be empty"):
            ExtractedRecord(
                content={"test": "value"},
                headers=[],
                _source={
                    "document_id": str(uuid4()),
                    "filename": "test.xlsx",
                    "sheet": "Sheet1",
                    "table_id": 1,
                    "row": 2,
                    "col_range": "A:A",
                },
            )

    def test_missing_required_source_field_raises_error(self):
        """Should raise error for missing required source fields."""
        with pytest.raises(ValueError, match="Missing required source field: filename"):
            ExtractedRecord(
                content={"test": "value"},
                headers=["test"],
                _source={
                    "document_id": str(uuid4()),
                    # Missing filename
                    "sheet": "Sheet1",
                    "table_id": 1,
                    "row": 2,
                    "col_range": "A:A",
                },
            )

    def test_invalid_uuid_raises_error(self):
        """Should raise error for invalid document_id UUID."""
        with pytest.raises(ValueError, match="Invalid document_id UUID"):
            ExtractedRecord(
                content={"test": "value"},
                headers=["test"],
                _source={
                    "document_id": "not-a-uuid",
                    "filename": "test.xlsx",
                    "sheet": "Sheet1",
                    "table_id": 1,
                    "row": 2,
                    "col_range": "A:A",
                },
            )


class TestBasicRecordBuilding:
    """Tests for basic record building functionality."""

    def test_build_simple_records(self, builder, simple_boundary, simple_headers):
        """Should build records from simple table with headers + data."""
        # Grid: 1 header row + 2 data rows
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header row (depth=1)
            ["Group A", "Widget", "100"],  # Data row 1
            ["Group B", "Gadget", "200"],  # Data row 2
        ]
        merge_metadata = []
        document_id = uuid4()
        filename = "test.xlsx"

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=document_id,
            filename=filename,
        )

        assert len(records) == 2

        # Check first record
        record1 = records[0]
        assert record1.content == {
            "Category": "Group A",
            "Product": "Widget",
            "Value": "100",
        }
        assert record1.headers == ["Category", "Product", "Value"]
        assert record1._source["document_id"] == str(document_id)
        assert record1._source["filename"] == filename
        assert record1._source["sheet"] == "Sheet1"
        assert record1._source["table_id"] == 1
        assert record1._source["row"] == 2  # First data row (after header)
        assert record1._source["col_range"] == "A:C"
        assert record1._merge_metadata is None

        # Check second record
        record2 = records[1]
        assert record2.content == {
            "Category": "Group B",
            "Product": "Gadget",
            "Value": "200",
        }
        assert record2._source["row"] == 3  # Second data row

    def test_build_records_with_multilevel_headers(
        self, builder, simple_boundary, multilevel_headers
    ):
        """Should map multi-level headers correctly."""
        # Grid: 2 header rows + 1 data row
        resolved_grid = [
            ["Product", "Sales", "Sales"],  # Header row 1
            ["", "Q1", "Q2"],  # Header row 2
            ["Widget", "100", "150"],  # Data row
        ]
        merge_metadata = []
        document_id = uuid4()
        filename = "test.xlsx"

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=multilevel_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=document_id,
            filename=filename,
        )

        assert len(records) == 1
        record = records[0]
        assert record.content == {
            "Product": "Widget",
            "Sales > Q1": "100",
            "Sales > Q2": "150",
        }
        assert record.headers == ["Product", "Sales > Q1", "Sales > Q2"]
        assert record._source["row"] == 3  # After 2 header rows


class TestDataTypeHandling:
    """Tests for data type conversion."""

    def test_handle_null_values(self, builder, simple_boundary, simple_headers):
        """Should convert None and empty strings to null."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            [None, "", "100"],  # Data with None and empty string
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 1
        assert records[0].content["Category"] is None
        assert records[0].content["Product"] is None
        assert records[0].content["Value"] == "100"

    def test_handle_numeric_values(self, builder, simple_boundary, simple_headers):
        """Should preserve numeric types (int, float)."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", 100],  # Integer
            ["Group B", "Gadget", 150.5],  # Float
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 2
        assert records[0].content["Value"] == 100
        assert isinstance(records[0].content["Value"], int)
        assert records[1].content["Value"] == 150.5
        assert isinstance(records[1].content["Value"], float)

    def test_handle_date_values(self, builder, simple_boundary, simple_headers):
        """Should convert date values to ISO 8601 strings."""
        test_date = date(2024, 11, 27)
        test_datetime = datetime(2024, 11, 27, 10, 30, 0)

        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Date", test_date, None],
            ["DateTime", test_datetime, None],
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 2
        assert records[0].content["Product"] == "2024-11-27"
        assert records[1].content["Product"] == "2024-11-27T10:30:00"

    def test_handle_boolean_values(self, builder, simple_boundary, simple_headers):
        """Should preserve boolean values."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Active", True, False],
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 1
        assert records[0].content["Product"] is True
        assert isinstance(records[0].content["Product"], bool)
        assert records[0].content["Value"] is False
        assert isinstance(records[0].content["Value"], bool)

    def test_handle_string_values(self, builder, simple_boundary, simple_headers):
        """Should preserve and trim string values."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["  Leading spaces  ", "Trailing  ", "Normal"],
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 1
        assert records[0].content["Category"] == "Leading spaces"
        assert records[0].content["Product"] == "Trailing"
        assert records[0].content["Value"] == "Normal"


class TestRowFiltering:
    """Tests for row filtering (empty, summary, repeated headers)."""

    def test_skip_empty_rows(self, builder, simple_boundary, simple_headers):
        """Should skip completely empty rows."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Data
            [None, None, None],  # Empty row
            ["", "", ""],  # Empty row
            ["Group B", "Gadget", "200"],  # Data
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        # Should only have 2 data records (empty rows skipped)
        assert len(records) == 2
        assert records[0].content["Category"] == "Group A"
        assert records[1].content["Category"] == "Group B"

    def test_skip_summary_rows(self, builder, simple_boundary, simple_headers):
        """Should skip rows with summary keywords."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Data
            ["Group B", "Gadget", "200"],  # Data
            ["Total", "", "300"],  # Summary row
            ["Grand Total", "", "300"],  # Summary row
            ["Average", "", "150"],  # Summary row
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        # Should only have 2 data records (summary rows skipped)
        assert len(records) == 2
        assert records[0].content["Category"] == "Group A"
        assert records[1].content["Category"] == "Group B"

    def test_skip_repeated_header_rows(self, builder, simple_boundary, simple_headers):
        """Should skip repeated header rows within data."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Data
            ["Category", "Product", "Value"],  # Repeated header
            ["Group B", "Gadget", "200"],  # Data
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        # Should only have 2 data records (repeated header skipped)
        assert len(records) == 2
        assert records[0].content["Category"] == "Group A"
        assert records[1].content["Category"] == "Group B"


class TestMergeMetadataIntegration:
    """Tests for merge metadata integration."""

    def test_include_merge_metadata(self, builder, simple_boundary, simple_headers):
        """Should include merge metadata when cells were propagated."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Row 2
            ["Group A", "Gadget", "200"],  # Row 3 (Category merged from A2:A3)
        ]

        # Category column (A) was merged A2:A3
        merge_metadata = [
            MergeMetadata(
                cell_coordinate="A3",
                merged_from_range="A2:A3",
                original_value="Group A",
            )
        ]

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 2

        # First record - no merge metadata (master cell)
        assert records[0]._merge_metadata is None

        # Second record - has merge metadata (propagated cell)
        assert records[1]._merge_metadata is not None
        assert "Category" in records[1]._merge_metadata
        assert records[1]._merge_metadata["Category"] == "A2:A3"

    def test_multiple_merge_metadata_in_record(
        self, builder, simple_boundary, simple_headers
    ):
        """Should handle multiple merged columns in same record."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Row 2
            ["Group A", "Widget", "200"],  # Row 3 (both merged)
        ]

        merge_metadata = [
            MergeMetadata(
                cell_coordinate="A3",
                merged_from_range="A2:A3",
                original_value="Group A",
            ),
            MergeMetadata(
                cell_coordinate="B3",
                merged_from_range="B2:B3",
                original_value="Widget",
            ),
        ]

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 2

        # Second record has both columns merged
        assert records[1]._merge_metadata is not None
        assert "Category" in records[1]._merge_metadata
        assert "Product" in records[1]._merge_metadata
        assert records[1]._merge_metadata["Category"] == "A2:A3"
        assert records[1]._merge_metadata["Product"] == "B2:B3"


class TestSourceMetadataGeneration:
    """Tests for source metadata generation."""

    def test_source_metadata_fields(self, builder, simple_boundary, simple_headers):
        """Should include all required source metadata fields."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Header
            ["Group A", "Widget", "100"],  # Row 2
        ]
        merge_metadata = []
        document_id = uuid4()
        filename = "test_document.xlsx"

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=document_id,
            filename=filename,
        )

        assert len(records) == 1
        source = records[0]._source

        assert "document_id" in source
        assert "filename" in source
        assert "sheet" in source
        assert "table_id" in source
        assert "row" in source
        assert "col_range" in source

        assert source["document_id"] == str(document_id)
        assert source["filename"] == filename
        assert source["sheet"] == "Sheet1"
        assert source["table_id"] == 1
        assert source["row"] == 2  # Physical row in Excel
        assert source["col_range"] == "A:C"

    def test_physical_row_calculation(self, builder, simple_boundary, simple_headers):
        """Should correctly calculate physical row numbers."""
        resolved_grid = [
            ["Category", "Product", "Value"],  # Row 1 (header)
            ["Data 1", "Value 1", "100"],  # Row 2
            ["Data 2", "Value 2", "200"],  # Row 3
            ["Data 3", "Value 3", "300"],  # Row 4
        ]
        merge_metadata = []

        records = builder.build_records(
            resolved_grid=resolved_grid,
            headers=simple_headers,
            merge_metadata=merge_metadata,
            table_boundary=simple_boundary,
            document_id=uuid4(),
            filename="test.xlsx",
        )

        assert len(records) == 3
        assert records[0]._source["row"] == 2
        assert records[1]._source["row"] == 3
        assert records[2]._source["row"] == 4


class TestErrorHandling:
    """Tests for error handling."""

    def test_extraction_error_on_failure(self, builder, simple_boundary, simple_headers):
        """Should raise ExtractionError on unexpected failures."""
        # Pass invalid grid that will cause error
        resolved_grid = None  # Invalid grid

        with pytest.raises(ExtractionError) as exc_info:
            builder.build_records(
                resolved_grid=resolved_grid,
                headers=simple_headers,
                merge_metadata=[],
                table_boundary=simple_boundary,
                document_id=uuid4(),
                filename="test.xlsx",
            )

        assert exc_info.value.code == "EXTRACTION_ERROR"
        assert "Failed to build records" in exc_info.value.message
