"""Unit tests for symbol dictionary detector."""

from pathlib import Path
from unittest.mock import Mock, patch
from uuid import uuid4

import pytest
from openpyxl import Workbook

from src.extraction.models import (
    DetectedDictionary,
    SymbolDetectionSummary,
    SymbolEntry,
)
from src.extraction.symbol_detector import (
    DICTIONARY_NAME_PATTERNS,
    MAX_SYMBOL_LENGTH,
    MIN_DATA_ROWS,
    SymbolDetector,
)


@pytest.fixture
def detector():
    """Create a SymbolDetector instance."""
    return SymbolDetector()


@pytest.fixture
def sample_workbook():
    """Create a sample workbook with various sheets."""
    wb = Workbook()

    # Legend sheet (name-based detection)
    legend = wb.active
    legend.title = "Legend"
    legend["A1"] = "Symbol"
    legend["B1"] = "Meaning"
    legend["A2"] = "○"
    legend["B2"] = "Applicable/Yes"
    legend["A3"] = "×"
    legend["B3"] = "Not applicable/No"
    legend["A4"] = "△"
    legend["B4"] = "Pending review"

    # Data sheet (not a dictionary)
    data = wb.create_sheet("DataSheet")
    data["A1"] = "Product Name"
    data["B1"] = "Price"
    data["C1"] = "Quantity"
    data["D1"] = "Total"
    data["A2"] = "Widget A"
    data["B2"] = 100.00
    data["C2"] = 5
    data["D2"] = 500.00

    return wb


@pytest.fixture
def structure_based_workbook():
    """Create a workbook with structure-based dictionary."""
    wb = Workbook()

    # Sheet with dictionary structure but no name pattern
    codes = wb.active
    codes.title = "ErrorCodes"
    codes["A1"] = "Code"
    codes["B1"] = "Description"
    codes["A2"] = "E01"
    codes["B2"] = "Invalid input format detected"
    codes["A3"] = "E02"
    codes["B3"] = "Missing required field in request"
    codes["A4"] = "E03"
    codes["B4"] = "Connection timeout occurred"
    codes["A5"] = "E04"
    codes["B5"] = "Authentication failed for user"

    return wb


class TestSymbolEntryModel:
    """Tests for SymbolEntry dataclass."""

    def test_valid_symbol_entry(self):
        """Test creating a valid symbol entry."""
        entry = SymbolEntry(symbol="○", meaning="Applicable/Yes")
        assert entry.symbol == "○"
        assert entry.meaning == "Applicable/Yes"
        assert entry.context is None

    def test_symbol_entry_with_context(self):
        """Test creating symbol entry with context."""
        entry = SymbolEntry(symbol="07", meaning="No required fields", context="error codes")
        assert entry.symbol == "07"
        assert entry.context == "error codes"

    def test_symbol_entry_empty_symbol_raises(self):
        """Test that empty symbol raises ValueError."""
        with pytest.raises(ValueError, match="Symbol cannot be empty"):
            SymbolEntry(symbol="", meaning="Some meaning")

    def test_symbol_entry_long_symbol_raises(self):
        """Test that symbol exceeding max length raises ValueError."""
        with pytest.raises(ValueError, match="Symbol too long"):
            SymbolEntry(symbol="x" * 51, meaning="Some meaning")

    def test_symbol_entry_empty_meaning_raises(self):
        """Test that empty meaning raises ValueError."""
        with pytest.raises(ValueError, match="Meaning cannot be empty"):
            SymbolEntry(symbol="○", meaning="")

    def test_symbol_entry_to_dict(self):
        """Test SymbolEntry.to_dict() serialization."""
        entry = SymbolEntry(symbol="○", meaning="Yes", context="status")
        result = entry.to_dict()
        assert result["symbol"] == "○"
        assert result["meaning"] == "Yes"
        assert result["context"] == "status"

    def test_symbol_entry_to_dict_no_context(self):
        """Test SymbolEntry.to_dict() without context."""
        entry = SymbolEntry(symbol="○", meaning="Yes")
        result = entry.to_dict()
        assert "context" not in result


class TestDetectedDictionaryModel:
    """Tests for DetectedDictionary dataclass."""

    def test_valid_detected_dictionary(self):
        """Test creating a valid detected dictionary."""
        symbols = [SymbolEntry(symbol="○", meaning="Yes")]
        dd = DetectedDictionary(
            sheet_name="Legend",
            detection_method="name",
            confidence=0.9,
            symbols=symbols,
        )
        assert dd.sheet_name == "Legend"
        assert dd.detection_method == "name"
        assert dd.confidence == 0.9
        assert len(dd.symbols) == 1

    def test_invalid_detection_method_raises(self):
        """Test that invalid detection method raises ValueError."""
        with pytest.raises(ValueError, match="Invalid detection method"):
            DetectedDictionary(
                sheet_name="Test",
                detection_method="invalid",
                confidence=0.5,
                symbols=[],
            )

    def test_invalid_confidence_raises(self):
        """Test that invalid confidence raises ValueError."""
        with pytest.raises(ValueError, match="Invalid confidence"):
            DetectedDictionary(
                sheet_name="Test",
                detection_method="name",
                confidence=1.5,
                symbols=[],
            )

    def test_detected_dictionary_to_dict(self):
        """Test DetectedDictionary.to_dict() serialization."""
        symbols = [SymbolEntry(symbol="○", meaning="Yes")]
        dd = DetectedDictionary(
            sheet_name="Legend",
            detection_method="name",
            confidence=0.9,
            symbols=symbols,
        )
        result = dd.to_dict()
        assert result["sheet_name"] == "Legend"
        assert result["symbol_count"] == 1
        assert len(result["symbols"]) == 1


class TestSymbolDetectionSummary:
    """Tests for SymbolDetectionSummary dataclass."""

    def test_valid_summary(self):
        """Test creating a valid summary."""
        summary = SymbolDetectionSummary(
            dictionaries_found=2,
            total_symbols=10,
            detected_sheets=[{"sheet_name": "Legend"}],
            warnings=[],
        )
        assert summary.dictionaries_found == 2
        assert summary.total_symbols == 10

    def test_summary_to_dict(self):
        """Test SymbolDetectionSummary.to_dict() serialization."""
        summary = SymbolDetectionSummary(
            dictionaries_found=1,
            total_symbols=5,
            detected_sheets=[{"sheet_name": "Legend"}],
            warnings=["Test warning"],
        )
        result = summary.to_dict()
        assert result["dictionaries_found"] == 1
        assert result["total_symbols"] == 5
        assert len(result["warnings"]) == 1


class TestNameBasedDetection:
    """Tests for name-based dictionary detection."""

    def test_detect_legend_by_name(self, detector):
        """Test detecting sheet named 'Legend'."""
        assert detector._is_dictionary_by_name("Legend") is True

    def test_detect_dictionary_by_name(self, detector):
        """Test detecting sheet named 'Dictionary'."""
        assert detector._is_dictionary_by_name("Dictionary") is True

    def test_detect_symbols_by_name(self, detector):
        """Test detecting sheet named 'Symbols'."""
        assert detector._is_dictionary_by_name("Symbols") is True

    def test_detect_codes_by_name(self, detector):
        """Test detecting sheet named 'Codes'."""
        assert detector._is_dictionary_by_name("Codes") is True

    def test_detect_key_by_name(self, detector):
        """Test detecting sheet named 'Key'."""
        assert detector._is_dictionary_by_name("Key") is True

    def test_detect_glossary_by_name(self, detector):
        """Test detecting sheet named 'Glossary'."""
        assert detector._is_dictionary_by_name("Glossary") is True

    def test_case_insensitive_detection(self, detector):
        """Test case-insensitive name detection."""
        assert detector._is_dictionary_by_name("LEGEND") is True
        assert detector._is_dictionary_by_name("legend") is True
        assert detector._is_dictionary_by_name("LeGeNd") is True

    def test_partial_name_match(self, detector):
        """Test partial name matching."""
        assert detector._is_dictionary_by_name("Symbol Legend") is True
        assert detector._is_dictionary_by_name("Error Codes List") is True

    def test_non_dictionary_name(self, detector):
        """Test non-dictionary sheet names."""
        assert detector._is_dictionary_by_name("DataSheet") is False
        assert detector._is_dictionary_by_name("Sheet1") is False
        assert detector._is_dictionary_by_name("Summary") is False

    def test_empty_name(self, detector):
        """Test empty sheet name."""
        assert detector._is_dictionary_by_name("") is False
        assert detector._is_dictionary_by_name(None) is False


class TestStructureBasedDetection:
    """Tests for structure-based dictionary detection."""

    def test_detect_by_structure_valid(self, detector, structure_based_workbook):
        """Test detecting dictionary by structure."""
        sheet = structure_based_workbook.active
        is_dict, confidence = detector._is_dictionary_by_structure(sheet)
        assert is_dict is True
        assert confidence > 0

    def test_empty_sheet_not_dictionary(self, detector):
        """Test that empty sheet is not detected as dictionary."""
        wb = Workbook()
        sheet = wb.active
        is_dict, confidence = detector._is_dictionary_by_structure(sheet)
        assert is_dict is False
        assert confidence == 0.0

    def test_single_row_not_dictionary(self, detector):
        """Test that single row sheet is not detected as dictionary."""
        wb = Workbook()
        sheet = wb.active
        sheet["A1"] = "Test"
        sheet["B1"] = "Value"
        is_dict, confidence = detector._is_dictionary_by_structure(sheet)
        assert is_dict is False

    def test_too_many_columns_not_dictionary(self, detector):
        """Test that sheet with many columns is not detected."""
        wb = Workbook()
        sheet = wb.active
        for col in range(1, 10):
            sheet.cell(row=1, column=col, value=f"Col{col}")
        for row in range(2, 6):
            for col in range(1, 10):
                sheet.cell(row=row, column=col, value=f"Val{row}{col}")
        is_dict, confidence = detector._is_dictionary_by_structure(sheet)
        assert is_dict is False

    def test_long_first_column_not_dictionary(self, detector):
        """Test that sheet with long first column values is not detected."""
        wb = Workbook()
        sheet = wb.active
        for row in range(1, 6):
            sheet.cell(row=row, column=1, value="This is a very long value that exceeds limit")
            sheet.cell(row=row, column=2, value="Description")
        is_dict, confidence = detector._is_dictionary_by_structure(sheet)
        assert is_dict is False


class TestSymbolExtraction:
    """Tests for symbol extraction."""

    def test_extract_symbols_basic(self, detector, sample_workbook):
        """Test basic symbol extraction."""
        legend = sample_workbook["Legend"]
        symbols = detector._extract_symbols(legend)
        assert len(symbols) == 3  # Skips header row
        assert symbols[0].symbol == "○"
        assert symbols[0].meaning == "Applicable/Yes"

    def test_extract_symbols_with_context(self, detector):
        """Test extraction with context column."""
        wb = Workbook()
        sheet = wb.active
        sheet["A1"] = "Symbol"
        sheet["B1"] = "Meaning"
        sheet["C1"] = "Context"
        sheet["A2"] = "07"
        sheet["B2"] = "No required fields"
        sheet["C2"] = "error codes"

        symbols = detector._extract_symbols(sheet)
        assert len(symbols) == 1
        assert symbols[0].context == "error codes"

    def test_extract_unicode_symbols(self, detector):
        """Test extraction of Unicode symbols."""
        wb = Workbook()
        sheet = wb.active
        sheet["A1"] = "○"
        sheet["B1"] = "Circle/Yes"
        sheet["A2"] = "×"
        sheet["B2"] = "Cross/No"
        sheet["A3"] = "△"
        sheet["B3"] = "Triangle/Maybe"
        sheet["A4"] = "◎"
        sheet["B4"] = "Double circle/Priority"

        symbols = detector._extract_symbols(sheet)
        assert len(symbols) == 4
        assert symbols[0].symbol == "○"
        assert symbols[1].symbol == "×"

    def test_skip_duplicate_symbols(self, detector):
        """Test that duplicate symbols are skipped."""
        wb = Workbook()
        sheet = wb.active
        # Use header row first so we can test actual data rows
        sheet["A1"] = "Symbol"
        sheet["B1"] = "Description"
        sheet["A2"] = "○"
        sheet["B2"] = "First occurrence - should be kept"
        sheet["A3"] = "○"
        sheet["B3"] = "Duplicate - should be skipped"
        sheet["A4"] = "×"
        sheet["B4"] = "Different symbol"

        symbols = detector._extract_symbols(sheet)
        assert len(symbols) == 2
        assert symbols[0].meaning == "First occurrence - should be kept"

    def test_skip_empty_rows(self, detector):
        """Test that empty rows are skipped."""
        wb = Workbook()
        sheet = wb.active
        sheet["A1"] = "○"
        sheet["B1"] = "Yes"
        sheet["A2"] = None
        sheet["B2"] = None
        sheet["A3"] = "×"
        sheet["B3"] = "No"

        symbols = detector._extract_symbols(sheet)
        assert len(symbols) == 2

    def test_skip_rows_missing_meaning(self, detector):
        """Test that rows without meaning are skipped."""
        wb = Workbook()
        sheet = wb.active
        sheet["A1"] = "○"
        sheet["B1"] = "Yes"
        sheet["A2"] = "×"
        sheet["B2"] = None  # Missing meaning

        symbols = detector._extract_symbols(sheet)
        assert len(symbols) == 1


class TestFullDetection:
    """Tests for full dictionary detection workflow."""

    def test_detect_dictionaries_basic(self, detector, sample_workbook):
        """Test full detection workflow."""
        dictionaries, summary = detector.detect_dictionaries(sample_workbook)

        assert len(dictionaries) == 1  # Only Legend sheet
        assert dictionaries[0].sheet_name == "Legend"
        assert dictionaries[0].detection_method == "name"
        assert summary.dictionaries_found == 1

    def test_detect_multiple_dictionaries(self, detector):
        """Test detecting multiple dictionary sheets."""
        wb = Workbook()

        # Legend sheet
        legend = wb.active
        legend.title = "Legend"
        legend["A1"] = "○"
        legend["B1"] = "Yes"
        legend["A2"] = "×"
        legend["B2"] = "No"
        legend["A3"] = "△"
        legend["B3"] = "Maybe"

        # Symbols sheet
        symbols = wb.create_sheet("Symbols")
        symbols["A1"] = "*"
        symbols["B1"] = "Required"
        symbols["A2"] = "#"
        symbols["B2"] = "Auto-calculated"
        symbols["A3"] = "!"
        symbols["B3"] = "Warning"

        dictionaries, summary = detector.detect_dictionaries(wb)

        assert len(dictionaries) == 2
        assert summary.dictionaries_found == 2
        assert summary.total_symbols == 6

    def test_detect_no_dictionaries(self, detector):
        """Test when no dictionaries are found."""
        wb = Workbook()
        sheet = wb.active
        sheet.title = "DataSheet"
        sheet["A1"] = "Product"
        sheet["B1"] = "Price"
        sheet["C1"] = "Quantity"
        sheet["D1"] = "Total"
        sheet["A2"] = "Widget"
        sheet["B2"] = 100
        sheet["C2"] = 5
        sheet["D2"] = 500

        dictionaries, summary = detector.detect_dictionaries(wb)

        assert len(dictionaries) == 0
        assert summary.dictionaries_found == 0
        assert summary.total_symbols == 0

    def test_detect_empty_workbook(self, detector):
        """Test detection with empty workbook."""
        wb = Workbook()
        # Remove default sheet
        wb.remove(wb.active)

        dictionaries, summary = detector.detect_dictionaries(wb)

        assert len(dictionaries) == 0
        assert summary.dictionaries_found == 0
        assert len(summary.warnings) == 1

    def test_detection_with_fixture_file(self, detector):
        """Test detection with real fixture file."""
        from openpyxl import load_workbook

        fixture_path = Path("tests/fixtures/sample_excel/test_symbol_dictionary.xlsx")
        if fixture_path.exists():
            wb = load_workbook(fixture_path)
            dictionaries, summary = detector.detect_dictionaries(wb)

            # Should find Legend and Symbols by name
            assert summary.dictionaries_found >= 2
            assert summary.total_symbols >= 6


class TestDatabaseStorage:
    """Tests for database storage functionality."""

    def test_store_symbols_empty_list(self, detector):
        """Test storing empty dictionary list."""
        session = Mock()
        count = detector.store_symbols([], uuid4(), session)
        assert count == 0
        session.bulk_insert_mappings.assert_not_called()

    def test_store_symbols_single_dictionary(self, detector):
        """Test storing symbols from single dictionary."""
        session = Mock()
        doc_id = uuid4()

        symbols = [
            SymbolEntry(symbol="○", meaning="Yes"),
            SymbolEntry(symbol="×", meaning="No"),
        ]
        dictionary = DetectedDictionary(
            sheet_name="Legend",
            detection_method="name",
            confidence=0.9,
            symbols=symbols,
        )

        count = detector.store_symbols([dictionary], doc_id, session)

        assert count == 2
        session.bulk_insert_mappings.assert_called_once()
        session.commit.assert_called_once()

    def test_store_symbols_handles_duplicates_across_sheets(self, detector):
        """Test that duplicates across sheets are handled."""
        session = Mock()
        doc_id = uuid4()

        dict1 = DetectedDictionary(
            sheet_name="Legend",
            detection_method="name",
            confidence=0.9,
            symbols=[SymbolEntry(symbol="○", meaning="Yes from Legend")],
        )
        dict2 = DetectedDictionary(
            sheet_name="Symbols",
            detection_method="name",
            confidence=0.9,
            symbols=[
                SymbolEntry(symbol="○", meaning="Yes from Symbols"),  # Duplicate
                SymbolEntry(symbol="×", meaning="No"),
            ],
        )

        count = detector.store_symbols([dict1, dict2], doc_id, session)

        # Should store 2, not 3 (duplicate skipped)
        assert count == 2

    def test_store_symbols_rollback_on_error(self, detector):
        """Test that rollback is called on error."""
        session = Mock()
        session.bulk_insert_mappings.side_effect = Exception("DB Error")

        symbols = [SymbolEntry(symbol="○", meaning="Yes")]
        dictionary = DetectedDictionary(
            sheet_name="Legend",
            detection_method="name",
            confidence=0.9,
            symbols=symbols,
        )

        with pytest.raises(Exception, match="DB Error"):
            detector.store_symbols([dictionary], uuid4(), session)

        session.rollback.assert_called_once()
