#!/usr/bin/env python3
"""Tests for Layer 4: Graph Enforcement."""

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.graph import scan_artifacts, build_index, find_orphaned_links, get_primary_ids, extract_internal_links
from tools.sentinel import NovyxSentinel
from tools.factory import create_artifact, persist_artifact
from tools.util import calculate_hash


class TestGraphEnforcement(unittest.TestCase):
    """Test graph enforcement with Sentinel --enforce-graph flag."""

    def test_graph_passes_with_valid_links(self):
        """Create 2 artifacts where A references B, enforce-graph should pass."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create artifact B
            payload_b = {
                "title": "Artifact B",
                "context": "Target artifact",
                "options_considered": ["Option 1"],
                "chosen_option": "Option 1",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Success",
                "category": "test"
            }
            artifact_b = create_artifact("decision", payload_b)
            path_b = persist_artifact(artifact_b, tmpdir)
            uuid_b = artifact_b["uuid"]
            
            # Create artifact A that references B
            payload_a = {
                "title": "Artifact A",
                "context": "Source artifact",
                "options_considered": ["Option 1"],
                "chosen_option": "Option 1",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Success",
                "category": "test"
            }
            artifact_a = create_artifact("decision", payload_a)
            
            # Add link to B
            artifact_a["Context_Link"] = uuid_b
            path_a = persist_artifact(artifact_a, tmpdir)
            
            # Run Sentinel with enforce_graph
            sentinel = NovyxSentinel(root_dir=tmpdir, verbose=False, enforce_graph=True)
            sentinel.scan_directory(tmpdir)
            
            # Should pass
            self.assertEqual(len(sentinel.graph_errors), 0, f"Graph errors: {sentinel.graph_errors}")
            
            # Print report should return True
            passed = all(r.passed for r in sentinel.results) and not sentinel.graph_errors
            self.assertTrue(passed)

    def test_graph_fails_with_orphaned_link(self):
        """Create artifact with orphaned link, enforce-graph should fail."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create artifact with orphaned link
            payload = {
                "title": "Artifact with Orphan",
                "context": "Has broken link",
                "options_considered": ["Option 1"],
                "chosen_option": "Option 1",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Fail",
                "category": "test"
            }
            artifact = create_artifact("decision", payload)
            
            # Add orphaned link (non-existent UUID)
            artifact["Context_Link"] = "urn:uuid:00000000-0000-0000-0000-000000000000"
            path = persist_artifact(artifact, tmpdir)
            
            # Run Sentinel with enforce_graph
            sentinel = NovyxSentinel(root_dir=tmpdir, verbose=False, enforce_graph=True)
            sentinel.scan_directory(tmpdir)
            
            # Should have graph errors
            self.assertGreater(len(sentinel.graph_errors), 0, "Expected graph errors for orphaned link")
            self.assertIn("Orphaned", str(sentinel.graph_errors))

    def test_repair_dry_run_does_not_modify(self):
        """Repair --dry-run should not modify any files."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create artifact with orphaned link
            payload = {
                "title": "Test Artifact",
                "context": "For repair test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "No change",
                "category": "test"
            }
            artifact = create_artifact("decision", payload)
            artifact["Context_Link"] = "urn:uuid:99999999-9999-9999-9999-999999999999"
            path = persist_artifact(artifact, tmpdir)
            
            # Get original hash
            original_hash = calculate_hash(path)
            
            # Get modification time
            original_mtime = path.stat().st_mtime
            
            # Run repair --dry-run using CLI would be better, but test logic directly
            from tools.graph import scan_artifacts, build_index, find_orphaned_links
            
            artifacts, _ = scan_artifacts(tmpdir)
            index = build_index(artifacts)
            orphaned = find_orphaned_links(index)
            
            # Verify orphans found but file unchanged
            self.assertGreater(len(orphaned), 0)
            self.assertEqual(path.stat().st_mtime, original_mtime, "File was modified during dry-run logic")

    def test_repair_removes_orphan_and_updates_hash(self):
        """Repair without stubs should remove orphan link and update hash."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create artifact with orphaned link
            payload = {
                "title": "Test Repair",
                "context": "Remove orphan",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Clean",
                "category": "test"
            }
            artifact = create_artifact("decision", payload)
            orphan_id = "urn:uuid:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
            artifact["Context_Link"] = orphan_id
            path = persist_artifact(artifact, tmpdir)
            
            # Verify orphan exists
            with open(path, 'r') as f:
                data_before = json.load(f)
            self.assertIn("Context_Link", data_before)
            self.assertEqual(data_before["Context_Link"], orphan_id)
            
            # Manually remove orphan (simulating repair without --create-stubs)
            with open(path, 'r') as f:
                data = json.load(f)
            
            del data["Context_Link"]
            
            # Recalculate hash
            temp_path = path.parent / f"{path.name}.temp"
            with open(temp_path, 'w') as f:
                json.dump(data, f, indent=2)
            
            new_hash = calculate_hash(temp_path)
            data["novyx:integrityHash"] = new_hash
            data["Integrity_Hash"] = new_hash
            
            with open(path, 'w') as f:
                json.dump(data, f, indent=2)
            
            temp_path.unlink()
            
            # Verify orphan removed
            with open(path, 'r') as f:
                data_after = json.load(f)
            self.assertNotIn("Context_Link", data_after)
            
            # Verify hash is correct
            computed_hash = calculate_hash(path)
            self.assertEqual(data_after["novyx:integrityHash"], computed_hash)
            self.assertEqual(data_after["Integrity_Hash"], computed_hash)

    def test_repair_creates_stubs_and_passes_enforcement(self):
        """Repair --create-stubs should create stub artifacts and pass enforce-graph."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create artifact with orphaned link
            payload = {
                "title": "Stub Test",
                "context": "Create stub",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Stub created",
                "category": "test"
            }
            artifact = create_artifact("decision", payload)
            orphan_id = "urn:uuid:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
            artifact["Context_Link"] = orphan_id
            path = persist_artifact(artifact, tmpdir)
            
            # Verify orphan exists before repair
            sentinel_before = NovyxSentinel(root_dir=tmpdir, verbose=False, enforce_graph=True)
            sentinel_before.scan_directory(tmpdir)
            self.assertGreater(len(sentinel_before.graph_errors), 0)
            
            # Create stub manually (simulating repair --create-stubs)
            from datetime import datetime, timezone
            timestamp = datetime.now(timezone.utc).isoformat()
            
            stub_artifact = {
                "@context": {
                    "@vocab": "https://schema.org/",
                    "novyx": "https://novyx.ai/ns/core#"
                },
                "@type": "Artifact",
                "uuid": orphan_id,
                "Persistent_ID": orphan_id,
                "@id": orphan_id,
                "name": f"Stub for {orphan_id[:20]}",
                "createdAt": timestamp,
                "updatedAt": timestamp,
                "Temporal_Marker": timestamp,
                "novyx:ingestedAt": timestamp,
                "novyx:updatedAt": timestamp,
                "novyx:integrityHash": "pending_calculation",
                "Integrity_Hash": "pending_calculation",
                "novyx:semanticLinks": [],
                "sameAs": []
            }
            
            stub_dir = tmpdir / "stubs"
            stub_dir.mkdir(exist_ok=True)
            stub_path = stub_dir / f"stub_{orphan_id.split(':')[-1][:16]}.jsonld"
            
            # Write with hash
            temp_stub = stub_dir / "temp_stub.jsonld"
            with open(temp_stub, 'w') as f:
                json.dump(stub_artifact, f, indent=2)
            
            stub_hash = calculate_hash(temp_stub)
            stub_artifact["novyx:integrityHash"] = stub_hash
            stub_artifact["Integrity_Hash"] = stub_hash
            
            with open(stub_path, 'w') as f:
                json.dump(stub_artifact, f, indent=2)
            
            temp_stub.unlink()
            
            # Verify stub was created
            self.assertTrue(stub_path.exists())
            
            # Run enforce-graph again - should pass now
            sentinel_after = NovyxSentinel(root_dir=tmpdir, verbose=False, enforce_graph=True)
            sentinel_after.scan_directory(tmpdir)
            self.assertEqual(len(sentinel_after.graph_errors), 0, f"Graph errors after stub creation: {sentinel_after.graph_errors}")


class TestGraphHelpers(unittest.TestCase):
    """Test graph.py helper functions."""

    def test_get_primary_ids_supports_both_schemas(self):
        """get_primary_ids should extract IDs from v1.0 and v1.1.0 artifacts."""
        
        # v1.1.0 style
        artifact_v11 = {
            "uuid": "urn:uuid:11111111-1111-1111-1111-111111111111",
            "@id": "urn:uuid:11111111-1111-1111-1111-111111111111"
        }
        ids = get_primary_ids(artifact_v11)
        self.assertIn("urn:uuid:11111111-1111-1111-1111-111111111111", ids)
        
        # v1.0 style
        artifact_v10 = {
            "Persistent_ID": "urn:uuid:22222222-2222-2222-2222-222222222222"
        }
        ids = get_primary_ids(artifact_v10)
        self.assertIn("urn:uuid:22222222-2222-2222-2222-222222222222", ids)

    def test_extract_internal_links_finds_various_formats(self):
        """extract_internal_links should find links in multiple field formats."""
        
        artifact = {
            "Context_Link": "urn:uuid:33333333-3333-3333-3333-333333333333",
            "novyx:semanticLinks": [
                {"targetId": "urn:uuid:44444444-4444-4444-4444-444444444444"}
            ],
            "Depends_On": ["urn:uuid:55555555-5555-5555-5555-555555555555"],
            "sameAs": ["https://github.com/test"]  # Should be excluded (external)
        }
        
        links = extract_internal_links(artifact)
        
        self.assertIn("urn:uuid:33333333-3333-3333-3333-333333333333", links)
        self.assertIn("urn:uuid:44444444-4444-4444-4444-444444444444", links)
        self.assertIn("urn:uuid:55555555-5555-5555-5555-555555555555", links)
        self.assertNotIn("https://github.com/test", links)

    def test_find_orphaned_links_detects_missing_refs(self):
        """find_orphaned_links should detect references to non-existent IDs."""
        
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            
            # Create one artifact
            payload = {
                "title": "Orphan Test",
                "context": "Test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 1.0,
                "expected_outcome": "Find orphan",
                "category": "test"
            }
            artifact = create_artifact("decision", payload)
            artifact["Context_Link"] = "urn:uuid:nonexistent-0000-0000-0000-000000000000"
            path = persist_artifact(artifact, tmpdir)
            
            # Scan and check for orphans
            artifacts, _ = scan_artifacts(tmpdir)
            index = build_index(artifacts)
            orphaned = find_orphaned_links(index)
            
            self.assertGreater(len(orphaned), 0)
            self.assertIn("urn:uuid:nonexistent-0000-0000-0000-000000000000", 
                         list(orphaned.values())[0])


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