"""Tests for the ScryCache class."""

import json
import tempfile
from pathlib import Path

import pytest

from scry_run.cache import ScryCache, CacheEntry


class TestCacheEntry:
    """Tests for CacheEntry dataclass."""

    def test_create_entry(self):
        """Test creating a cache entry with auto-generated metadata."""
        entry = CacheEntry.create(
            class_name="TestClass",
            attr_name="test_method",
            code="def test_method(self): return 42",
            code_type="method",
            docstring="Returns 42",
            dependencies=["import json"],
        )
        
        assert entry.class_name == "TestClass"
        assert entry.attr_name == "test_method"
        assert entry.code == "def test_method(self): return 42"
        assert entry.code_type == "method"
        assert entry.docstring == "Returns 42"
        assert entry.dependencies == ["import json"]
        assert entry.created_at  # Should have a timestamp
        assert entry.checksum  # Should have a checksum
        assert len(entry.checksum) == 64  # SHA256 hex digest

    def test_checksum_changes_with_code(self):
        """Test that checksum changes when code changes."""
        entry1 = CacheEntry.create("A", "b", "code1")
        entry2 = CacheEntry.create("A", "b", "code2")
        
        assert entry1.checksum != entry2.checksum


class TestScryCache:
    """Tests for ScryCache class."""

    @pytest.fixture
    def temp_cache(self, tmp_path):
        """Create a temporary cache directory."""
        return ScryCache(tmp_path / "cache")

    def test_init_creates_directory(self, tmp_path):
        """Test that init creates the cache directory."""
        cache_dir = tmp_path / "new_cache"
        assert not cache_dir.exists()
        
        cache = ScryCache(cache_dir)
        
        assert cache_dir.exists()

    def test_set_and_get(self, temp_cache):
        """Test setting and getting a cache entry."""
        entry = temp_cache.set(
            class_name="MyClass",
            attr_name="my_method",
            code="def my_method(self): pass",
            code_type="method",
            docstring="Test method",
        )
        
        assert entry.class_name == "MyClass"
        assert entry.attr_name == "my_method"
        
        retrieved = temp_cache.get("MyClass", "my_method")
        
        assert retrieved is not None
        assert retrieved.class_name == "MyClass"
        assert retrieved.attr_name == "my_method"
        assert retrieved.code == "def my_method(self): pass"

    def test_get_nonexistent(self, temp_cache):
        """Test getting a nonexistent entry returns None."""
        result = temp_cache.get("NonExistent", "method")
        
        assert result is None

    def test_delete_existing(self, temp_cache):
        """Test deleting an existing entry."""
        temp_cache.set("MyClass", "method1", "def method1(self): pass")
        
        result = temp_cache.delete("MyClass", "method1")
        
        assert result is True
        assert temp_cache.get("MyClass", "method1") is None

    def test_delete_nonexistent(self, temp_cache):
        """Test deleting a nonexistent entry returns False."""
        result = temp_cache.delete("NonExistent", "method")
        
        assert result is False

    def test_list_entries_empty(self, temp_cache):
        """Test listing entries from empty cache."""
        entries = temp_cache.list_entries()
        
        assert entries == []

    def test_list_entries_multiple(self, temp_cache):
        """Test listing multiple entries."""
        temp_cache.set("ClassA", "method1", "def method1(self): pass")
        temp_cache.set("ClassA", "method2", "def method2(self): pass")
        temp_cache.set("ClassB", "method1", "def method1(self): pass")
        
        entries = temp_cache.list_entries()
        
        assert len(entries) == 3
        class_names = {e.class_name for e in entries}
        assert class_names == {"ClassA", "ClassB"}

    def test_prune_specific_entry(self, temp_cache):
        """Test pruning a specific entry."""
        temp_cache.set("MyClass", "method1", "code1")
        temp_cache.set("MyClass", "method2", "code2")
        
        pruned = temp_cache.prune(class_name="MyClass", attr_name="method1")
        
        assert pruned == 1
        assert temp_cache.get("MyClass", "method1") is None
        assert temp_cache.get("MyClass", "method2") is not None

    def test_prune_class(self, temp_cache):
        """Test pruning all entries for a class."""
        temp_cache.set("MyClass", "method1", "code1")
        temp_cache.set("MyClass", "method2", "code2")
        temp_cache.set("OtherClass", "method1", "code3")
        
        pruned = temp_cache.prune(class_name="MyClass")
        
        assert pruned == 2
        assert temp_cache.get("MyClass", "method1") is None
        assert temp_cache.get("MyClass", "method2") is None
        assert temp_cache.get("OtherClass", "method1") is not None

    def test_prune_all(self, temp_cache):
        """Test pruning entire cache."""
        temp_cache.set("ClassA", "method1", "code1")
        temp_cache.set("ClassB", "method1", "code2")
        
        pruned = temp_cache.prune()
        
        assert pruned == 2
        assert temp_cache.list_entries() == []

    def test_export_empty(self, temp_cache):
        """Test exporting empty cache."""
        data = temp_cache.export()
        
        assert data == {}

    def test_export_with_entries(self, temp_cache):
        """Test exporting cache with entries."""
        temp_cache.set("ClassA", "method1", "def method1(self): pass")
        temp_cache.set("ClassB", "method2", "def method2(self): pass")
        
        data = temp_cache.export()
        
        assert "ClassA" in data
        assert "ClassB" in data
        assert "method1" in data["ClassA"]
        assert "method2" in data["ClassB"]

    def test_export_to_file(self, temp_cache, tmp_path):
        """Test exporting cache to Python file."""
        temp_cache.set(
            "MyClass", 
            "my_method", 
            "def my_method(self):\n    return 42",
            docstring="Returns 42",
            dependencies=["import json"],
        )
        
        output_file = tmp_path / "exported.py"
        temp_cache.export_to_file(output_file)
        
        assert output_file.exists()
        content = output_file.read_text()
        assert "import json" in content
        assert "def my_method(self):" in content
        assert "Returns 42" in content

    def test_persistence(self, tmp_path):
        """Test that cache persists across instances."""
        cache_dir = tmp_path / "persistent_cache"
        
        # First instance - write
        cache1 = ScryCache(cache_dir)
        cache1.set("MyClass", "method", "def method(self): pass")
        
        # Second instance - read
        cache2 = ScryCache(cache_dir)
        entry = cache2.get("MyClass", "method")
        
        assert entry is not None
        assert entry.code == "def method(self): pass"

    def test_multiple_entries_same_class(self, temp_cache):
        """Test storing multiple entries for the same class."""
        temp_cache.set("MyClass", "method1", "code1")
        temp_cache.set("MyClass", "method2", "code2")
        temp_cache.set("MyClass", "method3", "code3")
        
        assert temp_cache.get("MyClass", "method1").code == "code1"
        assert temp_cache.get("MyClass", "method2").code == "code2"
        assert temp_cache.get("MyClass", "method3").code == "code3"

    def test_update_existing_entry(self, temp_cache):
        """Test updating an existing cache entry."""
        temp_cache.set("MyClass", "method", "old_code")
        temp_cache.set("MyClass", "method", "new_code")

        entry = temp_cache.get("MyClass", "method")

        assert entry.code == "new_code"


class TestScryCacheSingleFile:
    """Tests for single-file cache mode (cache.json)."""

    def test_init_with_file_path(self, tmp_path):
        """Test cache can be initialized with a file path."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)
        assert cache.cache_path == cache_file
        assert cache._file_mode is True

    def test_init_with_directory_path(self, tmp_path):
        """Test cache uses directory mode for non-.json paths."""
        cache_dir = tmp_path / "cache"
        cache = ScryCache(cache_dir)
        assert cache.cache_path == cache_dir
        assert cache._file_mode is False

    def test_set_creates_file(self, tmp_path):
        """Test set creates cache file if missing."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)
        cache.set("MyClass", "my_method", "def my_method(self): pass")
        assert cache_file.exists()

    def test_get_from_single_file(self, tmp_path):
        """Test get retrieves from single file cache."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "def method1(self): pass")
        cache.set("OtherClass", "method2", "def method2(self): pass")

        entry1 = cache.get("MyClass", "method1")
        entry2 = cache.get("OtherClass", "method2")

        assert entry1 is not None
        assert entry1.code == "def method1(self): pass"
        assert entry2 is not None
        assert entry2.code == "def method2(self): pass"

    def test_single_file_structure(self, tmp_path):
        """Test single file has correct JSON structure."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("MyClass", "method2", "code2")

        data = json.loads(cache_file.read_text())

        # Structure: {class_name: {attr_name: entry}}
        assert "MyClass" in data
        assert "method1" in data["MyClass"]
        assert "method2" in data["MyClass"]

    def test_delete_from_single_file(self, tmp_path):
        """Test delete works in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("MyClass", "method2", "code2")

        result = cache.delete("MyClass", "method1")
        assert result is True

        # method1 gone, method2 still there
        assert cache.get("MyClass", "method1") is None
        assert cache.get("MyClass", "method2") is not None

    def test_list_entries_single_file(self, tmp_path):
        """Test list_entries works in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("OtherClass", "method2", "code2")

        entries = cache.list_entries()
        assert len(entries) == 2

    def test_prune_single_file(self, tmp_path):
        """Test prune works in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("OtherClass", "method2", "code2")

        pruned = cache.prune()
        assert pruned == 2
        assert not cache_file.exists()

    def test_prune_class_single_file(self, tmp_path):
        """Test prune class works in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("OtherClass", "method2", "code2")

        pruned = cache.prune(class_name="MyClass")
        assert pruned == 1
        assert cache.get("MyClass", "method1") is None
        assert cache.get("OtherClass", "method2") is not None

    def test_export_single_file(self, tmp_path):
        """Test export works in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        cache.set("MyClass", "method1", "code1")
        cache.set("OtherClass", "method2", "code2")

        data = cache.export()
        assert "MyClass" in data
        assert "OtherClass" in data
        assert "method1" in data["MyClass"]
        assert "method2" in data["OtherClass"]

    def test_persistence_single_file(self, tmp_path):
        """Test single file cache persists across instances."""
        cache_file = tmp_path / "cache.json"

        # First instance - write
        cache1 = ScryCache(cache_file)
        cache1.set("MyClass", "method", "def method(self): pass")

        # Second instance - read
        cache2 = ScryCache(cache_file)
        entry = cache2.get("MyClass", "method")

        assert entry is not None
        assert entry.code == "def method(self): pass"

    def test_get_nonexistent_single_file(self, tmp_path):
        """Test getting nonexistent entry returns None in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        result = cache.get("NonExistent", "method")
        assert result is None

    def test_delete_nonexistent_single_file(self, tmp_path):
        """Test deleting nonexistent entry returns False in single-file mode."""
        cache_file = tmp_path / "cache.json"
        cache = ScryCache(cache_file)

        result = cache.delete("NonExistent", "method")
        assert result is False
