"""Unit tests for cross-reference detection and resolution."""

from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest

from src.extraction.cross_ref_detector import (
    CROSS_REF_PATTERNS,
    CrossRefDetector,
    CrossRefResolutionSummary,
    CrossReferenceMatch,
)


class TestCrossReferenceMatch:
    """Tests for CrossReferenceMatch dataclass."""

    def test_valid_sheet_ref(self):
        """Test creating a valid sheet reference match."""
        match = CrossReferenceMatch(
            reference_text="See Sheet Mapping",
            reference_type="sheet_ref",
            field_name="notes",
            sheet_target="Mapping",
        )
        assert match.reference_type == "sheet_ref"
        assert match.sheet_target == "Mapping"
        assert match.resolved is False

    def test_valid_item_ref(self):
        """Test creating a valid item reference match."""
        match = CrossReferenceMatch(
            reference_text="Item 2024-4_0019",
            reference_type="item_ref",
            field_name="reference",
            item_target="2024-4_0019",
        )
        assert match.reference_type == "item_ref"
        assert match.item_target == "2024-4_0019"

    def test_valid_sheet_item_ref(self):
        """Test creating a combined sheet+item reference match."""
        match = CrossReferenceMatch(
            reference_text="See Sheet Data, Item 001",
            reference_type="sheet_item_ref",
            field_name="cross_ref",
            sheet_target="Data",
            item_target="001",
        )
        assert match.reference_type == "sheet_item_ref"
        assert match.sheet_target == "Data"
        assert match.item_target == "001"

    def test_invalid_reference_type_rejected(self):
        """Test that invalid reference type is rejected."""
        with pytest.raises(ValueError, match="Invalid reference type"):
            CrossReferenceMatch(
                reference_text="Some ref",
                reference_type="invalid_type",
                field_name="field",
            )

    def test_empty_reference_text_rejected(self):
        """Test that empty reference text is rejected."""
        with pytest.raises(ValueError, match="Reference text cannot be empty"):
            CrossReferenceMatch(
                reference_text="",
                reference_type="sheet_ref",
                field_name="field",
            )

    def test_empty_field_name_rejected(self):
        """Test that empty field name is rejected."""
        with pytest.raises(ValueError, match="Field name cannot be empty"):
            CrossReferenceMatch(
                reference_text="See Sheet X",
                reference_type="sheet_ref",
                field_name="",
            )

    def test_to_dict(self):
        """Test CrossReferenceMatch.to_dict() method."""
        source_id = uuid4()
        target_id = uuid4()
        match = CrossReferenceMatch(
            reference_text="See Sheet Data",
            reference_type="sheet_ref",
            field_name="notes",
            sheet_target="Data",
            source_record_id=source_id,
            target_record_id=target_id,
            resolved=True,
        )

        d = match.to_dict()
        assert d["reference_text"] == "See Sheet Data"
        assert d["reference_type"] == "sheet_ref"
        assert d["field_name"] == "notes"
        assert d["sheet_target"] == "Data"
        assert d["source_record_id"] == str(source_id)
        assert d["target_record_id"] == str(target_id)
        assert d["resolved"] is True


class TestCrossRefResolutionSummary:
    """Tests for CrossRefResolutionSummary dataclass."""

    def test_to_dict(self):
        """Test CrossRefResolutionSummary.to_dict() method."""
        doc_id = uuid4()
        summary = CrossRefResolutionSummary(
            document_id=doc_id,
            references_found=10,
            references_resolved=7,
            unresolved_count=3,
            unresolved_refs=["Ref A", "Ref B", "Ref C"],
            warnings=["Warning 1"],
        )

        d = summary.to_dict()
        assert d["document_id"] == str(doc_id)
        assert d["references_found"] == 10
        assert d["references_resolved"] == 7
        assert d["unresolved_count"] == 3
        assert d["unresolved_refs"] == ["Ref A", "Ref B", "Ref C"]
        assert d["warnings"] == ["Warning 1"]


class TestCrossRefPatterns:
    """Tests for cross-reference pattern matching."""

    def test_sheet_ref_pattern_english(self):
        """Test English 'See Sheet X' pattern detection."""
        detector = CrossRefDetector()
        content = {"notes": "See Sheet Mapping for details"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        sheet_match = next((m for m in matches if m.reference_type == "sheet_ref"), None)
        assert sheet_match is not None
        assert sheet_match.sheet_target == "Mapping for details"

    def test_sheet_ref_pattern_japanese(self):
        """Test Japanese '参照 シート' pattern detection."""
        detector = CrossRefDetector()
        content = {"備考": "参照 シート「データ一覧」"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        sheet_match = next((m for m in matches if m.reference_type == "sheet_ref"), None)
        assert sheet_match is not None

    def test_item_ref_pattern(self):
        """Test 'Item CODE' pattern detection."""
        detector = CrossRefDetector()
        content = {"reference": "Item 2024-4_0019"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        item_match = next((m for m in matches if m.reference_type == "item_ref"), None)
        assert item_match is not None
        assert item_match.item_target == "2024-4_0019"

    def test_code_ref_pattern(self):
        """Test 'Ref: CODE' pattern detection."""
        detector = CrossRefDetector()
        content = {"link": "Ref: ABC-123"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        code_match = next((m for m in matches if m.reference_type == "code_ref"), None)
        assert code_match is not None
        assert code_match.item_target == "ABC-123"

    def test_page_item_ref_pattern(self):
        """Test 'Page X, Item Y' pattern detection."""
        detector = CrossRefDetector()
        content = {"source": "Page 01.2-5, Item 3"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        page_match = next((m for m in matches if m.reference_type == "page_ref"), None)
        assert page_match is not None
        assert page_match.page_target == "01.2-5"
        assert page_match.item_target == "3"

    def test_multiple_references_in_cell(self):
        """Test detecting multiple references in a single cell."""
        detector = CrossRefDetector()
        content = {"notes": "See Sheet A. Item ABC123. Ref: XYZ-789"}
        matches = detector.detect_references(content)

        assert len(matches) >= 2  # At least sheet_ref and item_ref

    def test_no_references_in_content(self):
        """Test that no matches are returned for normal content."""
        detector = CrossRefDetector()
        content = {"name": "John Doe", "value": "12345"}
        matches = detector.detect_references(content)

        # Normal alphanumeric values might match item_ref pattern
        # But shouldn't have sheet refs
        sheet_matches = [m for m in matches if m.reference_type == "sheet_ref"]
        assert len(sheet_matches) == 0


class TestCrossRefDetectorDetection:
    """Tests for CrossRefDetector.detect_references() method."""

    def test_empty_content(self):
        """Test detection with empty content."""
        detector = CrossRefDetector()
        matches = detector.detect_references({})
        assert matches == []

    def test_none_values_skipped(self):
        """Test that None values are skipped."""
        detector = CrossRefDetector()
        content = {"field1": None, "field2": "See Sheet X"}
        matches = detector.detect_references(content)

        assert len(matches) >= 1
        assert all(m.field_name == "field2" for m in matches if m.reference_type == "sheet_ref")

    def test_non_string_values_converted(self):
        """Test that non-string values are converted."""
        detector = CrossRefDetector()
        content = {"number": 12345, "boolean": True}
        # Should not raise, should handle gracefully
        matches = detector.detect_references(content)
        # These might match item_ref patterns for short alphanumeric codes
        assert isinstance(matches, list)

    def test_field_name_captured(self):
        """Test that field name is captured for each match."""
        detector = CrossRefDetector()
        content = {"notes": "See Sheet X", "reference": "Item ABC"}
        matches = detector.detect_references(content)

        assert len(matches) >= 2
        field_names = {m.field_name for m in matches}
        assert "notes" in field_names or "reference" in field_names


class TestCrossRefDetectorResolution:
    """Tests for CrossRefDetector.resolve_reference() method."""

    def test_resolve_sheet_only_reference(self):
        """Test resolving a sheet-only reference."""
        detector = CrossRefDetector()
        doc_id = uuid4()
        target_record_id = uuid4()

        match = CrossReferenceMatch(
            reference_text="See Sheet Data",
            reference_type="sheet_ref",
            field_name="notes",
            sheet_target="Data",
        )

        mock_session = MagicMock()
        mock_record = MagicMock()
        mock_record.id = target_record_id

        # Set up query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.first.return_value = mock_record

        result = detector.resolve_reference(match, doc_id, mock_session)

        assert result == target_record_id

    def test_resolve_item_reference(self):
        """Test resolving an item code reference."""
        detector = CrossRefDetector()
        doc_id = uuid4()
        target_record_id = uuid4()

        match = CrossReferenceMatch(
            reference_text="Item ABC-123",
            reference_type="item_ref",
            field_name="ref",
            item_target="ABC-123",
        )

        mock_session = MagicMock()
        mock_record = MagicMock()
        mock_record.id = target_record_id
        mock_record.content = {"item_code": "ABC-123"}

        # Set up query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = [mock_record]

        result = detector.resolve_reference(match, doc_id, mock_session)

        assert result == target_record_id

    def test_resolve_unresolvable_reference(self):
        """Test that unresolvable references return None."""
        detector = CrossRefDetector()
        doc_id = uuid4()

        match = CrossReferenceMatch(
            reference_text="See Sheet NonExistent",
            reference_type="sheet_ref",
            field_name="notes",
            sheet_target="NonExistent",
        )

        mock_session = MagicMock()

        # Set up query chain to return empty
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.first.return_value = None

        result = detector.resolve_reference(match, doc_id, mock_session)

        assert result is None


class TestCrossRefDetectorContentSearch:
    """Tests for CrossRefDetector content search methods."""

    def test_content_contains_exact_match(self):
        """Test exact match in content."""
        detector = CrossRefDetector()

        content = {"item_code": "ABC-123", "name": "Test Item"}
        assert detector._content_contains_item(content, "ABC-123") is True

    def test_content_contains_partial_match(self):
        """Test partial match in content value."""
        detector = CrossRefDetector()

        content = {"description": "Reference to ABC-123 in document"}
        assert detector._content_contains_item(content, "ABC-123") is True

    def test_content_contains_case_insensitive(self):
        """Test case-insensitive matching."""
        detector = CrossRefDetector()

        content = {"code": "abc-123"}
        assert detector._content_contains_item(content, "ABC-123") is True

    def test_content_not_contains(self):
        """Test when content doesn't contain item."""
        detector = CrossRefDetector()

        content = {"name": "Test", "value": "Something else"}
        assert detector._content_contains_item(content, "ABC-123") is False

    def test_content_empty(self):
        """Test with empty content."""
        detector = CrossRefDetector()
        assert detector._content_contains_item({}, "ABC-123") is False
        assert detector._content_contains_item(None, "ABC-123") is False


class TestCrossRefDetectorStorage:
    """Tests for CrossRefDetector storage methods."""

    def test_store_cross_references_new(self):
        """Test storing new cross-references."""
        detector = CrossRefDetector()
        source_id = uuid4()
        target_id = uuid4()

        matches = [
            CrossReferenceMatch(
                reference_text="See Sheet X",
                reference_type="sheet_ref",
                field_name="notes",
                sheet_target="X",
                source_record_id=source_id,
                target_record_id=target_id,
                resolved=True,
            )
        ]

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.first.return_value = None  # No existing record

        stored = detector._store_cross_references(matches, mock_session)

        assert stored == 1
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    def test_store_cross_references_update_existing(self):
        """Test updating existing cross-references."""
        detector = CrossRefDetector()
        source_id = uuid4()
        target_id = uuid4()

        matches = [
            CrossReferenceMatch(
                reference_text="See Sheet X",
                reference_type="sheet_ref",
                field_name="notes",
                sheet_target="X",
                source_record_id=source_id,
                target_record_id=target_id,
                resolved=True,
            )
        ]

        mock_session = MagicMock()
        mock_existing = MagicMock()

        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.first.return_value = mock_existing  # Existing record

        stored = detector._store_cross_references(matches, mock_session)

        assert stored == 1
        assert mock_existing.target_record_id == target_id
        assert mock_existing.resolved is True
        mock_session.add.assert_not_called()  # Should update, not add

    def test_store_empty_matches(self):
        """Test storing empty matches list."""
        detector = CrossRefDetector()
        mock_session = MagicMock()

        stored = detector._store_cross_references([], mock_session)

        assert stored == 0
        mock_session.add.assert_not_called()


class TestCrossRefDetectorProcessDocument:
    """Tests for CrossRefDetector.process_document() method."""

    def test_process_document_no_records(self):
        """Test processing document with no records."""
        detector = CrossRefDetector()
        doc_id = uuid4()

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.all.return_value = []

        summary = detector.process_document(doc_id, mock_session)

        assert summary.document_id == doc_id
        assert summary.references_found == 0
        assert summary.references_resolved == 0
        assert "No extracted records found" in summary.warnings[0]

    def test_process_document_with_references(self):
        """Test processing document with cross-references."""
        detector = CrossRefDetector()
        doc_id = uuid4()
        record_id = uuid4()
        target_id = uuid4()

        mock_session = MagicMock()
        mock_record = MagicMock()
        mock_record.id = record_id
        mock_record.content = {"notes": "See Sheet Data"}

        mock_target = MagicMock()
        mock_target.id = target_id
        mock_target.content = {"name": "Data Sheet"}

        # Set up query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_filter = MagicMock()
        mock_query.filter.return_value = mock_filter

        # First call returns records for this document
        # Second call is for resolution
        call_count = [0]

        def filter_side_effect(*args, **kwargs):
            call_count[0] += 1
            return mock_filter

        mock_query.filter.side_effect = filter_side_effect
        mock_filter.filter.return_value = mock_filter
        mock_filter.all.return_value = [mock_record]
        mock_filter.order_by.return_value = mock_filter
        mock_filter.first.return_value = mock_target

        summary = detector.process_document(doc_id, mock_session)

        assert summary.document_id == doc_id
        assert summary.references_found >= 0  # May or may not find references
