"""Unit tests for RecordToNaturalLanguage service."""

import pytest
from unittest.mock import AsyncMock, patch, MagicMock

from src.extraction_v2.record_to_natural_language import (
    RecordToNaturalLanguage,
    SYSTEM_PROMPT,
    USER_PROMPT_TEMPLATE,
)

# Configure pytest-asyncio to use auto mode
pytestmark = pytest.mark.asyncio(loop_scope="session")


class TestRecordToNaturalLanguage:
    """Tests for RecordToNaturalLanguage class."""

    async def test_successful_conversion(self):
        """Test successful LLM conversion of records."""
        # Mock LLM service
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(
            return_value="In Employees (row 5), the name is Alice and the age is 30."
        )

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [{"Name": "Alice", "Age": "30"}]
        sheets = ["Employees"]
        rows = [5]

        result = await converter.convert_batch(records, sheets, rows)

        assert len(result) == 1
        assert "Alice" in result[0]
        assert "Employees" in result[0]
        assert "row 5" in result[0]
        mock_service.ainvoke.assert_called_once()

    async def test_batch_processing_multiple_records(self):
        """Test batch processing with multiple records."""
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(
            side_effect=[
                "In Sheet1 (row 1), the item is Widget and the price is $10.",
                "In Sheet1 (row 2), the item is Gadget and the price is $20.",
                "In Sheet1 (row 3), the item is Thing and the price is $30.",
            ]
        )

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [
            {"Item": "Widget", "Price": "$10"},
            {"Item": "Gadget", "Price": "$20"},
            {"Item": "Thing", "Price": "$30"},
        ]
        sheets = ["Sheet1", "Sheet1", "Sheet1"]
        rows = [1, 2, 3]

        result = await converter.convert_batch(records, sheets, rows)

        assert len(result) == 3
        assert "Widget" in result[0]
        assert "Gadget" in result[1]
        assert "Thing" in result[2]
        assert mock_service.ainvoke.call_count == 3

    
    async def test_graceful_fallback_on_exception(self):
        """Test fallback to record_to_text when LLM fails."""
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(side_effect=Exception("LLM API error"))

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [{"Name": "Bob", "Department": "IT"}]
        sheets = ["Staff"]
        rows = [10]

        result = await converter.convert_batch(records, sheets, rows)

        assert len(result) == 1
        # Fallback should use record_to_text format
        assert "Name: Bob" in result[0]
        assert "Department: IT" in result[0]
        assert "Staff" in result[0]
        assert "Row: 10" in result[0]

    
    async def test_timeout_handling(self):
        """Test timeout handling with AsyncTimeoutError."""
        import asyncio

        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(side_effect=asyncio.TimeoutError("Timeout"))

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [{"Field": "Value"}]
        sheets = ["Sheet1"]
        rows = [1]

        result = await converter.convert_batch(records, sheets, rows)

        assert len(result) == 1
        # Should fallback to record_to_text
        assert "Field: Value" in result[0]

    
    async def test_empty_records(self):
        """Test handling of empty record list."""
        converter = RecordToNaturalLanguage()

        result = await converter.convert_batch([], [], [])

        assert result == []

    
    async def test_mismatched_input_lengths(self):
        """Test error handling for mismatched input lengths."""
        converter = RecordToNaturalLanguage()

        records = [{"A": "B"}]
        sheets = ["Sheet1", "Sheet2"]  # Wrong length
        rows = [1]

        with pytest.raises(ValueError, match="Input length mismatch"):
            await converter.convert_batch(records, sheets, rows)

    
    @pytest.mark.parametrize(
        "record,expected_field",
        [
            ({"Name": "Test"}, "Name"),
            ({"Key": "Value", "Another": "Field"}, "Key"),
            ({"日本語": "テスト"}, "日本語"),  # Unicode
            ({"Field&Special": "Value<>"}, "Field&Special"),  # Special chars
        ],
    )
    async def test_various_record_structures(self, record, expected_field):
        """Test conversion with various record structures."""
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(
            return_value=f"In Sheet1 (row 1), {expected_field} present."
        )

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        result = await converter.convert_batch([record], ["Sheet1"], [1])

        assert len(result) == 1
        assert expected_field in str(result[0]) or expected_field in str(record)

    
    async def test_large_record_handling(self):
        """Test handling of large records with many fields."""
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(
            return_value="In Sheet1 (row 1), converted successfully."
        )

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        # Create record with 100 fields
        large_record = {f"Field_{i}": f"Value_{i}" for i in range(100)}

        result = await converter.convert_batch([large_record], ["Sheet1"], [1])

        assert len(result) == 1
        # Verify LLM was called with the large record
        mock_service.ainvoke.assert_called_once()

    
    async def test_fallback_conversion_directly(self):
        """Test the _fallback_conversion method directly."""
        converter = RecordToNaturalLanguage()

        record = {"Name": "Charlie", "Role": "Engineer"}
        result = converter._fallback_conversion(record, "Team", 25)

        assert "Name: Charlie" in result
        assert "Role: Engineer" in result
        assert "Team" in result
        assert "Row: 25" in result

    
    async def test_partial_batch_success(self):
        """Test batch where some records succeed and some fail."""
        mock_service = AsyncMock()
        # First succeeds, second fails, third succeeds
        mock_service.ainvoke = AsyncMock(
            side_effect=[
                "Success 1",
                Exception("Fail"),
                "Success 3",
            ]
        )

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [
            {"A": "1"},
            {"B": "2"},
            {"C": "3"},
        ]
        sheets = ["S1", "S2", "S3"]
        rows = [1, 2, 3]

        result = await converter.convert_batch(records, sheets, rows)

        assert len(result) == 3
        assert "Success 1" == result[0]
        # Second should be fallback format
        assert "B: 2" in result[1]
        assert "Success 3" == result[2]

    def test_prompts_are_defined(self):
        """Test that required prompts are properly defined."""
        assert SYSTEM_PROMPT is not None
        assert len(SYSTEM_PROMPT) > 0
        assert "natural language" in SYSTEM_PROMPT.lower()

        assert USER_PROMPT_TEMPLATE is not None
        assert "{sheet_name}" in USER_PROMPT_TEMPLATE
        assert "{row_number}" in USER_PROMPT_TEMPLATE
        assert "{record_json}" in USER_PROMPT_TEMPLATE



    async def test_empty_field_values_handled(self):
        """Test records with empty/None field values."""
        mock_service = AsyncMock()
        mock_service.ainvoke = AsyncMock(return_value="Converted")

        converter = RecordToNaturalLanguage()
        converter.llm_service = mock_service

        records = [{"Name": "Test", "Empty": "", "Null": None}]
        result = await converter.convert_batch(records, ["Sheet1"], [1])

        assert len(result) == 1
        mock_service.ainvoke.assert_called_once()
