"""
Test cases for LiuYao AI analysis functionality.

This module tests various scenarios including normal cases, edge cases, and outliers.
"""
import json
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from django.utils import timezone
from datetime import datetime

from ai.utils.liuyao_analysis import (
    LiuYaoConstants,
    DataFormatter,
    ResponseValidator,
    get_hexagram_name,
    get_trigram_name,
    identify_changing_lines,
    analyze_liuyao,
    prepare_liuyao_prompt
)


class MockLiuYao:
    """Mock LiuYao object for testing."""
    
    def __init__(self, **kwargs):
        self.id = kwargs.get('id', 1)
        self.question = kwargs.get('question', "测试问题")
        self.qdate = kwargs.get('qdate', timezone.now())
        self.y1 = kwargs.get('y1', '111')
        self.y2 = kwargs.get('y2', '0')
        self.y3 = kwargs.get('y3', '111')
        self.y4 = kwargs.get('y4', '1')
        self.y5 = kwargs.get('y5', '111')
        self.y6 = kwargs.get('y6', '111')
        self.reading = kwargs.get('reading', '')
        self.data = kwargs.get('data', {})
        self.ai_analysis = kwargs.get('ai_analysis', None)
        self.analysis_status = kwargs.get('analysis_status', 'pending')
        self.analysis_timestamp = kwargs.get('analysis_timestamp', None)
    
    def save(self, **kwargs):
        """Mock save method."""
        pass


class TestLiuYaoConstants(TestCase):
    """Test LiuYao constants and utility functions."""
    
    def test_hexagram_names_coverage(self):
        """Test that all 64 hexagrams are defined."""
        self.assertEqual(len(LiuYaoConstants.HEXAGRAM_NAMES), 64)
        for i in range(1, 65):
            self.assertIn(i, LiuYaoConstants.HEXAGRAM_NAMES)
    
    def test_trigram_names_coverage(self):
        """Test that all 8 trigrams are defined."""
        self.assertEqual(len(LiuYaoConstants.TRIGRAM_NAMES), 8)
        expected_trigrams = ["111", "000", "001", "010", "100", "110", "101", "011"]
        for trigram in expected_trigrams:
            self.assertIn(trigram, LiuYaoConstants.TRIGRAM_NAMES)
    
    def test_line_positions(self):
        """Test line positions are correctly defined."""
        self.assertEqual(len(LiuYaoConstants.LINE_POSITIONS), 6)
        for i in range(1, 7):
            self.assertIn(i, LiuYaoConstants.LINE_POSITIONS)
    
    def test_six_gods_names(self):
        """Test six gods names are correctly defined."""
        self.assertEqual(len(LiuYaoConstants.SIX_GOD_NAMES), 6)
        for i in range(6):
            self.assertIn(i, LiuYaoConstants.SIX_GOD_NAMES)


class TestUtilityFunctions(TestCase):
    """Test utility functions."""
    
    def test_get_hexagram_name(self):
        """Test hexagram name retrieval."""
        # Normal cases
        self.assertEqual(get_hexagram_name(1), "乾 (qián) - The Creative")
        self.assertEqual(get_hexagram_name(64), "未济 (wèi jì) - Before Completion")
        
        # Edge cases
        self.assertEqual(get_hexagram_name(0), "Unknown hexagram 0")
        self.assertEqual(get_hexagram_name(65), "Unknown hexagram 65")
        self.assertEqual(get_hexagram_name(-1), "Unknown hexagram -1")
    
    def test_get_trigram_name(self):
        """Test trigram name retrieval."""
        # Normal cases
        self.assertEqual(get_trigram_name("111"), "乾 (qián) - Heaven")
        self.assertEqual(get_trigram_name("000"), "坤 (kūn) - Earth")
        
        # Edge cases
        self.assertEqual(get_trigram_name("222"), "Unknown trigram 222")
        self.assertEqual(get_trigram_name(""), "Unknown trigram ")
    
    def test_identify_changing_lines(self):
        """Test identification of changing lines."""
        # Normal case - some changing lines (老阴老阳)
        liuyao = MockLiuYao(y1='000', y2='111', y3='1', y4='111', y5='000', y6='111')
        changing_lines = identify_changing_lines(liuyao)
        self.assertEqual(set(changing_lines), {1, 2, 4, 5, 6})
        
        # No changing lines
        liuyao = MockLiuYao(y1='1', y2='1', y3='1', y4='1', y5='1', y6='1')
        changing_lines = identify_changing_lines(liuyao)
        self.assertEqual(changing_lines, [])
        
        # All changing lines
        liuyao = MockLiuYao(y1='000', y2='111', y3='000', y4='111', y5='000', y6='111')
        changing_lines = identify_changing_lines(liuyao)
        self.assertEqual(set(changing_lines), {1, 2, 3, 4, 5, 6})


class TestDataFormatter(TestCase):
    """Test DataFormatter class."""
    
    def test_format_data_string(self):
        """Test formatting string data."""
        result = DataFormatter.format_data("test string", "test")
        self.assertEqual(result, "test string")
    
    def test_format_data_dict(self):
        """Test formatting dictionary data."""
        data = {"key1": "value1", "key2": "value2"}
        result = DataFormatter.format_data(data, "test")
        expected_lines = ["key1：value1", "key2：value2"]
        for line in expected_lines:
            self.assertIn(line, result)
    
    def test_format_data_list(self):
        """Test formatting list data."""
        data = ["item1", "item2", "item3"]
        result = DataFormatter.format_data(data, "test")
        expected_lines = ["item1", "item2", "item3"]
        for line in expected_lines:
            self.assertIn(line, result)
    
    def test_format_data_empty(self):
        """Test formatting empty data."""
        result = DataFormatter.format_data(None, "test")
        self.assertEqual(result, "无test数据 (No test data)")
        
        result = DataFormatter.format_data("", "test")
        self.assertEqual(result, "无test数据 (No test data)")
    
    def test_format_six_gods(self):
        """Test Six Gods formatting."""
        # Normal cases
        for i in range(6):
            result = DataFormatter.format_six_gods(i)
            self.assertIn("六神:", result)
            self.assertIn(LiuYaoConstants.SIX_GOD_NAMES[i], result)
        
        # Edge cases
        result = DataFormatter.format_six_gods(None)
        self.assertEqual(result, "无六神数据 (No Six Gods data)")
        
        result = DataFormatter.format_six_gods(10)
        self.assertIn("未知 (Unknown): 10", result)
    
    def test_format_wangshui_line(self):
        """Test wangshui line formatting."""
        # Normal case
        rel_data = {
            "day": ["empty", "cs12-3"],
            "month": ["produce", "cs12-7"]
        }
        result = DataFormatter.format_wangshui_line(rel_data)
        self.assertIn("日:", result)
        self.assertIn("月:", result)
        self.assertIn("空", result)  # "empty" maps to "空"
        self.assertIn("生", result)  # "produce" maps to "生"
        
        # Empty data
        result = DataFormatter.format_wangshui_line({})
        self.assertEqual(result, "")
        
        result = DataFormatter.format_wangshui_line(None)
        self.assertEqual(result, "")


class TestResponseValidator(TestCase):
    """Test ResponseValidator class."""
    
    def test_extract_json_from_text_markdown(self):
        """Test JSON extraction from markdown code blocks."""
        text = '''
        Here is the analysis:
        ```json
        {
            "think": "This is my thinking",
            "analysis": "This is the analysis"
        }
        ```
        '''
        result = ResponseValidator.extract_json_from_text(text)
        self.assertIsNotNone(result)
        self.assertEqual(result["think"], "This is my thinking")
        self.assertEqual(result["analysis"], "This is the analysis")
    
    def test_extract_json_from_text_direct(self):
        """Test direct JSON extraction."""
        text = '{"think": "Direct thinking", "analysis": "Direct analysis"}'
        result = ResponseValidator.extract_json_from_text(text)
        self.assertIsNotNone(result)
        self.assertEqual(result["think"], "Direct thinking")
        self.assertEqual(result["analysis"], "Direct analysis")
    
    def test_extract_json_from_text_invalid(self):
        """Test handling of invalid JSON."""
        text = "This is not JSON at all"
        result = ResponseValidator.extract_json_from_text(text)
        self.assertIsNone(result)
    
    def test_extract_fields_manually(self):
        """Test manual field extraction using regex."""
        text = 'Response with "think": "Manual thinking" and "analysis": "Manual analysis"'
        result = ResponseValidator.extract_fields_manually(text)
        self.assertEqual(result["think"], "Manual thinking")
        self.assertEqual(result["analysis"], "Manual analysis")
    
    def test_validate_liuyao_response_complete(self):
        """Test complete response validation."""
        # Valid JSON response
        response = '{"think": "Complete thinking", "analysis": "Complete analysis"}'
        result = ResponseValidator.validate_liuyao_response(response)
        self.assertEqual(result["think"], "Complete thinking")
        self.assertEqual(result["liuyao_analysis"], "Complete analysis")
        
        # Fallback to raw text
        response = "This is just plain text without JSON"
        result = ResponseValidator.validate_liuyao_response(response)
        self.assertEqual(result["liuyao_analysis"], response)
        self.assertEqual(result["think"], "")


class TestAnalyzeLiuYao(TestCase):
    """Test the main analyze_liuyao function."""
    
    @patch('ai.utils.liuyao_analysis.prepare_liuyao_prompt')
    @patch('ai.utils.liuyao_analysis.LLMServiceFactory')
    @patch('ai.utils.config.get_ai_config')
    def test_analyze_liuyao_success(self, mock_get_ai_config, mock_factory, mock_prepare_prompt):
        """Test successful analysis."""
        # Setup mocks
        mock_prepare_prompt.return_value = "Test prompt"
        mock_service = MagicMock()
        mock_service.get_completion.return_value = '{"think": "Test thinking", "analysis": "Test analysis"}'
        mock_service.model = "test-model"
        mock_factory.get_service.return_value = mock_service
        mock_get_ai_config.return_value = {'provider': 'test-provider', 'model': 'test-model'}
        
        # Create test LiuYao
        liuyao = MockLiuYao()
        
        # Run analysis
        result = analyze_liuyao(liuyao)
        
        # Verify results
        self.assertIsNotNone(result)
        self.assertEqual(result["think"], "Test thinking")
        self.assertEqual(result["liuyao_analysis"], "Test analysis")
        self.assertEqual(result["model"], "test-model")
        self.assertEqual(result["language"], 'zh-hans')
        self.assertEqual(liuyao.analysis_status, 'completed')
    
    @patch('ai.utils.liuyao_analysis.prepare_liuyao_prompt')
    def test_analyze_liuyao_no_prompt(self, mock_prepare_prompt):
        """Test analysis failure when prompt preparation fails."""
        mock_prepare_prompt.return_value = None
        
        liuyao = MockLiuYao()
        result = analyze_liuyao(liuyao)
        
        self.assertIsNone(result)
        self.assertEqual(liuyao.analysis_status, 'error')
    
    @patch('ai.utils.liuyao_analysis.prepare_liuyao_prompt')
    @patch('ai.utils.liuyao_analysis.LLMServiceFactory')
    def test_analyze_liuyao_empty_response(self, mock_factory, mock_prepare_prompt):
        """Test handling of empty LLM response."""
        mock_prepare_prompt.return_value = "Test prompt"
        mock_service = MagicMock()
        mock_service.get_completion.return_value = ""
        mock_factory.get_service.return_value = mock_service
        
        liuyao = MockLiuYao()
        result = analyze_liuyao(liuyao)
        
        self.assertIsNone(result)
        self.assertEqual(liuyao.analysis_status, 'error')


class TestEdgeCases(TestCase):
    """Test edge cases and error handling."""
    
    def test_extreme_hexagram_numbers(self):
        """Test handling of extreme hexagram numbers."""
        # Very large number
        result = get_hexagram_name(999999)
        self.assertIn("Unknown hexagram", result)
        
        # Negative number
        result = get_hexagram_name(-999)
        self.assertIn("Unknown hexagram", result)
    
    def test_malformed_trigram_codes(self):
        """Test handling of malformed trigram codes."""
        test_cases = ["1234", "abc", "12", ""]
        for case in test_cases:
            result = get_trigram_name(case)
            self.assertIn("Unknown trigram", result)
    
    def test_liuyao_with_missing_attributes(self):
        """Test LiuYao objects with missing attributes."""
        class IncompleteLiuYao:
            def __init__(self):
                self.y1 = '1'
                self.y2 = '0'
                # Missing y3-y6
        
        liuyao = IncompleteLiuYao()
        # Should not crash, but may return incomplete results
        try:
            result = identify_changing_lines(liuyao)
            # Should handle missing attributes gracefully
            self.assertIsInstance(result, list)
        except AttributeError:
            # This is acceptable for incomplete objects
            pass
    
    def test_data_formatter_with_complex_objects(self):
        """Test DataFormatter with complex nested objects."""
        complex_data = {
            "level1": {
                "level2": ["item1", "item2"],
                "level2b": None
            },
            "list_of_dicts": [
                {"a": 1, "b": 2},
                {"c": 3, "d": 4}
            ]
        }
        
        result = DataFormatter.format_data(complex_data, "complex")
        self.assertIsInstance(result, str)
        self.assertIn("level1", result)


class TestOutlierCases(TestCase):
    """Test outlier and unusual cases."""
    
    def test_unicode_question_text(self):
        """Test handling of Unicode and special characters."""
        unicode_questions = [
            "测试中文问题？",
            "Test with émojis 🔮✨",
            "Mixed 中英文 question",
            "Special chars: @#$%^&*()",
            ""  # Empty question
        ]
        
        for question in unicode_questions:
            liuyao = MockLiuYao(question=question)
            # Should not crash with any Unicode content
            result = identify_changing_lines(liuyao)
            self.assertIsInstance(result, list)
    
    def test_all_possible_line_combinations(self):
        """Test all possible line value combinations."""
        line_values = ['0', '1', '111', '000', 'invalid']
        
        for y1 in line_values[:3]:  # Only test valid values to avoid too many combinations
            for y2 in line_values[:3]:
                liuyao = MockLiuYao(y1=y1, y2=y2, y3='111', y4='111', y5='111', y6='111')
                result = identify_changing_lines(liuyao)
                self.assertIsInstance(result, list)
    
    def test_response_with_array_format(self):
        """Test handling of array-format responses."""
        array_response = {
            "think": ["First thought", "Second thought", "Third thought"],
            "analysis": ["First paragraph", "Second paragraph"]
        }
        
        # This should be handled gracefully by the frontend
        json_response = json.dumps(array_response)
        result = ResponseValidator.validate_liuyao_response(json_response)
        self.assertIsNotNone(result)
    
    def test_extremely_long_responses(self):
        """Test handling of very long responses."""
        long_text = "Very long analysis " * 1000  # 20,000 characters
        long_response = f'{{"think": "Long thinking", "analysis": "{long_text}"}}'
        
        result = ResponseValidator.validate_liuyao_response(long_response)
        self.assertIsNotNone(result)
        self.assertEqual(len(result["liuyao_analysis"]), len(long_text))


class TestIntegrationScenarios(TestCase):
    """Test integration scenarios that combine multiple components."""
    
    @patch('ai.utils.liuyao_analysis.get_active_template')
    def test_prepare_prompt_with_complex_data(self, mock_get_active_template):
        """Test prompt preparation with complex LiuYao data."""
        mock_get_active_template.return_value = "Question: {{question}}\nHexagram: {{hexagram_info}}\nChanging lines: {{changing_lines}}"
        
        # Complex LiuYao data
        complex_data = {
            "ly": {
                "ogua": {
                    "gua": 0,  # 乾卦
                    "palace": {"palace": 0, "shi": 3, "yin": 6}
                },
                "cgua": {
                    "gua": 1  # 坤卦
                }
            },
            "bz": {
                "year": {"g": 0, "e": 0},
                "month": {"g": 1, "e": 1},
                "day": {"g": 2, "e": 2},
                "hour": {"g": 3, "e": 3},
                "empty": {"0": 4, "1": 5}
            },
            "god6": 0,
            "rel": {}
        }
        
        liuyao = MockLiuYao(
            question="复杂问题测试",
            y1='1', y2='0', y3='111',  # Some changing lines
            data=complex_data
        )
        
        with patch('iching.utils.liuyao.recalculate_liuyao_data') as mock_recalc:
            mock_recalc.return_value = {'data': complex_data}
            
            result = prepare_liuyao_prompt(liuyao)
            
            self.assertIsNotNone(result)
            self.assertIn("复杂问题测试", result)


class TestResponseValidationSamples(TestCase):
    """Test response validation with various sample responses."""
    
    def test_response_validation_samples(self):
        """Test response validation with various sample responses."""
        sample_responses = [
            # Normal JSON response
            '{"think": "Normal thinking", "analysis": "Normal analysis"}',
            
            # JSON with markdown
            '''```json
            {
                "think": "Markdown thinking",
                "analysis": "Markdown analysis"
            }
            ```''',
            
            # Malformed JSON
            '{"think": "Broken JSON", "analysis": "Missing quote}',
            
            # Plain text
            "This is just plain text analysis without any JSON structure",
            
            # Mixed format
            'Some text before {"think": "Mixed thinking", "analysis": "Mixed analysis"} and after',
            
            # Array format (problematic case we fixed)
            '{"think": ["Think 1", "Think 2"], "analysis": ["Para 1", "Para 2"]}',
            
            # Empty response
            "",
            
            # Very large response
            '{"think": "' + "Very long thinking " * 100 + '", "analysis": "' + "Very long analysis " * 200 + '"}',
        ]
        
        for response_text in sample_responses:
            with self.subTest(response=response_text[:50] + "..." if len(response_text) > 50 else response_text):
                result = ResponseValidator.validate_liuyao_response(response_text)
                
                # Should always return a valid dictionary
                self.assertIsInstance(result, dict)
                self.assertIn("liuyao_analysis", result)
                self.assertIn("think", result)
                
                # Should have some content (even if fallback to original text)
                self.assertTrue(
                    len(result["liuyao_analysis"]) > 0 or 
                    len(result["think"]) > 0 or 
                    len(response_text) == 0
                ) 