"""
Tests for the Vault module.
"""

import pytest
import time
from threading import Thread

from bgx.vault import ResultVault, create_vault, short_lived_vault, long_lived_vault


class TestResultVault:
    """Test ResultVault functionality."""
    
    def test_vault_creation(self):
        """Test vault creation with default parameters."""
        vault = ResultVault()
        assert vault.ttl == 300.0
        assert vault.max_size == 1000
        assert vault.size() == 0
    
    def test_vault_creation_custom(self):
        """Test vault creation with custom parameters."""
        vault = ResultVault(ttl=60.0, max_size=500)
        assert vault.ttl == 60.0
        assert vault.max_size == 500
    
    def test_store_and_get_result(self):
        """Test storing and retrieving results."""
        vault = ResultVault()
        
        # Store a result
        vault.store("task1", "result1")
        
        # Retrieve the result
        result = vault.get("task1")
        assert result == "result1"
        
        # Check size
        assert vault.size() == 1
    
    def test_store_and_get_error(self):
        """Test storing and retrieving errors."""
        vault = ResultVault()
        
        # Store an error
        error = ValueError("test error")
        vault.store("task1", error=error)
        
        # Retrieve should raise the error
        with pytest.raises(ValueError, match="test error"):
            vault.get("task1")
    
    def test_get_nonexistent(self):
        """Test getting non-existent task."""
        vault = ResultVault()
        
        result = vault.get("nonexistent")
        assert result is None
    
    def test_ttl_expiration(self):
        """Test TTL-based expiration."""
        vault = ResultVault(ttl=0.1)  # 100ms TTL
        
        # Store a result
        vault.store("task1", "result1")
        
        # Should be available immediately
        result = vault.get("task1")
        assert result == "result1"
        
        # Wait for expiration
        time.sleep(0.15)
        
        # Should be expired
        result = vault.get("task1")
        assert result is None
    
    def test_lru_eviction(self):
        """Test LRU eviction when max_size is exceeded."""
        vault = ResultVault(max_size=2)
        
        # Store first result
        vault.store("task1", "result1")
        assert vault.size() == 1
        
        # Store second result
        vault.store("task2", "result2")
        assert vault.size() == 2
        
        # Access first result to make it recently used
        vault.get("task1")
        
        # Store third result - should evict task2 (least recently used)
        vault.store("task3", "result3")
        assert vault.size() == 2
        
        # task1 should still exist
        assert vault.get("task1") == "result1"
        
        # task2 should be evicted
        assert vault.get("task2") is None
        
        # task3 should exist
        assert vault.get("task3") == "result3"
    
    def test_wait_for_immediate(self):
        """Test waiting for immediately available result."""
        vault = ResultVault()
        
        # Store result first
        vault.store("task1", "result1")
        
        # Wait should return immediately
        result = vault.wait_for("task1", timeout=1.0)
        assert result == "result1"
    
    def test_wait_for_delayed(self):
        """Test waiting for delayed result."""
        vault = ResultVault()
        
        def delayed_store():
            time.sleep(0.1)
            vault.store("task1", "result1")
        
        # Start delayed store in background
        Thread(target=delayed_store).start()
        
        # Wait should return after delay
        result = vault.wait_for("task1", timeout=1.0)
        assert result == "result1"
    
    def test_wait_for_timeout(self):
        """Test wait_for timeout."""
        vault = ResultVault()
        
        # Should timeout waiting for non-existent task
        with pytest.raises(TimeoutError):
            vault.wait_for("task1", timeout=0.1)
    
    def test_wait_for_error(self):
        """Test waiting for task that stored an error."""
        vault = ResultVault()
        
        # Store error first
        error = ValueError("test error")
        vault.store("task1", error=error)
        
        # Wait should raise the error
        with pytest.raises(ValueError, match="test error"):
            vault.wait_for("task1", timeout=1.0)
    
    def test_remove(self):
        """Test removing entries."""
        vault = ResultVault()
        
        # Store result
        vault.store("task1", "result1")
        assert vault.size() == 1
        
        # Remove existing entry
        removed = vault.remove("task1")
        assert removed is True
        assert vault.size() == 0
        assert vault.get("task1") is None
        
        # Try to remove non-existent entry
        removed = vault.remove("task1")
        assert removed is False
    
    def test_clear(self):
        """Test clearing all entries."""
        vault = ResultVault()
        
        # Store multiple results
        vault.store("task1", "result1")
        vault.store("task2", "result2")
        vault.store("task3", "result3")
        assert vault.size() == 3
        
        # Clear all
        count = vault.clear()
        assert count == 3
        assert vault.size() == 0
    
    def test_get_stats(self):
        """Test getting vault statistics."""
        vault = ResultVault(ttl=0.1, max_size=100)
        
        # Store some results
        vault.store("task1", "result1")
        vault.store("task2", "result2")
        
        stats = vault.get_stats()
        assert stats["total_entries"] == 2
        assert stats["max_size"] == 100
        assert stats["ttl"] == 0.1
        assert stats["expired_entries"] == 0
        assert stats["utilization"] == 0.02
        
        # Wait for expiration
        time.sleep(0.15)
        
        stats = vault.get_stats()
        assert stats["expired_entries"] == 2
    
    def test_get_entry_info(self):
        """Test getting detailed entry information."""
        vault = ResultVault()
        
        # Store a result
        vault.store("task1", "result1")
        
        info = vault.get_entry_info("task1")
        assert info is not None
        assert info["task_id"] == "task1"
        assert info["has_result"] is True
        assert info["has_error"] is False
        assert info["stored_at"] > 0
        assert info["last_accessed"] > 0
        assert info["age_seconds"] >= 0
        assert info["time_since_access"] >= 0
        assert info["expired"] is False
        
        # Access the entry to update last_accessed
        time.sleep(0.01)
        vault.get("task1")
        
        info2 = vault.get_entry_info("task1")
        assert info2["last_accessed"] > info["last_accessed"]
        assert info2["time_since_access"] < info["time_since_access"]
        
        # Test non-existent entry
        info = vault.get_entry_info("nonexistent")
        assert info is None
    
    def test_concurrent_access(self):
        """Test concurrent access to vault."""
        vault = ResultVault()
        results = []
        errors = []
        
        def store_results(start_id, count):
            for i in range(count):
                try:
                    vault.store(f"task{start_id + i}", f"result{start_id + i}")
                    results.append(f"task{start_id + i}")
                except Exception as e:
                    errors.append(e)
        
        def get_results(start_id, count):
            for i in range(count):
                try:
                    result = vault.get(f"task{start_id + i}")
                    if result is not None:
                        results.append(f"got_{result}")
                except Exception as e:
                    errors.append(e)
        
        # Start multiple threads
        threads = []
        threads.append(Thread(target=store_results, args=(0, 50)))
        threads.append(Thread(target=store_results, args=(50, 50)))
        threads.append(Thread(target=get_results, args=(0, 100)))
        
        for thread in threads:
            thread.start()
        
        for thread in threads:
            thread.join()
        
        # Should have no errors
        assert len(errors) == 0
        
        # Should have stored and retrieved some results
        assert len(results) > 0
    
    def test_shutdown(self):
        """Test vault shutdown."""
        vault = ResultVault()
        
        # Store some data
        vault.store("task1", "result1")
        assert vault.size() == 1
        
        # Shutdown vault
        vault.shutdown()
        
        # Should be empty after shutdown
        assert vault.size() == 0


class TestVaultUtilities:
    """Test vault utility functions."""
    
    def test_create_vault(self):
        """Test create_vault utility function."""
        vault = create_vault(ttl=60.0, max_size=200)
        assert isinstance(vault, ResultVault)
        assert vault.ttl == 60.0
        assert vault.max_size == 200
    
    def test_short_lived_vault(self):
        """Test short_lived_vault utility function."""
        vault = short_lived_vault()
        assert isinstance(vault, ResultVault)
        assert vault.ttl == 60.0
        assert vault.max_size == 500
        
        vault = short_lived_vault(ttl=30.0, max_size=100)
        assert vault.ttl == 30.0
        assert vault.max_size == 100
    
    def test_long_lived_vault(self):
        """Test long_lived_vault utility function."""
        vault = long_lived_vault()
        assert isinstance(vault, ResultVault)
        assert vault.ttl == 3600.0
        assert vault.max_size == 2000
        
        vault = long_lived_vault(ttl=7200.0, max_size=5000)
        assert vault.ttl == 7200.0
        assert vault.max_size == 5000
