#!/usr/bin/env python3
"""Tests for Layer 2: Artifact Factory + Schema-Driven Sentinel."""

import json
import sys
import tempfile
import unittest
from pathlib import Path

# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from tools.factory import create_artifact, persist_artifact, get_required_properties
from tools.sentinel import NovyxSentinel
from tools.util import calculate_hash


class TestFactoryCreatesValidDecision(unittest.TestCase):
    """Test that factory creates valid Decision artifacts that pass Sentinel validation."""

    def test_factory_creates_valid_decision_artifact(self):
        """Create a Decision via factory, persist to tmp dir, run sentinel validation, expect pass."""
        
        # Create a Decision artifact
        payload = {
            "title": "Choose Database",
            "context": "Need fast querying for analytics",
            "options_considered": ["PostgreSQL", "MongoDB", "DynamoDB"],
            "chosen_option": "PostgreSQL",
            "reasoning": "Mature ecosystem, strong ACID guarantees, good query performance",
            "constraints": ["Budget: $500/month", "Must scale to 10M rows"],
            "assumptions": ["Read-heavy workload", "Team knows SQL"],
            "confidence": 0.85,
            "expected_outcome": "Fast queries with low operational overhead",
            "category": "technical"
        }
        
        artifact = create_artifact("decision", payload, external_links=["https://github.com/test"])
        
        # Verify artifact structure
        self.assertIn("uuid", artifact)
        self.assertIn("novyx:integrityHash", artifact)
        self.assertIn("Integrity_Hash", artifact)
        self.assertEqual(artifact["novyx:integrityHash"], "pending_calculation")
        self.assertEqual(artifact["Integrity_Hash"], "pending_calculation")
        self.assertEqual(artifact["title"], "Choose Database")
        self.assertEqual(artifact["chosen_option"], "PostgreSQL")
        self.assertIsInstance(artifact["options_considered"], list)
        self.assertIsInstance(artifact["constraints"], list)
        self.assertIsInstance(artifact["assumptions"], list)
        
        # Persist to temp directory
        with tempfile.TemporaryDirectory() as tmpdir:
            output_dir = Path(tmpdir) / "decisions"
            output_path = persist_artifact(artifact, output_dir)
            
            # Verify file was created
            self.assertTrue(output_path.exists())
            
            # Verify hash was calculated (no longer "pending_calculation")
            with open(output_path, 'r') as f:
                persisted = json.load(f)
            
            self.assertNotEqual(persisted["novyx:integrityHash"], "pending_calculation")
            self.assertNotEqual(persisted["Integrity_Hash"], "pending_calculation")
            self.assertEqual(persisted["novyx:integrityHash"], persisted["Integrity_Hash"])
            
            # Verify hash is correct
            expected_hash = calculate_hash(output_path)
            self.assertEqual(persisted["novyx:integrityHash"], expected_hash)
            
            # Run Sentinel validation
            sentinel = NovyxSentinel(root_dir=output_dir, verbose=False)
            sentinel.scan_directory(output_dir)
            
            # Check validation passed
            self.assertEqual(len(sentinel.results), 1)
            result = sentinel.results[0]
            self.assertTrue(result.passed, f"Sentinel validation failed: {result.errors}")
            self.assertEqual(len(result.errors), 0, f"Unexpected errors: {result.errors}")

    def test_factory_validates_required_fields(self):
        """Factory should raise KeyError when required fields are missing."""
        
        # Missing several required fields
        incomplete_payload = {
            "title": "Test Decision",
            "context": "Some context"
            # Missing: options_considered, chosen_option, reasoning, etc.
        }
        
        with self.assertRaises(KeyError) as ctx:
            create_artifact("decision", incomplete_payload)
        
        self.assertIn("Missing required fields", str(ctx.exception))


class TestSentinelUsesSchemaRequiredProperties(unittest.TestCase):
    """Test that Sentinel validates using CORE_SCHEMA.jsonld requiredProperties."""

    def test_sentinel_uses_schema_required_properties(self):
        """Remove one required field from Decision artifact and ensure sentinel fails with 'missing required field'."""
        
        # Create a valid Decision artifact
        payload = {
            "title": "Test Decision",
            "context": "Test context",
            "options_considered": ["A", "B"],
            "chosen_option": "A",
            "reasoning": "Because A is better",
            "constraints": ["Time"],
            "assumptions": ["Stable environment"],
            "confidence": 0.9,
            "expected_outcome": "Success",
            "category": "test"
        }
        
        artifact = create_artifact("decision", payload)
        
        # Persist to temp directory
        with tempfile.TemporaryDirectory() as tmpdir:
            output_dir = Path(tmpdir) / "decisions"
            output_path = persist_artifact(artifact, output_dir)
            
            # Verify it passes validation first
            sentinel = NovyxSentinel(root_dir=output_dir, verbose=False)
            result = sentinel.validate_artifact(output_path)
            self.assertTrue(result.passed, f"Valid artifact should pass: {result.errors}")
            
            # Now remove a required field (reasoning) and re-save
            with open(output_path, 'r') as f:
                broken_artifact = json.load(f)
            
            del broken_artifact["reasoning"]  # Remove required field
            
            # Update hash for the broken artifact
            broken_artifact["novyx:integrityHash"] = "pending_calculation"
            broken_artifact["Integrity_Hash"] = "pending_calculation"
            
            broken_path = output_dir / "broken_decision.jsonld"
            with open(broken_path, 'w') as f:
                json.dump(broken_artifact, f, indent=2)
            
            # Calculate new hash
            new_hash = calculate_hash(broken_path)
            broken_artifact["novyx:integrityHash"] = new_hash
            broken_artifact["Integrity_Hash"] = new_hash
            
            with open(broken_path, 'w') as f:
                json.dump(broken_artifact, f, indent=2)
            
            # Validate broken artifact
            sentinel2 = NovyxSentinel(root_dir=output_dir, verbose=False)
            result2 = sentinel2.validate_artifact(broken_path)
            
            # Should fail due to missing reasoning field
            self.assertFalse(result2.passed, "Artifact with missing required field should fail")
            
            # Check that error mentions missing field
            error_text = " ".join(result2.errors).lower()
            self.assertTrue(
                "missing" in error_text or "reasoning" in error_text,
                f"Expected error about missing 'reasoning', got: {result2.errors}"
            )

    def test_schema_driven_validation_detects_decision_type(self):
        """Sentinel should detect Decision artifacts by @type and apply schema validation."""
        
        # Create valid Decision
        payload = {
            "title": "Schema Test",
            "context": "Testing schema detection",
            "options_considered": ["Option1"],
            "chosen_option": "Option1",
            "reasoning": "Testing",
            "constraints": [],
            "assumptions": [],
            "confidence": 1.0,
            "expected_outcome": "Pass",
            "category": "test"
        }
        
        artifact = create_artifact("decision", payload)
        
        # Verify @type includes "Decision"
        self.assertIn("@type", artifact)
        type_list = artifact["@type"] if isinstance(artifact["@type"], list) else [artifact["@type"]]
        self.assertTrue("Decision" in type_list or "novyx:Decision" in type_list)
        
        with tempfile.TemporaryDirectory() as tmpdir:
            output_dir = Path(tmpdir) / "decisions"
            output_path = persist_artifact(artifact, output_dir)
            
            # Load and verify Sentinel detects it as Decision
            with open(output_path, 'r') as f:
                data = json.load(f)
            
            sentinel = NovyxSentinel(root_dir=output_dir, verbose=False)
            schema_id = sentinel.get_artifact_schema_id(data)
            self.assertEqual(schema_id, "novyx:Decision")
            
            # Verify schema-driven required properties are loaded
            required_props = get_required_properties("novyx:Decision")
            self.assertGreater(len(required_props), 0)
            self.assertIn("title", [p.split("|")[0] for p in required_props])


class TestBackwardCompatibility(unittest.TestCase):
    """Ensure Layer 2 changes don't break existing artifacts or hash behavior."""

    def test_hash_behavior_preserved(self):
        """Verify hash calculation behavior remains unchanged."""
        
        # Create artifact with factory
        payload = {
            "title": "Hash Test",
            "context": "Testing hash consistency",
            "options_considered": ["A"],
            "chosen_option": "A",
            "reasoning": "Test",
            "constraints": [],
            "assumptions": [],
            "confidence": 1.0,
            "expected_outcome": "Consistent hash",
            "category": "test"
        }
        
        artifact = create_artifact("decision", payload)
        
        with tempfile.TemporaryDirectory() as tmpdir:
            output_dir = Path(tmpdir)
            output_path = persist_artifact(artifact, output_dir)
            
            # Read persisted artifact
            with open(output_path, 'r') as f:
                persisted = json.load(f)
            
            # Recalculate hash manually using util function
            expected_hash = calculate_hash(output_path)
            
            # Verify both hash fields match
            self.assertEqual(persisted["novyx:integrityHash"], expected_hash)
            self.assertEqual(persisted["Integrity_Hash"], expected_hash)
            
            # Verify hash is a valid SHA-256 hex (64 chars)
            self.assertEqual(len(expected_hash), 64)
            self.assertTrue(all(c in '0123456789abcdef' for c in expected_hash))


if __name__ == '__main__':
    unittest.main()
