"""Tests for the caching and batch operations module."""

import tempfile
import threading
import time
from pathlib import Path
from unittest.mock import patch

import pytest


class TestLRUCache:
    """Tests for the LRU cache implementation."""

    def test_basic_get_set(self):
        """Test basic get/set operations."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=60)

        cache.set("key1", 100)
        assert cache.get("key1") == 100

    def test_missing_key_returns_none(self):
        """Test that missing keys return None."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=60)

        assert cache.get("nonexistent") is None

    def test_ttl_expiration(self):
        """Test that entries expire after TTL."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=0.1)

        cache.set("key1", 100)
        assert cache.get("key1") == 100

        time.sleep(0.15)
        assert cache.get("key1") is None

    def test_lru_eviction(self):
        """Test that least recently used entries are evicted."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=3, ttl_seconds=60)

        cache.set("key1", 1)
        cache.set("key2", 2)
        cache.set("key3", 3)

        # Access key1 to make it recently used
        cache.get("key1")

        # Add key4, should evict key2 (least recently used)
        cache.set("key4", 4)

        assert cache.get("key1") == 1  # Still present
        assert cache.get("key2") is None  # Evicted
        assert cache.get("key3") == 3  # Still present
        assert cache.get("key4") == 4  # Newly added

    def test_cache_stats(self):
        """Test cache statistics tracking."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=60)

        cache.set("key1", 1)
        cache.get("key1")  # Hit
        cache.get("key1")  # Hit
        cache.get("key2")  # Miss

        stats = cache.stats
        assert stats["hits"] == 2
        assert stats["misses"] == 1
        assert stats["hit_rate"] == 2 / 3

    def test_thread_safety(self):
        """Test that cache is thread-safe."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=100, ttl_seconds=60)
        errors = []

        def writer(cache, start):
            try:
                for i in range(100):
                    cache.set(f"key{start + i}", start + i)
            except Exception as e:
                errors.append(e)

        def reader(cache):
            try:
                for _ in range(100):
                    for i in range(50):
                        cache.get(f"key{i}")
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=writer, args=(cache, 0)),
            threading.Thread(target=writer, args=(cache, 50)),
            threading.Thread(target=reader, args=(cache,)),
            threading.Thread(target=reader, args=(cache,)),
        ]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0

    def test_delete(self):
        """Test delete operation."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=60)

        cache.set("key1", 1)
        assert cache.delete("key1") is True
        assert cache.get("key1") is None
        assert cache.delete("key1") is False  # Already deleted

    def test_clear(self):
        """Test clear operation."""
        from sltk.api.cache import LRUCache

        cache = LRUCache[str, int](max_size=10, ttl_seconds=60)

        cache.set("key1", 1)
        cache.set("key2", 2)
        cache.clear()

        assert len(cache) == 0
        assert cache.get("key1") is None


class TestFileHashing:
    """Tests for file hashing functions."""

    def test_compute_quick_hash(self, tmp_path):
        """Test quick hash computation."""
        from sltk.api.cache import compute_quick_hash

        # Create a test file
        test_file = tmp_path / "test.txt"
        test_file.write_text("test content")

        hash1 = compute_quick_hash(test_file)
        assert isinstance(hash1, str)
        assert len(hash1) == 16  # sha256 truncated

        # Same file, same hash
        hash2 = compute_quick_hash(test_file)
        assert hash1 == hash2

        # Modify file, different hash
        time.sleep(0.01)  # Ensure mtime changes
        test_file.write_text("modified content")
        hash3 = compute_quick_hash(test_file)
        assert hash1 != hash3

    def test_compute_file_hash(self, tmp_path):
        """Test full content hash computation."""
        from sltk.api.cache import compute_file_hash

        test_file = tmp_path / "test.txt"
        test_file.write_text("test content")

        hash1 = compute_file_hash(test_file)
        assert isinstance(hash1, str)
        assert len(hash1) == 64  # sha256 full

        # Same content, same hash
        test_file2 = tmp_path / "test2.txt"
        test_file2.write_text("test content")
        hash2 = compute_file_hash(test_file2)
        assert hash1 == hash2

    def test_file_not_found(self, tmp_path):
        """Test error on missing file."""
        from sltk.api.cache import compute_quick_hash

        with pytest.raises(FileNotFoundError):
            compute_quick_hash(tmp_path / "nonexistent.txt")


class TestSegmentPredictionCache:
    """Tests for segment prediction cache."""

    def test_make_key(self, tmp_path):
        """Test cache key generation."""
        from sltk.api.cache import SegmentPredictionCache

        cache = SegmentPredictionCache()

        # Create test file
        test_file = tmp_path / "features.h5"
        test_file.write_bytes(b"test data")

        key1 = cache.make_key(test_file, model_version="v1.0")
        key2 = cache.make_key(test_file, model_version="v1.0")
        key3 = cache.make_key(test_file, model_version="v2.0")

        assert key1 == key2  # Same inputs
        assert key1 != key3  # Different model version

    def test_cache_predictions(self, tmp_path):
        """Test caching and retrieving predictions."""
        from sltk.api.cache import SegmentPredictionCache

        cache = SegmentPredictionCache()

        test_file = tmp_path / "features.h5"
        test_file.write_bytes(b"test data")

        key = cache.make_key(test_file, model_version="v1.0")
        predictions = [{"start": 0, "end": 1, "label": "A"}]

        cache.set(key, predictions)
        assert cache.get(key) == predictions


class TestMetricCache:
    """Tests for metric cache."""

    def test_make_key(self, tmp_path):
        """Test metric cache key generation."""
        from sltk.api.cache import MetricCache

        cache = MetricCache()

        preds = tmp_path / "preds.eaf"
        refs = tmp_path / "refs.eaf"
        preds.write_text("<eaf></eaf>")
        refs.write_text("<eaf></eaf>")

        key = cache.make_key(
            predictions_path=preds,
            references_path=refs,
            metric_name="segmentation",
            params={"tolerance_sec": 0.1},
        )

        assert key.metric_name == "segmentation"
        assert key.params_hash != ""

    def test_cache_metrics(self, tmp_path):
        """Test caching and retrieving metrics."""
        from sltk.api.cache import MetricCache

        cache = MetricCache()

        preds = tmp_path / "preds.eaf"
        refs = tmp_path / "refs.eaf"
        preds.write_text("<eaf></eaf>")
        refs.write_text("<eaf></eaf>")

        key = cache.make_key(preds, refs, "segmentation")
        metrics = {"f1": 0.85, "precision": 0.90}

        cache.set(key, metrics)
        assert cache.get(key) == metrics


class TestBatchUpdateSegments:
    """Tests for batch segment update function."""

    def test_batch_update_empty(self, tmp_path):
        """Test batch update with empty list."""
        from sltk.api.cache import batch_update_segments

        result = batch_update_segments(
            segments=[],
            output_path=str(tmp_path / "output.eaf"),
        )

        assert result.total == 0
        assert result.successful == 0
        assert result.failed == 0

    def test_batch_update_success(self, tmp_path):
        """Test successful batch update."""
        from sltk.api.cache import batch_update_segments

        segments = [
            {"start": 0.0, "end": 1.0, "label": "A", "tier": "gloss"},
            {"start": 1.5, "end": 2.5, "label": "B", "tier": "gloss"},
            {"start": 3.0, "end": 4.0, "label": "C", "tier": "gloss"},
        ]

        output_path = tmp_path / "output.eaf"

        result = batch_update_segments(
            segments=segments,
            output_path=str(output_path),
            chunk_size=2,
        )

        assert result.total == 3
        assert result.successful == 3
        assert result.failed == 0
        assert result.duration_ms > 0
        assert output_path.exists()

    def test_batch_update_with_errors(self, tmp_path):
        """Test batch update with invalid segments."""
        from sltk.api.cache import batch_update_segments

        segments = [
            {"start": 0.0, "end": 1.0, "label": "A"},
            {"start": 2.0, "end": 1.0, "label": "B"},  # Invalid: end < start
            {"start": 3.0, "end": 4.0, "label": "C"},
        ]

        result = batch_update_segments(
            segments=segments,
            output_path=str(tmp_path / "output.eaf"),
        )

        assert result.total == 3
        assert result.successful == 2
        assert result.failed == 1
        assert len(result.errors) == 1

    def test_batch_update_with_progress(self, tmp_path):
        """Test batch update with progress callback."""
        from sltk.api.cache import batch_update_segments

        progress_calls = []

        def progress_callback(processed, total):
            progress_calls.append((processed, total))

        segments = [{"start": i, "end": i + 1, "label": str(i)} for i in range(10)]

        batch_update_segments(
            segments=segments,
            output_path=str(tmp_path / "output.eaf"),
            chunk_size=3,
            progress_callback=progress_callback,
        )

        assert len(progress_calls) > 0
        assert progress_calls[-1] == (10, 10)


class TestGlobalCaches:
    """Tests for global cache management."""

    def test_get_segment_prediction_cache(self):
        """Test getting global segment prediction cache."""
        from sltk.api.cache import get_segment_prediction_cache

        cache1 = get_segment_prediction_cache()
        cache2 = get_segment_prediction_cache()

        assert cache1 is cache2  # Same instance

    def test_get_metric_cache(self):
        """Test getting global metric cache."""
        from sltk.api.cache import get_metric_cache

        cache1 = get_metric_cache()
        cache2 = get_metric_cache()

        assert cache1 is cache2  # Same instance

    def test_clear_all_caches(self):
        """Test clearing all global caches."""
        from sltk.api.cache import (
            clear_all_caches,
            get_metric_cache,
            get_segment_prediction_cache,
        )

        # Add some entries
        seg_cache = get_segment_prediction_cache()
        metric_cache = get_metric_cache()

        seg_cache._cache.set("test_key", [])
        metric_cache._cache.set("test_key", {})

        # Clear all
        cleared = clear_all_caches()

        assert "segment_predictions" in cleared or "metrics" in cleared

    def test_get_cache_stats(self):
        """Test getting cache statistics."""
        from sltk.api.cache import get_cache_stats

        stats = get_cache_stats()

        # Stats may be empty if caches not initialized
        assert isinstance(stats, dict)


class TestLazySegmentLoader:
    """Tests for lazy segment loader."""

    def test_lazy_loading(self, tmp_path):
        """Test that segments are loaded lazily."""
        from sltk.api.cache import LazySegmentLoader

        # Create test ELAN files
        elan_content = """<?xml version="1.0" encoding="UTF-8"?>
<ANNOTATION_DOCUMENT>
    <HEADER MEDIA_FILE="" TIME_UNITS="milliseconds"/>
    <TIME_ORDER>
        <TIME_SLOT TIME_SLOT_ID="ts1" TIME_VALUE="0"/>
        <TIME_SLOT TIME_SLOT_ID="ts2" TIME_VALUE="1000"/>
    </TIME_ORDER>
    <TIER LINGUISTIC_TYPE_REF="default-lt" TIER_ID="default">
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a1" TIME_SLOT_REF1="ts1" TIME_SLOT_REF2="ts2">
                <ANNOTATION_VALUE>test</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
    </TIER>
    <LINGUISTIC_TYPE LINGUISTIC_TYPE_ID="default-lt"/>
</ANNOTATION_DOCUMENT>
"""

        files = []
        for i in range(3):
            f = tmp_path / f"test_{i}.eaf"
            f.write_text(elan_content)
            files.append(str(f))

        loader = LazySegmentLoader(paths=files)

        assert len(loader) == 3
        assert len(loader._loaded) == 0  # Nothing loaded yet

        # Access first file
        _ = loader[0]
        assert len(loader._loaded) == 1

    def test_unload(self, tmp_path):
        """Test unloading segments."""
        from sltk.api.cache import LazySegmentLoader

        elan_content = """<?xml version="1.0" encoding="UTF-8"?>
<ANNOTATION_DOCUMENT>
    <HEADER MEDIA_FILE="" TIME_UNITS="milliseconds"/>
    <TIME_ORDER/>
    <LINGUISTIC_TYPE LINGUISTIC_TYPE_ID="default-lt"/>
</ANNOTATION_DOCUMENT>
"""

        f = tmp_path / "test.eaf"
        f.write_text(elan_content)

        loader = LazySegmentLoader(paths=[str(f)])

        # Load
        _ = loader[0]
        assert len(loader._loaded) == 1

        # Unload
        loader.unload()
        assert len(loader._loaded) == 0
