"""
Knowledge Graph Schema Tests
============================

BLACK BOX + WHITE BOX testing for Story KG-001.

Test Coverage:
- Entity creation and retrieval
- Relationship creation and traversal
- Soft delete functionality
- Versioning
- Index performance
- Data integrity constraints
- PostgreSQL compliance (no SQLite)

RULE 2: Full testing protocol for atomic story completion.
"""

import sys
import unittest
from datetime import datetime
from uuid import uuid4

sys.path.append('/mnt/e/genesis-system')
sys.path.append('/mnt/e/genesis-system/data/genesis-memory')

import psycopg2
from elestio_config import PostgresConfig

from core.knowledge.schema import (
    # Models
    Item, Skill, Knowledge, Learning, Axiom, Relationship,
    # Enums
    EntityType, RelationshipType, AxiomStatus,
    # Functions
    get_engine, get_session, create_all_tables
)


# ============================================================================
# BLACK BOX TESTS
# ============================================================================

class TestKnowledgeGraphBlackBox(unittest.TestCase):
    """
    Black box tests: Test from outside without implementation knowledge.
    Focus on inputs, outputs, and behavior.
    """

    @classmethod
    def setUpClass(cls):
        """Set up test database"""
        cls.engine = get_engine()
        create_all_tables()

    def setUp(self):
        """Create fresh session for each test"""
        self.session = get_session()

    def tearDown(self):
        """Clean up session"""
        self.session.rollback()
        self.session.close()

    # ------------------------------------------------------------------------
    # Input Validation Tests
    # ------------------------------------------------------------------------

    def test_create_item_with_valid_data(self):
        """Should create Item with all valid required fields"""
        item = Item(
            name="Test Domain",
            description="A test domain entity",
            item_type="domain",
            status="active"
        )
        self.session.add(item)
        self.session.commit()

        self.assertIsNotNone(item.id)
        self.assertEqual(item.name, "Test Domain")
        self.assertEqual(item.entity_type, EntityType.ITEM.value)
        self.assertFalse(item.is_deleted)

    def test_create_skill_with_usage_tracking(self):
        """Should create Skill with usage tracking initialized"""
        skill = Skill(
            name="GHL API",
            description="GoHighLevel API integration",
            skill_type="api",
            capability="crm_management"
        )
        self.session.add(skill)
        self.session.commit()

        self.assertEqual(skill.usage_count, 0)
        self.assertEqual(skill.success_count, 0)
        self.assertEqual(skill.failure_count, 0)

    def test_create_axiom_with_validation_status(self):
        """Should create Axiom with default proposed status"""
        axiom = Axiom(
            name="Rate Limit Rule",
            statement="Gemini API allows 1500 RPM",
            axiom_type="rate_limit"
        )
        self.session.add(axiom)
        self.session.commit()

        self.assertEqual(axiom.status, AxiomStatus.PROPOSED.value)
        self.assertEqual(axiom.applied_count, 0)
        self.assertEqual(axiom.violation_count, 0)

    # ------------------------------------------------------------------------
    # Output Verification Tests
    # ------------------------------------------------------------------------

    def test_retrieve_created_entity(self):
        """Should retrieve entity after creation"""
        knowledge = Knowledge(
            name="Telnyx Documentation",
            knowledge_type="api_docs",
            source="https://telnyx.com/docs"
        )
        self.session.add(knowledge)
        self.session.commit()

        retrieved = self.session.query(Knowledge).filter_by(id=knowledge.id).first()

        self.assertIsNotNone(retrieved)
        self.assertEqual(retrieved.name, "Telnyx Documentation")

    def test_query_entities_by_type(self):
        """Should filter entities by type"""
        # Create multiple items
        for i in range(3):
            item = Item(name=f"Item {i}", item_type="test")
            self.session.add(item)

        self.session.commit()

        items = self.session.query(Item).filter_by(item_type="test").all()

        self.assertGreaterEqual(len(items), 3)

    # ------------------------------------------------------------------------
    # Boundary Condition Tests
    # ------------------------------------------------------------------------

    def test_long_name_field(self):
        """Should handle name up to 500 characters"""
        long_name = "A" * 500
        item = Item(name=long_name)
        self.session.add(item)
        self.session.commit()

        retrieved = self.session.query(Item).filter_by(id=item.id).first()
        self.assertEqual(len(retrieved.name), 500)

    def test_null_optional_fields(self):
        """Should allow null in optional fields"""
        item = Item(
            name="Minimal Item",
            # All other fields optional
        )
        self.session.add(item)
        self.session.commit()

        self.assertIsNone(item.description)
        self.assertIsNone(item.location)

    # ------------------------------------------------------------------------
    # Error Handling Tests
    # ------------------------------------------------------------------------

    def test_missing_required_name_field(self):
        """Should raise error when name is missing"""
        with self.assertRaises(Exception):
            item = Item()  # Missing required name
            self.session.add(item)
            self.session.commit()

    def test_missing_required_statement_in_axiom(self):
        """Should raise error when axiom statement is missing"""
        with self.assertRaises(Exception):
            axiom = Axiom(name="Test Axiom")  # Missing required statement
            self.session.add(axiom)
            self.session.commit()

    # ------------------------------------------------------------------------
    # Integration Point Tests
    # ------------------------------------------------------------------------

    def test_create_relationship_between_entities(self):
        """Should create relationship linking two entities"""
        # Create source and target
        source = Item(name="Source Item")
        target = Skill(name="Target Skill")
        self.session.add_all([source, target])
        self.session.commit()

        # Create relationship
        rel = Relationship(
            source_id=source.id,
            source_type=EntityType.ITEM.value,
            target_id=target.id,
            target_type=EntityType.SKILL.value,
            relationship_type=RelationshipType.USES.value
        )
        self.session.add(rel)
        self.session.commit()

        self.assertIsNotNone(rel.id)
        self.assertEqual(rel.source_id, source.id)
        self.assertEqual(rel.target_id, target.id)

    def test_query_outgoing_relationships(self):
        """Should retrieve all outgoing relationships for entity"""
        # Create entities
        source = Item(name="Hub")
        targets = [Skill(name=f"Skill {i}") for i in range(3)]
        self.session.add_all([source] + targets)
        self.session.commit()

        # Create relationships
        for target in targets:
            rel = Relationship(
                source_id=source.id,
                source_type=EntityType.ITEM.value,
                target_id=target.id,
                target_type=EntityType.SKILL.value,
                relationship_type=RelationshipType.USES.value
            )
            self.session.add(rel)

        self.session.commit()

        # Query relationships
        relationships = self.session.query(Relationship).filter_by(
            source_id=source.id
        ).all()

        self.assertEqual(len(relationships), 3)


# ============================================================================
# WHITE BOX TESTS
# ============================================================================

class TestKnowledgeGraphWhiteBox(unittest.TestCase):
    """
    White box tests: Test with implementation knowledge.
    Focus on code paths, branches, internal state.
    """

    @classmethod
    def setUpClass(cls):
        """Set up test database"""
        cls.engine = get_engine()
        create_all_tables()

    def setUp(self):
        """Create fresh session for each test"""
        self.session = get_session()

    def tearDown(self):
        """Clean up session"""
        self.session.rollback()
        self.session.close()

    # ------------------------------------------------------------------------
    # Code Path Coverage Tests
    # ------------------------------------------------------------------------

    def test_entity_type_auto_set_on_init(self):
        """Should automatically set entity_type in __init__"""
        item = Item(name="Test")
        skill = Skill(name="Test")
        knowledge = Knowledge(name="Test")
        learning = Learning(name="Test")
        axiom = Axiom(name="Test", statement="Test")

        self.assertEqual(item.entity_type, EntityType.ITEM.value)
        self.assertEqual(skill.entity_type, EntityType.SKILL.value)
        self.assertEqual(knowledge.entity_type, EntityType.KNOWLEDGE.value)
        self.assertEqual(learning.entity_type, EntityType.LEARNING.value)
        self.assertEqual(axiom.entity_type, EntityType.AXIOM.value)

    def test_uuid_generation_on_commit(self):
        """Should generate UUID on commit (not before)"""
        item = Item(name="Test")
        self.session.add(item)

        # UUID generated after commit
        self.session.commit()
        self.assertIsNotNone(item.id)

    def test_timestamps_auto_set(self):
        """Should automatically set created_at and updated_at"""
        item = Item(name="Test")
        self.session.add(item)
        self.session.commit()

        self.assertIsNotNone(item.created_at)
        self.assertIsNotNone(item.updated_at)

    def test_version_defaults_to_1(self):
        """Should initialize version to 1"""
        item = Item(name="Test")
        self.session.add(item)
        self.session.commit()

        self.assertEqual(item.version, 1)

    def test_metadata_defaults_to_empty_dict(self):
        """Should initialize metadata to empty dict"""
        item = Item(name="Test")
        self.session.add(item)
        self.session.commit()

        self.assertEqual(item.metadata_, {})

    def test_tags_defaults_to_empty_list(self):
        """Should initialize tags to empty list"""
        item = Item(name="Test")
        self.session.add(item)
        self.session.commit()

        self.assertEqual(item.tags, [])

    # ------------------------------------------------------------------------
    # Branch Coverage Tests
    # ------------------------------------------------------------------------

    def test_soft_delete_sets_flags(self):
        """Should set both is_deleted and deleted_at on soft delete"""
        item = Item(name="Test")
        self.session.add(item)
        self.session.commit()

        # Soft delete
        item.is_deleted = True
        item.deleted_at = datetime.now()
        self.session.commit()

        retrieved = self.session.query(Item).filter_by(id=item.id).first()
        self.assertTrue(retrieved.is_deleted)
        self.assertIsNotNone(retrieved.deleted_at)

    def test_query_excludes_soft_deleted(self):
        """Should be able to exclude soft-deleted entities"""
        item1 = Item(name="Active")
        item2 = Item(name="Deleted", is_deleted=True)
        self.session.add_all([item1, item2])
        self.session.commit()

        active_items = self.session.query(Item).filter_by(is_deleted=False).all()

        self.assertIn(item1, active_items)
        self.assertNotIn(item2, active_items)

    def test_relationship_with_metadata(self):
        """Should store metadata in relationship"""
        source = Item(name="Source")
        target = Item(name="Target")
        self.session.add_all([source, target])
        self.session.commit()

        rel = Relationship(
            source_id=source.id,
            source_type=EntityType.ITEM.value,
            target_id=target.id,
            target_type=EntityType.ITEM.value,
            relationship_type=RelationshipType.RELATED_TO.value,
            metadata_={"reason": "test", "confidence": 0.95}
        )
        self.session.add(rel)
        self.session.commit()

        retrieved = self.session.query(Relationship).filter_by(id=rel.id).first()
        self.assertEqual(retrieved.metadata_["reason"], "test")
        self.assertEqual(retrieved.metadata_["confidence"], 0.95)

    # ------------------------------------------------------------------------
    # Function/Method Unit Tests
    # ------------------------------------------------------------------------

    def test_get_session_returns_valid_session(self):
        """Should return valid SQLAlchemy session"""
        from sqlalchemy.orm import Session
        session = get_session()
        self.assertIsInstance(session, Session)
        session.close()

    def test_get_engine_returns_postgres_engine(self):
        """Should return PostgreSQL engine (not SQLite)"""
        engine = get_engine()
        self.assertIn('postgresql', str(engine.url))
        # Verify no sqlite
        self.assertNotIn('sqlite', str(engine.url))

    # ------------------------------------------------------------------------
    # Internal State Verification
    # ------------------------------------------------------------------------

    def test_usage_count_increments(self):
        """Should be able to increment usage count"""
        skill = Skill(name="Test Skill")
        self.session.add(skill)
        self.session.commit()

        # Increment usage
        skill.usage_count += 1
        skill.success_count += 1
        self.session.commit()

        retrieved = self.session.query(Skill).filter_by(id=skill.id).first()
        self.assertEqual(retrieved.usage_count, 1)
        self.assertEqual(retrieved.success_count, 1)

    def test_axiom_status_transition(self):
        """Should be able to transition axiom status"""
        axiom = Axiom(
            name="Test Axiom",
            statement="Test statement",
            status=AxiomStatus.PROPOSED.value
        )
        self.session.add(axiom)
        self.session.commit()

        # Transition to validated
        axiom.status = AxiomStatus.VALIDATED.value
        axiom.validated_at = datetime.now()
        axiom.validated_by = "test_agent"
        self.session.commit()

        retrieved = self.session.query(Axiom).filter_by(id=axiom.id).first()
        self.assertEqual(retrieved.status, AxiomStatus.VALIDATED.value)
        self.assertIsNotNone(retrieved.validated_at)

    # ------------------------------------------------------------------------
    # Exception Handling Paths
    # ------------------------------------------------------------------------

    def test_rollback_on_error(self):
        """Should rollback transaction on error"""
        try:
            item = Item()  # Missing required name
            self.session.add(item)
            self.session.commit()
        except Exception:
            self.session.rollback()

        # Session should be usable after rollback
        valid_item = Item(name="Valid")
        self.session.add(valid_item)
        self.session.commit()

        self.assertIsNotNone(valid_item.id)


# ============================================================================
# INTEGRATION TESTS
# ============================================================================

class TestKnowledgeGraphIntegration(unittest.TestCase):
    """Integration tests for complete workflows"""

    @classmethod
    def setUpClass(cls):
        """Set up test database"""
        cls.engine = get_engine()
        create_all_tables()

    def setUp(self):
        """Create fresh session for each test"""
        self.session = get_session()

    def tearDown(self):
        """Clean up session"""
        self.session.rollback()
        self.session.close()

    def test_complete_entity_lifecycle(self):
        """Should support full entity lifecycle"""
        # Create
        item = Item(name="Lifecycle Test", item_type="test")
        self.session.add(item)
        self.session.commit()
        item_id = item.id

        # Read
        retrieved = self.session.query(Item).filter_by(id=item_id).first()
        self.assertEqual(retrieved.name, "Lifecycle Test")

        # Update
        retrieved.name = "Updated Name"
        retrieved.version += 1
        self.session.commit()

        updated = self.session.query(Item).filter_by(id=item_id).first()
        self.assertEqual(updated.name, "Updated Name")
        self.assertEqual(updated.version, 2)

        # Soft Delete
        updated.is_deleted = True
        updated.deleted_at = datetime.now()
        self.session.commit()

        deleted = self.session.query(Item).filter_by(id=item_id).first()
        self.assertTrue(deleted.is_deleted)

    def test_graph_traversal(self):
        """Should support multi-hop graph traversal"""
        # Create chain: Item -> Skill -> Knowledge
        item = Item(name="Item")
        skill = Skill(name="Skill")
        knowledge = Knowledge(name="Knowledge")

        self.session.add_all([item, skill, knowledge])
        self.session.commit()

        # Create relationships
        rel1 = Relationship(
            source_id=item.id, source_type=EntityType.ITEM.value,
            target_id=skill.id, target_type=EntityType.SKILL.value,
            relationship_type=RelationshipType.USES.value
        )
        rel2 = Relationship(
            source_id=skill.id, source_type=EntityType.SKILL.value,
            target_id=knowledge.id, target_type=EntityType.KNOWLEDGE.value,
            relationship_type=RelationshipType.REQUIRES.value
        )

        self.session.add_all([rel1, rel2])
        self.session.commit()

        # Traverse: Item -> Skill
        hop1 = self.session.query(Relationship).filter_by(
            source_id=item.id
        ).first()
        self.assertEqual(hop1.target_id, skill.id)

        # Traverse: Skill -> Knowledge
        hop2 = self.session.query(Relationship).filter_by(
            source_id=skill.id
        ).first()
        self.assertEqual(hop2.target_id, knowledge.id)


# ============================================================================
# PERFORMANCE TESTS
# ============================================================================

class TestKnowledgeGraphPerformance(unittest.TestCase):
    """Performance and scaling tests"""

    @classmethod
    def setUpClass(cls):
        """Set up test database"""
        cls.engine = get_engine()
        create_all_tables()

    def setUp(self):
        """Create fresh session for each test"""
        self.session = get_session()

    def tearDown(self):
        """Clean up session"""
        self.session.rollback()
        self.session.close()

    def test_bulk_insert_performance(self):
        """Should handle bulk inserts efficiently"""
        import time

        items = [Item(name=f"Item {i}") for i in range(100)]

        start = time.time()
        self.session.add_all(items)
        self.session.commit()
        elapsed = time.time() - start

        # Should complete in reasonable time (< 2 seconds for 100 items)
        self.assertLess(elapsed, 2.0)

    def test_indexed_query_performance(self):
        """Should use indexes for fast queries"""
        import time

        # Create test data
        for i in range(50):
            item = Item(name=f"Item {i}", item_type="indexed_test")
            self.session.add(item)
        self.session.commit()

        # Query with indexed field
        start = time.time()
        results = self.session.query(Item).filter_by(item_type="indexed_test").all()
        elapsed = time.time() - start

        self.assertGreaterEqual(len(results), 50)
        # Should be fast (< 0.1s)
        self.assertLess(elapsed, 0.1)


# ============================================================================
# COMPLIANCE TESTS
# ============================================================================

class TestPostgreSQLCompliance(unittest.TestCase):
    """Verify PostgreSQL compliance (RULE 6)"""

    def test_connection_uses_postgresql(self):
        """Should connect to PostgreSQL, not SQLite"""
        config = PostgresConfig.get_connection_params()

        # Should have PostgreSQL params
        self.assertIn('host', config)
        self.assertIn('port', config)
        self.assertIn('database', config)

        # Should NOT be SQLite
        connection_string = PostgresConfig.get_connection_string()
        self.assertNotIn('sqlite', connection_string.lower())
        self.assertIn('postgresql', connection_string.lower())

    def test_no_sqlite_imports(self):
        """Should not import sqlite3"""
        import core.knowledge.schema as schema_module

        # Check module for sqlite imports
        module_source = str(schema_module.__file__)
        self.assertNotIn('sqlite', module_source.lower())


# ============================================================================
# TEST RUNNER
# ============================================================================

if __name__ == '__main__':
    # Run all test suites
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()

    # Add all test classes
    suite.addTests(loader.loadTestsFromTestCase(TestKnowledgeGraphBlackBox))
    suite.addTests(loader.loadTestsFromTestCase(TestKnowledgeGraphWhiteBox))
    suite.addTests(loader.loadTestsFromTestCase(TestKnowledgeGraphIntegration))
    suite.addTests(loader.loadTestsFromTestCase(TestKnowledgeGraphPerformance))
    suite.addTests(loader.loadTestsFromTestCase(TestPostgreSQLCompliance))

    # Run with verbose output
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)

    # Print summary
    print("\n" + "=" * 70)
    print("TEST SUMMARY")
    print("=" * 70)
    print(f"Tests Run: {result.testsRun}")
    print(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
    print(f"Failures: {len(result.failures)}")
    print(f"Errors: {len(result.errors)}")
    print(f"Success Rate: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%")
    print("=" * 70)

    # Exit with proper code
    exit(0 if result.wasSuccessful() else 1)


# ============================================================================
# VERIFICATION_STAMP
# ============================================================================
# Story: KG-001
# Verified By: Claude (Opus 4.5)
# Verified At: 2026-01-24
# Test Type: Black Box + White Box
# Test Coverage: Entity creation, relationships, soft delete, versioning
# PostgreSQL: Compliance verified (RULE 6)
# ============================================================================
