# tests/test_cache_manager.py
"""Tests for the L1 cache manager."""

import tempfile
import time
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

from cache_manager import (
    L1CacheManager,
    L2SharedChecker,
    CachedFile,
    file_hash,
)


@pytest.fixture
def temp_dir():
    """Create a temporary directory for tests."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


@pytest.fixture
def mock_l2():
    """Mock L2 server responses."""
    with patch("cache_manager.requests") as mock_requests:
        mock_requests.get.return_value = Mock(status_code=404)
        yield mock_requests


@pytest.fixture
def manager(temp_dir, mock_l2):
    """Create a cache manager instance."""
    return L1CacheManager(
        cache_dir=temp_dir / "cache",
        l2_server="http://mock-l2:8200",
    )


def create_test_file(path: Path, content: str = "test content") -> Path:
    """Create a test file with content."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content)
    return path


class TestFileHash:
    """Tests for file_hash function."""

    def test_consistent_hash(self, temp_dir):
        """Same content produces same hash."""
        file1 = create_test_file(temp_dir / "f1.txt", "hello")
        file2 = create_test_file(temp_dir / "f2.txt", "hello")

        assert file_hash(file1) == file_hash(file2)

    def test_different_content_different_hash(self, temp_dir):
        """Different content produces different hash."""
        file1 = create_test_file(temp_dir / "f1.txt", "hello")
        file2 = create_test_file(temp_dir / "f2.txt", "world")

        assert file_hash(file1) != file_hash(file2)

    def test_sha3_256_length(self, temp_dir):
        """Hash is SHA3-256 (64 hex chars)."""
        f = create_test_file(temp_dir / "f.txt", "test")
        assert len(file_hash(f)) == 64


class TestL2SharedChecker:
    """Tests for L2 shared status checking."""

    def test_not_shared_returns_false(self, mock_l2):
        """Non-existent content returns False."""
        checker = L2SharedChecker("http://mock:8200")
        mock_l2.get.return_value = Mock(status_code=404)

        assert checker.is_shared("abc123") is False

    def test_shared_returns_true(self, mock_l2):
        """Published content returns True."""
        checker = L2SharedChecker("http://mock:8200")
        mock_l2.get.return_value = Mock(status_code=200)

        assert checker.is_shared("abc123") is True

    def test_caches_result(self, mock_l2):
        """Results are cached to avoid repeated API calls."""
        checker = L2SharedChecker("http://mock:8200", cache_ttl=60)
        mock_l2.get.return_value = Mock(status_code=200)

        checker.is_shared("abc123")
        checker.is_shared("abc123")

        # Should only call API once
        assert mock_l2.get.call_count == 1

    def test_mark_shared(self, mock_l2):
        """mark_shared updates cache without API call."""
        checker = L2SharedChecker("http://mock:8200")

        checker.mark_shared("abc123")

        assert checker.is_shared("abc123") is True
        assert mock_l2.get.call_count == 0

    def test_invalidate(self, mock_l2):
        """invalidate clears cache for a hash."""
        checker = L2SharedChecker("http://mock:8200")
        mock_l2.get.return_value = Mock(status_code=200)

        checker.is_shared("abc123")
        checker.invalidate("abc123")

        mock_l2.get.return_value = Mock(status_code=404)
        assert checker.is_shared("abc123") is False

    def test_error_returns_true(self, mock_l2):
        """API errors return True (safe - prevents accidental deletion)."""
        checker = L2SharedChecker("http://mock:8200")
        mock_l2.get.side_effect = Exception("Network error")

        # On error, assume IS shared to prevent accidental deletion
        assert checker.is_shared("abc123") is True


class TestL1CacheManagerStorage:
    """Tests for cache storage operations."""

    def test_put_and_get_by_cid(self, manager, temp_dir):
        """Can store and retrieve by content hash."""
        test_file = create_test_file(temp_dir / "input.txt", "hello world")

        cached, cid = manager.put(test_file, node_type="test")

        retrieved_path = manager.get_by_cid(cached.cid)
        assert retrieved_path is not None
        assert retrieved_path.read_text() == "hello world"

    def test_put_with_custom_node_id(self, manager, temp_dir):
        """Can store with custom node_id."""
        test_file = create_test_file(temp_dir / "input.txt", "content")

        cached, cid = manager.put(test_file, node_id="custom-node-123", node_type="test")

        assert cached.node_id == "custom-node-123"
        assert manager.get_by_node_id("custom-node-123") is not None

    def test_has_content(self, manager, temp_dir):
        """has_content checks existence."""
        test_file = create_test_file(temp_dir / "input.txt", "data")

        cached, cid = manager.put(test_file, node_type="test")

        assert manager.has_content(cached.cid) is True
        assert manager.has_content("nonexistent") is False

    def test_list_all(self, manager, temp_dir):
        """list_all returns all cached files."""
        f1 = create_test_file(temp_dir / "f1.txt", "one")
        f2 = create_test_file(temp_dir / "f2.txt", "two")

        manager.put(f1, node_type="test")
        manager.put(f2, node_type="test")

        all_files = manager.list_all()
        assert len(all_files) == 2

    def test_deduplication(self, manager, temp_dir):
        """Same content is not stored twice."""
        f1 = create_test_file(temp_dir / "f1.txt", "identical")
        f2 = create_test_file(temp_dir / "f2.txt", "identical")

        cached1, cid1 = manager.put(f1, node_type="test")
        cached2, cid2 = manager.put(f2, node_type="test")

        assert cached1.cid == cached2.cid
        assert len(manager.list_all()) == 1


class TestL1CacheManagerActivities:
    """Tests for activity tracking."""

    def test_record_simple_activity(self, manager, temp_dir):
        """Can record a simple activity."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        activity = manager.record_simple_activity(
            input_hashes=[input_cached.cid],
            output_cid=output_cached.cid,
            run_id="run-001",
        )

        assert activity.activity_id == "run-001"
        assert input_cached.cid in activity.input_ids
        assert activity.output_id == output_cached.cid

    def test_list_activities(self, manager, temp_dir):
        """Can list all activities."""
        for i in range(3):
            inp = create_test_file(temp_dir / f"in{i}.txt", f"input{i}")
            out = create_test_file(temp_dir / f"out{i}.txt", f"output{i}")
            inp_c, _ = manager.put(inp, node_type="source")
            out_c, _ = manager.put(out, node_type="effect")
            manager.record_simple_activity([inp_c.cid], out_c.cid)

        activities = manager.list_activities()
        assert len(activities) == 3

    def test_find_activities_by_inputs(self, manager, temp_dir):
        """Can find activities with same inputs."""
        input_file = create_test_file(temp_dir / "shared_input.txt", "shared")
        input_cached, _ = manager.put(input_file, node_type="source")

        # Two activities with same input
        out1 = create_test_file(temp_dir / "out1.txt", "output1")
        out2 = create_test_file(temp_dir / "out2.txt", "output2")
        out1_c, _ = manager.put(out1, node_type="effect")
        out2_c, _ = manager.put(out2, node_type="effect")

        manager.record_simple_activity([input_cached.cid], out1_c.cid, "run1")
        manager.record_simple_activity([input_cached.cid], out2_c.cid, "run2")

        found = manager.find_activities_by_inputs([input_cached.cid])
        assert len(found) == 2


class TestL1CacheManagerDeletionRules:
    """Tests for deletion rules enforcement."""

    def test_can_delete_orphaned_item(self, manager, temp_dir):
        """Orphaned items can be deleted."""
        test_file = create_test_file(temp_dir / "orphan.txt", "orphan")
        cached, _ = manager.put(test_file, node_type="test")

        can_delete, reason = manager.can_delete(cached.cid)
        assert can_delete is True

    def test_cannot_delete_activity_input(self, manager, temp_dir):
        """Activity inputs cannot be deleted."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
        )

        can_delete, reason = manager.can_delete(input_cached.cid)
        assert can_delete is False
        assert "input" in reason.lower()

    def test_cannot_delete_activity_output(self, manager, temp_dir):
        """Activity outputs cannot be deleted."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
        )

        can_delete, reason = manager.can_delete(output_cached.cid)
        assert can_delete is False
        assert "output" in reason.lower()

    def test_cannot_delete_pinned_item(self, manager, temp_dir):
        """Pinned items cannot be deleted."""
        test_file = create_test_file(temp_dir / "shared.txt", "shared")
        cached, _ = manager.put(test_file, node_type="test")

        # Mark as pinned (published)
        manager.pin(cached.cid, reason="published")

        can_delete, reason = manager.can_delete(cached.cid)
        assert can_delete is False
        assert "pinned" in reason

    def test_delete_orphaned_item(self, manager, temp_dir):
        """Can delete orphaned items."""
        test_file = create_test_file(temp_dir / "orphan.txt", "orphan")
        cached, _ = manager.put(test_file, node_type="test")

        success, msg = manager.delete_by_cid(cached.cid)

        assert success is True
        assert manager.has_content(cached.cid) is False

    def test_delete_protected_item_fails(self, manager, temp_dir):
        """Cannot delete protected items."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
        )

        success, msg = manager.delete_by_cid(input_cached.cid)

        assert success is False
        assert manager.has_content(input_cached.cid) is True


class TestL1CacheManagerActivityDiscard:
    """Tests for activity discard functionality."""

    def test_can_discard_unshared_activity(self, manager, temp_dir):
        """Activities with no shared items can be discarded."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        activity = manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
            "run-001",
        )

        can_discard, reason = manager.can_discard_activity("run-001")
        assert can_discard is True

    def test_cannot_discard_activity_with_pinned_output(self, manager, temp_dir):
        """Activities with pinned outputs cannot be discarded."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
            "run-001",
        )

        # Mark output as pinned (published)
        manager.pin(output_cached.cid, reason="published")

        can_discard, reason = manager.can_discard_activity("run-001")
        assert can_discard is False
        assert "pinned" in reason

    def test_discard_activity_cleans_up(self, manager, temp_dir):
        """Discarding activity cleans up orphaned items."""
        input_file = create_test_file(temp_dir / "input.txt", "input")
        output_file = create_test_file(temp_dir / "output.txt", "output")

        input_cached, _ = manager.put(input_file, node_type="source")
        output_cached, _ = manager.put(output_file, node_type="effect")

        manager.record_simple_activity(
            [input_cached.cid],
            output_cached.cid,
            "run-001",
        )

        success, msg = manager.discard_activity("run-001")

        assert success is True
        assert manager.get_activity("run-001") is None


class TestL1CacheManagerStats:
    """Tests for cache statistics."""

    def test_get_stats(self, manager, temp_dir):
        """get_stats returns cache statistics."""
        f1 = create_test_file(temp_dir / "f1.txt", "content1")
        f2 = create_test_file(temp_dir / "f2.txt", "content2")

        manager.put(f1, node_type="test")
        manager.put(f2, node_type="test")

        stats = manager.get_stats()

        assert stats["total_entries"] == 2
        assert stats["total_size_bytes"] > 0
        assert "activities" in stats
