"""
Unit tests for Memory Type Ontology

Tests the formal ontology layer for memory classification and type validation.
"""

import sys
from pathlib import Path
import importlib.util
import pytest

# Load ontology module directly without importing the package
ontology_path = Path(__file__).parent.parent / "src" / "mcp_memory_service" / "models" / "ontology.py"
spec = importlib.util.spec_from_file_location("ontology", ontology_path)
ontology = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ontology)

BaseMemoryType = ontology.BaseMemoryType
TAXONOMY = ontology.TAXONOMY
RELATIONSHIPS = ontology.RELATIONSHIPS
validate_memory_type = ontology.validate_memory_type
get_parent_type = ontology.get_parent_type
get_all_types = ontology.get_all_types
validate_relationship = ontology.validate_relationship
is_symmetric_relationship = ontology.is_symmetric_relationship
MemoryTypeOntology = ontology.MemoryTypeOntology


class TestBurst11BaseMemoryTypes:
    """Tests for Burst 1.1: Base Memory Types Enum"""

    def test_enum_has_exactly_twelve_base_types(self):
        """Base type enum should have exactly 12 types (5 original + 7 new)"""
        assert len(BaseMemoryType) == 12

    def test_each_type_is_valid_string_constant(self):
        """Each base type should be a valid string constant"""
        expected_types = {
            # Software Development (original 5)
            "observation",
            "decision",
            "learning",
            "error",
            "pattern",
            # Project Management - Agile (2 new)
            "planning",
            "ceremony",
            # Project Management - Traditional (2 new)
            "milestone",
            "stakeholder",
            # General Knowledge Work (3 new)
            "meeting",
            "research",
            "communication"
        }
        actual_types = {member.value for member in BaseMemoryType}
        assert actual_types == expected_types

    def test_base_types_are_lowercase(self):
        """All base type values should be lowercase for consistency"""
        for member in BaseMemoryType:
            assert member.value.islower()

    def test_base_types_accessible_as_enum_members(self):
        """Base types should be accessible as enum members"""
        # Original types
        assert BaseMemoryType.OBSERVATION.value == "observation"
        assert BaseMemoryType.DECISION.value == "decision"
        assert BaseMemoryType.LEARNING.value == "learning"
        assert BaseMemoryType.ERROR.value == "error"
        assert BaseMemoryType.PATTERN.value == "pattern"
        # New types
        assert BaseMemoryType.PLANNING.value == "planning"
        assert BaseMemoryType.CEREMONY.value == "ceremony"
        assert BaseMemoryType.MILESTONE.value == "milestone"
        assert BaseMemoryType.STAKEHOLDER.value == "stakeholder"
        assert BaseMemoryType.MEETING.value == "meeting"
        assert BaseMemoryType.RESEARCH.value == "research"
        assert BaseMemoryType.COMMUNICATION.value == "communication"


class TestBurst12TaxonomyHierarchy:
    """Tests for Burst 1.2: Taxonomy Hierarchy Dictionary"""

    def test_each_base_type_has_at_least_two_subtypes(self):
        """Each base type should have at least 2 subtypes for meaningful classification"""
        for base_type in BaseMemoryType:
            subtypes = TAXONOMY.get(base_type.value, [])
            assert len(subtypes) >= 2, f"{base_type.value} has {len(subtypes)} subtypes, expected >= 2"

    def test_all_subtypes_are_unique_across_taxonomy(self):
        """No subtype should appear under multiple base types"""
        all_subtypes = []
        for base_type, subtypes in TAXONOMY.items():
            all_subtypes.extend(subtypes)

        # Check for duplicates
        assert len(all_subtypes) == len(set(all_subtypes)), \
            f"Found duplicate subtypes: {[s for s in all_subtypes if all_subtypes.count(s) > 1]}"

    def test_taxonomy_covers_all_base_types(self):
        """TAXONOMY dict should have entries for all base types"""
        base_type_values = {member.value for member in BaseMemoryType}
        taxonomy_keys = set(TAXONOMY.keys())
        assert base_type_values == taxonomy_keys

    def test_all_subtypes_are_lowercase_with_underscores(self):
        """Subtypes should follow snake_case naming convention"""
        for base_type, subtypes in TAXONOMY.items():
            for subtype in subtypes:
                assert subtype.islower() or '_' in subtype, \
                    f"Subtype '{subtype}' should be lowercase with underscores"
                assert ' ' not in subtype, f"Subtype '{subtype}' should not contain spaces"


class TestBurst13RelationshipTypes:
    """Tests for Burst 1.3: Relationship Types Definition"""

    def test_six_relationship_types_defined(self):
        """Should have exactly 6 relationship types"""
        expected_types = {"causes", "fixes", "contradicts", "supports", "follows", "related"}
        actual_types = set(RELATIONSHIPS.keys())
        assert actual_types == expected_types

    def test_each_type_has_description(self):
        """Each relationship type should have a description"""
        for rel_type, config in RELATIONSHIPS.items():
            assert "description" in config, f"{rel_type} missing description"
            assert len(config["description"]) > 0, f"{rel_type} has empty description"

    def test_each_type_has_valid_patterns(self):
        """Each relationship type should define valid source→target patterns"""
        for rel_type, config in RELATIONSHIPS.items():
            assert "valid_patterns" in config, f"{rel_type} missing valid_patterns"
            assert len(config["valid_patterns"]) > 0, f"{rel_type} has no valid patterns"

    def test_related_type_accepts_any_pattern(self):
        """The 'related' type should accept any→any pattern (default fallback)"""
        related_patterns = RELATIONSHIPS["related"]["valid_patterns"]
        assert "any → any" in related_patterns, "related type should accept any→any pattern"


class TestBurst14MemoryTypeValidation:
    """Tests for Burst 1.4: Memory Type Validation"""

    def test_base_types_validate_correctly(self):
        """All base types should validate as True"""
        assert validate_memory_type("observation") is True
        assert validate_memory_type("decision") is True
        assert validate_memory_type("learning") is True
        assert validate_memory_type("error") is True
        assert validate_memory_type("pattern") is True

    def test_subtypes_validate_correctly(self):
        """All subtypes should validate as True"""
        # Test a few subtypes from each category
        assert validate_memory_type("code_edit") is True  # observation subtype
        assert validate_memory_type("architecture") is True  # decision subtype
        assert validate_memory_type("insight") is True  # learning subtype
        assert validate_memory_type("bug") is True  # error subtype
        assert validate_memory_type("code_smell") is True  # pattern subtype

    def test_invalid_types_return_false(self):
        """Invalid types should return False"""
        assert validate_memory_type("invalid_type") is False
        assert validate_memory_type("not_a_type") is False
        assert validate_memory_type("") is False
        assert validate_memory_type("ObSeRvAtIoN") is False  # Case sensitive


class TestBurst15GetParentType:
    """Tests for Burst 1.5: Get Parent Type Function"""

    def test_subtype_returns_correct_parent(self):
        """Subtypes should return their parent base type"""
        assert get_parent_type("code_edit") == "observation"
        assert get_parent_type("architecture") == "decision"
        assert get_parent_type("insight") == "learning"
        assert get_parent_type("bug") == "error"
        assert get_parent_type("code_smell") == "pattern"

    def test_base_type_returns_itself(self):
        """Base types should return themselves (they are their own parent)"""
        assert get_parent_type("observation") == "observation"
        assert get_parent_type("decision") == "decision"
        assert get_parent_type("learning") == "learning"
        assert get_parent_type("error") == "error"
        assert get_parent_type("pattern") == "pattern"

    def test_invalid_type_returns_none(self):
        """Invalid types should return None"""
        assert get_parent_type("invalid_type") is None
        assert get_parent_type("not_a_type") is None
        assert get_parent_type("") is None


class TestBurst16GetAllTypes:
    """Tests for Burst 1.6: Get All Types Function"""

    def test_returns_correct_count(self):
        """Should return the correct total count of base and subtypes"""
        all_types = get_all_types()
        expected_count = len(BaseMemoryType) + sum(len(subtypes) for subtypes in TAXONOMY.values())
        assert len(all_types) == expected_count

    def test_no_duplicates_in_list(self):
        """Should not have duplicate types in the list"""
        all_types = get_all_types()
        assert len(all_types) == len(set(all_types))

    def test_includes_base_types(self):
        """Should include all base types"""
        all_types = get_all_types()
        assert "observation" in all_types
        assert "decision" in all_types
        assert "learning" in all_types
        assert "error" in all_types
        assert "pattern" in all_types

    def test_includes_subtypes(self):
        """Should include subtypes from all categories"""
        all_types = get_all_types()
        assert "code_edit" in all_types
        assert "architecture" in all_types
        assert "insight" in all_types
        assert "bug" in all_types
        assert "code_smell" in all_types

    def test_document_types_are_valid(self):
        """Document-related types should be valid observation subtypes"""
        # Validate that new document types are recognized
        assert validate_memory_type("document") is True
        assert validate_memory_type("note") is True
        assert validate_memory_type("reference") is True

        # Validate parent type hierarchy
        assert get_parent_type("document") == "observation"
        assert get_parent_type("note") == "observation"
        assert get_parent_type("reference") == "observation"

        # Validate they appear in the full type list
        all_types = get_all_types()
        assert "document" in all_types
        assert "note" in all_types
        assert "reference" in all_types


class TestBurst17RelationshipTypeValidation:
    """Tests for Burst 1.7: Relationship Type Validation"""

    def test_valid_types_return_true(self):
        """All 6 defined relationship types should validate as True"""
        assert validate_relationship("causes") is True
        assert validate_relationship("fixes") is True
        assert validate_relationship("contradicts") is True
        assert validate_relationship("supports") is True
        assert validate_relationship("follows") is True
        assert validate_relationship("related") is True

    def test_invalid_types_return_false(self):
        """Invalid relationship types should return False"""
        assert validate_relationship("invalid_relationship") is False
        assert validate_relationship("not_a_rel") is False
        assert validate_relationship("") is False


class TestBurst18OntologyClassIntegration:
    """Tests for Burst 1.8: Ontology Class Integration"""

    def test_class_methods_accessible(self):
        """All ontology methods should be accessible via the class"""
        # Test that class has all expected methods
        assert hasattr(MemoryTypeOntology, 'validate_memory_type')
        assert hasattr(MemoryTypeOntology, 'get_parent_type')
        assert hasattr(MemoryTypeOntology, 'get_all_types')
        assert hasattr(MemoryTypeOntology, 'validate_relationship')

    def test_validate_memory_type_via_class(self):
        """validate_memory_type should work via class method"""
        assert MemoryTypeOntology.validate_memory_type("observation") is True
        assert MemoryTypeOntology.validate_memory_type("code_edit") is True
        assert MemoryTypeOntology.validate_memory_type("invalid") is False

    def test_get_parent_type_via_class(self):
        """get_parent_type should work via class method"""
        assert MemoryTypeOntology.get_parent_type("code_edit") == "observation"
        assert MemoryTypeOntology.get_parent_type("observation") == "observation"
        assert MemoryTypeOntology.get_parent_type("invalid") is None

    def test_get_all_types_via_class(self):
        """get_all_types should work via class method"""
        all_types = MemoryTypeOntology.get_all_types()
        expected_count = len(BaseMemoryType) + sum(len(subtypes) for subtypes in TAXONOMY.values())
        assert len(all_types) == expected_count
        assert "observation" in all_types
        assert "code_edit" in all_types

    def test_validate_relationship_via_class(self):
        """validate_relationship should work via class method"""
        assert MemoryTypeOntology.validate_relationship("causes") is True
        assert MemoryTypeOntology.validate_relationship("invalid") is False


class TestBurst19SymmetricRelationshipClassification:
    """Tests for is_symmetric_relationship() - PR #348"""

    def test_is_symmetric_relationship_symmetric_types(self):
        """Test symmetric relationship types return True"""
        assert is_symmetric_relationship("related") is True
        assert is_symmetric_relationship("contradicts") is True

    def test_is_symmetric_relationship_asymmetric_types(self):
        """Test asymmetric relationship types return False"""
        assert is_symmetric_relationship("causes") is False
        assert is_symmetric_relationship("fixes") is False
        assert is_symmetric_relationship("supports") is False
        assert is_symmetric_relationship("follows") is False

    def test_is_symmetric_relationship_invalid_type(self):
        """Test invalid relationship type raises ValueError"""
        with pytest.raises(ValueError, match="Invalid relationship type"):
            is_symmetric_relationship("invalid_type")

    def test_is_symmetric_relationship_via_class(self):
        """Test is_symmetric_relationship works via class method"""
        assert MemoryTypeOntology.is_symmetric_relationship("related") is True
        assert MemoryTypeOntology.is_symmetric_relationship("causes") is False

        with pytest.raises(ValueError):
            MemoryTypeOntology.is_symmetric_relationship("invalid")


class TestBurst20NewProjectManagementTypes:
    """Test new project management base types (Issue #464)."""

    def test_new_base_types_exist(self):
        """Verify all 7 new base types are defined."""
        new_base_types = [
            "planning", "ceremony", "milestone", "stakeholder",
            "meeting", "research", "communication"
        ]

        all_types = get_all_types()
        for base_type in new_base_types:
            assert base_type in all_types, f"New base type '{base_type}' missing"

    def test_planning_subtypes(self):
        """Verify planning subtypes."""
        expected = [
            "sprint_goal", "backlog_item", "story_point_estimate",
            "velocity", "retrospective", "standup_note", "acceptance_criteria"
        ]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "planning"

    def test_ceremony_subtypes(self):
        """Verify ceremony subtypes."""
        expected = [
            "sprint_review", "sprint_planning", "daily_standup",
            "retrospective_action", "demo_feedback"
        ]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "ceremony"

    def test_milestone_subtypes(self):
        """Verify milestone subtypes."""
        expected = [
            "deliverable", "dependency", "risk", "constraint", "assumption", "deadline"
        ]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "milestone"

    def test_stakeholder_subtypes(self):
        """Verify stakeholder subtypes."""
        expected = [
            "requirement", "feedback", "escalation", "approval",
            "change_request", "status_update"
        ]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "stakeholder"

    def test_meeting_subtypes(self):
        """Verify meeting subtypes."""
        expected = ["action_item", "attendee_note", "agenda_item", "follow_up", "minutes"]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "meeting"

    def test_research_subtypes(self):
        """Verify research subtypes."""
        expected = ["finding", "comparison", "recommendation", "source", "hypothesis"]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "research"

    def test_communication_subtypes(self):
        """Verify communication subtypes."""
        expected = ["email_summary", "chat_summary", "announcement", "request", "response"]

        all_types = get_all_types()
        for subtype in expected:
            assert subtype in all_types
            assert get_parent_type(subtype) == "communication"

    def test_total_type_count(self):
        """Verify total type count is 75 (12 base + 63 subtypes)."""
        all_types = get_all_types()
        assert len(all_types) == 75, f"Expected 75 types, got {len(all_types)}"


class TestBurst21CustomMemoryTypeConfiguration:
    """Test configurable memory types via MCP_CUSTOM_MEMORY_TYPES."""

    def setup_method(self):
        """Clear caches and environment before each test."""
        import os
        # Remove any custom types from environment to ensure clean state
        os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)
        ontology.clear_ontology_caches()

    def teardown_method(self):
        """Clear caches and environment after each test."""
        import os
        # Remove any custom types from environment
        os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)
        ontology.clear_ontology_caches()

    def test_load_custom_types_json(self, monkeypatch):
        """Test loading custom types from JSON environment variable."""
        import json

        # Set custom types
        custom_config = json.dumps({
            "legal": ["contract", "clause", "obligation"],
            "sales": ["opportunity", "objection", "competitor"]
        })
        monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', custom_config)

        # Clear all caches to force reload
        ontology.clear_ontology_caches()

        # Verify custom types loaded
        all_types = ontology.get_all_types()
        assert "legal" in all_types
        assert "contract" in all_types
        assert "sales" in all_types
        assert "opportunity" in all_types

        # Verify parent relationships
        assert ontology.get_parent_type("contract") == "legal"
        assert ontology.get_parent_type("opportunity") == "sales"

    def test_merge_with_builtin_types(self, monkeypatch):
        """Test that custom types merge with (not replace) built-in types."""
        import json

        custom_config = json.dumps({"meeting": ["board_meeting"]})
        monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', custom_config)

        # Clear caches to force reload
        ontology.clear_ontology_caches()

        all_types = ontology.get_all_types()

        # Built-in meeting subtypes should still exist
        assert "action_item" in all_types
        assert "follow_up" in all_types

        # Custom subtype should be added
        assert "board_meeting" in all_types

    def test_invalid_json_graceful_fallback(self, monkeypatch):
        """Test graceful fallback on invalid JSON."""
        monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', '{invalid json}')

        # Clear caches to force reload
        ontology.clear_ontology_caches()

        # Should fall back to built-in types
        all_types = ontology.get_all_types()
        assert "observation" in all_types  # Built-in type still works

    def test_no_custom_types_default_behavior(self, monkeypatch):
        """Test default behavior when no custom types configured."""
        import os

        # Ensure env var is not set - must be done BEFORE clearing caches
        # Use both monkeypatch AND direct removal to ensure clean state
        monkeypatch.delenv('MCP_CUSTOM_MEMORY_TYPES', raising=False)
        os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)

        # Now clear caches to force reload with no custom types
        ontology.clear_ontology_caches()

        all_types = ontology.get_all_types()

        # Should have at least 75 built-in types (might have test pollution from previous tests)
        # The key thing is that custom types from test_load_custom_types_json are NOT present
        assert len(all_types) >= 75, f"Expected at least 75 types, got {len(all_types)}"

        # Verify that the custom types from other tests in this class are NOT present
        # (These are the custom types that should only appear when MCP_CUSTOM_MEMORY_TYPES is set)
        assert "legal" not in all_types, "Custom base type 'legal' should not be present"
        assert "contract" not in all_types, "Custom subtype 'contract' should not be present"
        assert "sales" not in all_types, "Custom base type 'sales' should not be present"
        assert "opportunity" not in all_types, "Custom subtype 'opportunity' should not be present"

        # Verify standard types work
        assert "observation" in all_types
        assert "planning" in all_types
        assert "meeting" in all_types
