#!/usr/bin/env python3
"""Tests for tools/graph_cache.py graph index caching."""

import json
import os
import sys
import time
import tempfile
import shutil
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock

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

from tools.graph_cache import get_index, invalidate, reset_cache, _get_ttl


def create_test_artifact(directory: Path, name: str, uuid: str) -> Path:
    """Create a minimal test artifact."""
    artifact = {
        "@context": {"@vocab": "https://schema.org/"},
        "@type": "Artifact",
        "uuid": uuid,
        "name": name,
        "novyx:integrityHash": "test_hash_123"
    }
    filepath = directory / f"{name}.jsonld"
    with open(filepath, 'w') as f:
        json.dump(artifact, f, indent=2)
    return filepath


class TestGraphCache(unittest.TestCase):
    """Tests for graph index caching functionality."""

    def setUp(self):
        """Create temp directory and reset cache before each test."""
        self.temp_dir = Path(tempfile.mkdtemp())
        reset_cache()
        # Set short TTL for testing
        os.environ["NOVYX_GRAPH_CACHE_TTL_SECONDS"] = "30"

    def tearDown(self):
        """Clean up temp directory after each test."""
        shutil.rmtree(self.temp_dir, ignore_errors=True)
        reset_cache()
        if "NOVYX_GRAPH_CACHE_TTL_SECONDS" in os.environ:
            del os.environ["NOVYX_GRAPH_CACHE_TTL_SECONDS"]

    def test_cache_hit_does_not_rescan(self):
        """First call builds index; second call within TTL does NOT rescan."""
        # Create test artifact
        create_test_artifact(self.temp_dir, "artifact1", "urn:uuid:test-1")

        # Track scan_artifacts calls
        call_count = {"count": 0}
        original_scan = None

        def counting_scan(directory):
            call_count["count"] += 1
            return original_scan(directory)

        # Patch scan_artifacts to count calls
        from tools import graph_cache
        original_scan = graph_cache.scan_artifacts

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            # First call - should scan
            index1 = get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)

            # Second call within TTL - should use cache (no scan)
            index2 = get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)  # Still 1, not 2

            # Verify same result
            self.assertEqual(index1['id_to_file'], index2['id_to_file'])

    def test_mtime_change_invalidates_cache(self):
        """Touch/modify a jsonld file mtime -> cache invalidates and rebuilds."""
        # Create test artifact
        artifact_path = create_test_artifact(
            self.temp_dir, "artifact1", "urn:uuid:test-1"
        )

        # First call - builds cache
        index1 = get_index(self.temp_dir)
        self.assertIn("urn:uuid:test-1", index1['id_to_file'])

        # Modify the file (change mtime)
        time.sleep(0.1)  # Ensure mtime changes
        with open(artifact_path, 'r') as f:
            data = json.load(f)
        data["name"] = "modified"
        with open(artifact_path, 'w') as f:
            json.dump(data, f, indent=2)

        # Second call - should detect mtime change and rebuild
        call_count = {"count": 0}
        original_scan = None

        def counting_scan(directory):
            call_count["count"] += 1
            from tools.graph import scan_artifacts as real_scan
            return real_scan(directory)

        from tools import graph_cache

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            index2 = get_index(self.temp_dir)
            # Should have scanned due to mtime change
            self.assertEqual(call_count["count"], 1)

    def test_force_rebuild_bypasses_cache(self):
        """force_rebuild=True rebuilds even within TTL."""
        # Create test artifact
        create_test_artifact(self.temp_dir, "artifact1", "urn:uuid:test-1")

        call_count = {"count": 0}

        def counting_scan(directory):
            call_count["count"] += 1
            from tools.graph import scan_artifacts as real_scan
            return real_scan(directory)

        from tools import graph_cache

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            # First call - builds cache
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)

            # Second call with force_rebuild - should scan again
            get_index(self.temp_dir, force_rebuild=True)
            self.assertEqual(call_count["count"], 2)

            # Third call without force_rebuild - should use cache
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 2)  # Still 2

    def test_separate_directories_have_separate_caches(self):
        """Cache is keyed per directory (two temp dirs don't share)."""
        # Create two separate directories
        dir1 = self.temp_dir / "dir1"
        dir2 = self.temp_dir / "dir2"
        dir1.mkdir()
        dir2.mkdir()

        # Create different artifacts in each
        create_test_artifact(dir1, "artifact1", "urn:uuid:dir1-artifact")
        create_test_artifact(dir2, "artifact2", "urn:uuid:dir2-artifact")

        # Get index for each directory
        index1 = get_index(dir1)
        index2 = get_index(dir2)

        # Verify they have different contents
        self.assertIn("urn:uuid:dir1-artifact", index1['id_to_file'])
        self.assertNotIn("urn:uuid:dir2-artifact", index1['id_to_file'])

        self.assertIn("urn:uuid:dir2-artifact", index2['id_to_file'])
        self.assertNotIn("urn:uuid:dir1-artifact", index2['id_to_file'])

    def test_invalidate_clears_cache(self):
        """Repair invalidates cache (calling invalidate after writes)."""
        # Create test artifact
        create_test_artifact(self.temp_dir, "artifact1", "urn:uuid:test-1")

        call_count = {"count": 0}

        def counting_scan(directory):
            call_count["count"] += 1
            from tools.graph import scan_artifacts as real_scan
            return real_scan(directory)

        from tools import graph_cache

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            # First call - builds cache
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)

            # Second call - uses cache
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)

            # Simulate repair: invalidate cache
            invalidate(self.temp_dir)

            # Third call after invalidate - should rebuild
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 2)

    def test_invalidate_all_clears_everything(self):
        """invalidate() with no args clears all caches."""
        # Create two directories with artifacts
        dir1 = self.temp_dir / "dir1"
        dir2 = self.temp_dir / "dir2"
        dir1.mkdir()
        dir2.mkdir()

        create_test_artifact(dir1, "artifact1", "urn:uuid:dir1-artifact")
        create_test_artifact(dir2, "artifact2", "urn:uuid:dir2-artifact")

        # Build caches for both
        get_index(dir1)
        get_index(dir2)

        call_count = {"count": 0}

        def counting_scan(directory):
            call_count["count"] += 1
            from tools.graph import scan_artifacts as real_scan
            return real_scan(directory)

        from tools import graph_cache

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            # Invalidate all
            invalidate()

            # Both should rebuild
            get_index(dir1)
            get_index(dir2)
            self.assertEqual(call_count["count"], 2)

    def test_tenant_id_creates_separate_cache_key(self):
        """tenant_id parameter creates separate cache entries."""
        # Create test artifact
        create_test_artifact(self.temp_dir, "artifact1", "urn:uuid:test-1")

        call_count = {"count": 0}

        def counting_scan(directory):
            call_count["count"] += 1
            from tools.graph import scan_artifacts as real_scan
            return real_scan(directory)

        from tools import graph_cache

        with patch.object(graph_cache, 'scan_artifacts', side_effect=counting_scan):
            # Call with no tenant
            get_index(self.temp_dir)
            self.assertEqual(call_count["count"], 1)

            # Call with tenant_id - should be separate cache key
            get_index(self.temp_dir, tenant_id="tenant-a")
            self.assertEqual(call_count["count"], 2)

            # Call with same tenant_id - should use cache
            get_index(self.temp_dir, tenant_id="tenant-a")
            self.assertEqual(call_count["count"], 2)

            # Call with different tenant_id - should be separate
            get_index(self.temp_dir, tenant_id="tenant-b")
            self.assertEqual(call_count["count"], 3)


class TestCacheTTL(unittest.TestCase):
    """Tests for cache TTL configuration."""

    def setUp(self):
        reset_cache()

    def tearDown(self):
        reset_cache()
        if "NOVYX_GRAPH_CACHE_TTL_SECONDS" in os.environ:
            del os.environ["NOVYX_GRAPH_CACHE_TTL_SECONDS"]

    def test_ttl_from_environment(self):
        """TTL is configurable via NOVYX_GRAPH_CACHE_TTL_SECONDS."""
        reset_cache()
        os.environ["NOVYX_GRAPH_CACHE_TTL_SECONDS"] = "60"
        ttl = _get_ttl()
        self.assertEqual(ttl, 60.0)

    def test_ttl_default(self):
        """Default TTL is 30 seconds."""
        reset_cache()
        if "NOVYX_GRAPH_CACHE_TTL_SECONDS" in os.environ:
            del os.environ["NOVYX_GRAPH_CACHE_TTL_SECONDS"]
        ttl = _get_ttl()
        self.assertEqual(ttl, 30.0)


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