import json

import pytest

from autotrainer.training import TrainingProgress
from autotrainer.training.training_predicate import (
    TrainingPredicate, CompoundPredicate, NumberComparisonPredicate, PythonPredicate
)
from autotrainer.training.training_protocols import PredicateContext


class TestTrainingPredicate:
    """Test TrainingPredicate serialization and deserialization"""

    @pytest.fixture
    def sample_system_context(self):
        """Sample SystemContext for testing predicate evaluation"""
        return PredicateContext()

    @pytest.fixture
    def sample_training_phase(self):
        """Mock training phase for testing"""
        class MockTrainingPhase:
            def __init__(self):
                self.progress = TrainingProgress()
                self.progress.session_count = 5
                self.progress.pellets_consumed = 10
                self.progress.successful_reaches = 8
                self.starting_baseline_intensity = 15
                self.is_pellet_cover_enabled = True
                self.is_auto_clamp_enabled = False
        
        return MockTrainingPhase()

    def test_number_comparison_predicate_serialization(self):
        """Test NumberComparisonPredicate to_dict() includes all properties"""
        predicate = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GT,
            "progress.session_count",
            5
        )
        
        result = predicate.to_dict()
        
        assert result["type"] == "NumberComparisonPredicate"
        assert result["module"] == "autotrainer.training.training_predicate"
        assert result["properties"]["comparisonType"] == "GT"
        assert result["properties"]["path"] == ["progress", "session_count"]
        assert result["properties"]["value"] == 5

    def test_number_comparison_predicate_deserialization(self):
        """Test NumberComparisonPredicate from_dict() restores all properties"""
        data = {
            "type": "NumberComparisonPredicate",
            "module": "autotrainer.training.training_predicate",
            "properties": {
                "comparisonType": "LTE",
                "path": ["progress", "pellets_consumed"],
                "value": 20.5
            }
        }
        
        predicate = TrainingPredicate.from_dict(data)
        
        assert isinstance(predicate, NumberComparisonPredicate)
        assert predicate.comparison_type == NumberComparisonPredicate.NumberComparisonType.LTE
        assert predicate.path == ["progress", "pellets_consumed"]
        assert predicate.value == 20.5

    def test_number_comparison_predicate_round_trip(self):
        """Test NumberComparisonPredicate complete round-trip serialization"""
        original = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.EQ,
            "progress.successful_reaches",
            8
        )
        
        serialized = original.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, NumberComparisonPredicate)
        assert restored.comparison_type == original.comparison_type
        assert restored.path == original.path
        assert restored.value == original.value

    def test_number_comparison_all_types(self):
        """Test all NumberComparisonPredicate comparison types"""
        types_and_values = [
            (NumberComparisonPredicate.NumberComparisonType.LT, 10),
            (NumberComparisonPredicate.NumberComparisonType.LTE, 15),
            (NumberComparisonPredicate.NumberComparisonType.EQ, 20),
            (NumberComparisonPredicate.NumberComparisonType.GTE, 25),
            (NumberComparisonPredicate.NumberComparisonType.GT, 30)
        ]
        
        for comparison_type, value in types_and_values:
            predicate = NumberComparisonPredicate(
                comparison_type,
                "test.path",
                value
            )
            
            serialized = predicate.to_dict()
            restored = TrainingPredicate.from_dict(serialized)
            
            assert isinstance(restored, NumberComparisonPredicate)
            assert restored.comparison_type == comparison_type
            assert restored.value == value

    def test_python_predicate_serialization(self):
        """Test PythonPredicate to_dict() includes python code"""
        code = "phase.progress.session_count > 5 and context.algorithm is not None"
        predicate = PythonPredicate(code)
        
        result = predicate.to_dict()
        
        assert result["type"] == "PythonPredicate"
        assert result["module"] == "autotrainer.training.training_predicate"
        assert result["properties"]["pythonCode"] == code

    def test_python_predicate_deserialization(self):
        """Test PythonPredicate from_dict() restores python code"""
        code = "len(phase.progress.user_context) > 0"
        data = {
            "type": "PythonPredicate",
            "module": "autotrainer.training.training_predicate",
            "properties": {
                "pythonCode": code
            }
        }
        
        predicate = TrainingPredicate.from_dict(data)
        
        assert isinstance(predicate, PythonPredicate)
        assert predicate.python_code == code

    def test_python_predicate_complex_code(self):
        """Test PythonPredicate with complex code including special characters"""
        complex_code = '''
def check_conditions(phase, context):
    return (phase.progress.session_count >= 10 and
            phase.progress.pellets_consumed / phase.progress.session_count > 2.5 and
            "notes" in phase.progress.user_context)
        '''
        
        predicate = PythonPredicate(complex_code)
        serialized = predicate.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, PythonPredicate)
        assert restored.python_code == complex_code

    def test_compound_predicate_and_serialization(self):
        """Test CompoundPredicate AND type serialization"""
        child1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GT,
            "progress.session_count",
            3
        )
        child2 = PythonPredicate("phase.is_pellet_cover_enabled")
        
        compound = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [child1, child2]
        )
        
        result = compound.to_dict()
        
        assert result["type"] == "CompoundPredicate"
        assert result["module"] == "autotrainer.training.training_predicate"
        assert result["properties"]["compoundType"] == "AND"
        assert len(result["properties"]["predicates"]) == 2
        assert result["properties"]["predicates"][0]["type"] == "NumberComparisonPredicate"
        assert result["properties"]["predicates"][1]["type"] == "PythonPredicate"

    def test_compound_predicate_or_deserialization(self):
        """Test CompoundPredicate OR type deserialization"""
        data = {
            "type": "CompoundPredicate",
            "module": "autotrainer.training.training_predicate",
            "properties": {
                "compoundType": "OR",
                "predicates": [
                    {
                        "type": "NumberComparisonPredicate",
                        "module": "autotrainer.training.training_predicate",
                        "properties": {
                            "comparisonType": "LT",
                            "path": ["progress", "phase_attempts"],
                            "value": 5
                        }
                    },
                    {
                        "type": "PythonPredicate",
                        "module": "autotrainer.training.training_predicate",
                        "properties": {
                            "pythonCode": "phase.starting_baseline_intensity < 20"
                        }
                    }
                ]
            }
        }
        
        predicate = TrainingPredicate.from_dict(data)
        
        assert isinstance(predicate, CompoundPredicate)
        assert predicate.compound_type == CompoundPredicate.CompoundType.OR
        assert len(predicate.predicates) == 2
        assert isinstance(predicate.predicates[0], NumberComparisonPredicate)
        assert isinstance(predicate.predicates[1], PythonPredicate)

    def test_compound_predicate_round_trip(self):
        """Test CompoundPredicate complete round-trip serialization"""
        child1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GTE,
            "progress.successful_reaches",
            10
        )
        child2 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.LTE,
            "progress.pellets_consumed",
            50
        )
        child3 = PythonPredicate("phase.is_auto_clamp_enabled == False")
        
        original = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [child1, child2, child3]
        )
        
        serialized = original.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == original.compound_type
        assert len(restored.predicates) == len(original.predicates)
        
        for original_child, restored_child in zip(original.predicates, restored.predicates):
            assert type(restored_child) == type(original_child)
            if isinstance(original_child, NumberComparisonPredicate):
                assert restored_child.comparison_type == original_child.comparison_type
                assert restored_child.path == original_child.path
                assert restored_child.value == original_child.value
            elif isinstance(original_child, PythonPredicate):
                assert restored_child.python_code == original_child.python_code

    def test_nested_compound_predicates(self):
        """Test CompoundPredicate containing another CompoundPredicate"""
        inner_child1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GT,
            "progress.session_count",
            5
        )
        inner_child2 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.LT,
            "progress.pellets_consumed",
            100
        )
        
        inner_compound = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [inner_child1, inner_child2]
        )
        
        outer_child = PythonPredicate("phase.starting_baseline_intensity > 10")
        
        outer_compound = CompoundPredicate(
            CompoundPredicate.CompoundType.OR,
            [inner_compound, outer_child]
        )
        
        serialized = outer_compound.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == CompoundPredicate.CompoundType.OR
        assert len(restored.predicates) == 2
        
        restored_inner = restored.predicates[0]
        assert isinstance(restored_inner, CompoundPredicate)
        assert restored_inner.compound_type == CompoundPredicate.CompoundType.AND
        assert len(restored_inner.predicates) == 2
        
        restored_outer_child = restored.predicates[1]
        assert isinstance(restored_outer_child, PythonPredicate)
        assert restored_outer_child.python_code == outer_child.python_code

    def test_deeply_nested_compound_predicates(self):
        """Test multiple levels of CompoundPredicate nesting"""
        level3_pred1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.EQ,
            "progress.phase_attempts",
            1
        )
        level3_pred2 = PythonPredicate("True")
        
        level2_compound = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [level3_pred1, level3_pred2]
        )
        
        level1_pred = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GT,
            "progress.successful_reaches",
            0
        )
        
        level1_compound = CompoundPredicate(
            CompoundPredicate.CompoundType.OR,
            [level2_compound, level1_pred]
        )
        
        root_pred = PythonPredicate("phase.is_pellet_cover_enabled")
        
        root_compound = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [level1_compound, root_pred]
        )
        
        serialized = root_compound.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == CompoundPredicate.CompoundType.AND
        
        restored_level1 = restored.predicates[0]
        assert isinstance(restored_level1, CompoundPredicate)
        assert restored_level1.compound_type == CompoundPredicate.CompoundType.OR
        
        restored_level2 = restored_level1.predicates[0]
        assert isinstance(restored_level2, CompoundPredicate)
        assert restored_level2.compound_type == CompoundPredicate.CompoundType.AND
        assert len(restored_level2.predicates) == 2

    def test_edge_case_empty_compound_predicate(self):
        """Test CompoundPredicate with empty predicates list"""
        compound = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            []
        )
        
        serialized = compound.to_dict()
        restored = TrainingPredicate.from_dict(serialized)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == CompoundPredicate.CompoundType.AND
        assert len(restored.predicates) == 0

    def test_edge_case_number_predicate_with_zero(self):
        """Test NumberComparisonPredicate with zero and negative values"""
        test_values = [0, -1, -100.5, 0.0, 1e-10, 1e10]
        
        for value in test_values:
            predicate = NumberComparisonPredicate(
                NumberComparisonPredicate.NumberComparisonType.EQ,
                "progress.test_value",
                value
            )
            
            serialized = predicate.to_dict()
            restored = TrainingPredicate.from_dict(serialized)
            
            assert isinstance(restored, NumberComparisonPredicate)
            assert restored.value == value

    def test_edge_case_python_predicate_special_characters(self):
        """Test PythonPredicate with special characters and edge cases"""
        special_codes = [
            "",  # Empty string
            "True",  # Simple boolean
            'phase.progress.user_context.get("key with spaces", 0) > 5',  # Quotes and spaces
            "phase.progress.session_count\n> 10",  # Newlines
            "# Comment\nphase.is_pellet_cover_enabled",  # Comments
            "lambda x: x > 5",  # Lambda
        ]
        
        for code in special_codes:
            predicate = PythonPredicate(code)
            serialized = predicate.to_dict()
            restored = TrainingPredicate.from_dict(serialized)
            
            assert isinstance(restored, PythonPredicate)
            assert restored.python_code == code

    def test_integration_complex_predicate_tree(self):
        """Test complex predicate tree with all types and deep nesting"""
        numerical_check1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GTE,
            "progress.session_count",
            10
        )
        
        numerical_check2 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.LT,
            "progress.pellets_consumed",
            100
        )
        
        basic_conditions = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [numerical_check1, numerical_check2]
        )
        
        advanced_check = PythonPredicate(
            "phase.progress.successful_reaches / max(phase.progress.session_count, 1) > 0.8"
        )
        
        config_check1 = NumberComparisonPredicate(
            NumberComparisonPredicate.NumberComparisonType.GT,
            "starting_baseline_intensity",
            15
        )
        
        config_check2 = PythonPredicate("phase.is_auto_clamp_enabled")
        
        config_conditions = CompoundPredicate(
            CompoundPredicate.CompoundType.OR,
            [config_check1, config_check2]
        )
        
        performance_gate = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [basic_conditions, advanced_check]
        )
        
        final_predicate = CompoundPredicate(
            CompoundPredicate.CompoundType.OR,
            [performance_gate, config_conditions]
        )
        
        serialized = final_predicate.to_dict()
        
        json_string = json.dumps(serialized, indent=2)
        parsed_data = json.loads(json_string)
        
        restored = TrainingPredicate.from_dict(parsed_data)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == CompoundPredicate.CompoundType.OR
        assert len(restored.predicates) == 2
        
        restored_performance = restored.predicates[0]
        assert isinstance(restored_performance, CompoundPredicate)
        assert restored_performance.compound_type == CompoundPredicate.CompoundType.AND
        
        restored_basic = restored_performance.predicates[0]
        assert isinstance(restored_basic, CompoundPredicate)
        assert len(restored_basic.predicates) == 2
        assert all(isinstance(p, NumberComparisonPredicate) for p in restored_basic.predicates)
        
        restored_advanced = restored_performance.predicates[1]
        assert isinstance(restored_advanced, PythonPredicate)
        
        restored_config = restored.predicates[1]
        assert isinstance(restored_config, CompoundPredicate)
        assert restored_config.compound_type == CompoundPredicate.CompoundType.OR

    def test_integration_json_serialization(self):
        """Test that predicates can be serialized to and from JSON strings"""
        predicate = CompoundPredicate(
            CompoundPredicate.CompoundType.AND,
            [
                NumberComparisonPredicate(
                    NumberComparisonPredicate.NumberComparisonType.GT,
                    "progress.session_count",
                    5
                ),
                PythonPredicate("phase.is_pellet_cover_enabled")
            ]
        )
        
        dict_data = predicate.to_dict()
        json_string = json.dumps(dict_data)
        
        parsed_data = json.loads(json_string)
        restored = TrainingPredicate.from_dict(parsed_data)
        
        assert isinstance(restored, CompoundPredicate)
        assert restored.compound_type == predicate.compound_type
        assert len(restored.predicates) == len(predicate.predicates)

    def test_protocol_compliance(self):
        """Test that all predicate types comply with TrainingPredicate protocol"""
        predicates = [
            NumberComparisonPredicate(
                NumberComparisonPredicate.NumberComparisonType.EQ,
                "progress.session_count",
                10
            ),
            PythonPredicate("True"),
            CompoundPredicate(
                CompoundPredicate.CompoundType.OR,
                [
                    NumberComparisonPredicate(
                        NumberComparisonPredicate.NumberComparisonType.GT,
                        "progress.pellets_consumed",
                        0
                    )
                ]
            )
        ]
        
        for predicate in predicates:
            assert hasattr(predicate, "to_dict")
            assert hasattr(predicate, "evaluate")
            
            dict_result = predicate.to_dict()
            assert "type" in dict_result
            assert "module" in dict_result
            
            restored = TrainingPredicate.from_dict(dict_result)
            assert type(restored) == type(predicate)