"""
tests.unit.infrastructure.test_incremental_updates | Layer: TEST
Tests: BLAKE3 Incremental Update Manager | Target: GAP-005

TDD Phase: RED → Tests written before implementation
Research Reference: README_RESEARCH_3.md, Lines 33-35

Research Targets:
- 90-99% computation savings for incremental updates
- 100-1000x speedup for <1% changes
- 10-50x speedup for 1-10% changes
- BLAKE3 hashing (8-10x faster than SHA-256)
"""

import pytest
import numpy as np
from dataclasses import dataclass

# Check if blake3 is available
try:
    import blake3
    HAS_BLAKE3 = True
except ImportError:
    HAS_BLAKE3 = False


# =============================================================================
# CHANGE DETECTION TESTS
# =============================================================================


class TestContentHashTracker:
    """Test content hash tracking for change detection."""

    def test_tracker_class_exists(self):
        """Test ContentHashTracker class exists."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        assert tracker is not None

    def test_tracker_stores_hash(self):
        """Test tracker stores content hash."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "customer email address")
        
        assert tracker.has_hash("entry_1")
        assert tracker.get_hash("entry_1") is not None

    def test_tracker_returns_none_for_unknown(self):
        """Test tracker returns None for unknown entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        assert tracker.get_hash("unknown_entry") is None

    def test_tracker_detects_change(self):
        """Test tracker detects content changes."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "original content")
        
        # Same content should not be changed
        assert not tracker.has_changed("entry_1", "original content")
        
        # Different content should be changed
        assert tracker.has_changed("entry_1", "modified content")

    def test_tracker_detects_new_entry(self):
        """Test tracker identifies new entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        
        # Unknown entry is always "changed" (new)
        assert tracker.has_changed("new_entry", "some content")

    def test_tracker_uses_blake3(self):
        """Test tracker uses BLAKE3 for hashing."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "test content")
        
        # Hash should be hex string (BLAKE3 default output)
        hash_value = tracker.get_hash("entry_1")
        assert isinstance(hash_value, str)
        assert len(hash_value) == 64  # BLAKE3 produces 256-bit (64 hex chars) by default


class TestChangeDetector:
    """Test change detection for dictionary entries."""

    def test_detector_class_exists(self):
        """Test ChangeDetector class exists."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeDetector,
        )
        
        detector = ChangeDetector()
        assert detector is not None

    def test_detector_finds_added_entries(self):
        """Test detector identifies newly added entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeDetector,
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        # Track existing entries
        tracker.track("entry_1", "content 1")
        tracker.track("entry_2", "content 2")
        
        # New entries to check
        new_entries = {
            "entry_1": "content 1",
            "entry_2": "content 2",
            "entry_3": "content 3",  # New!
        }
        
        detector = ChangeDetector(tracker)
        changes = detector.detect_changes(new_entries)
        
        assert "entry_3" in changes.added
        assert len(changes.added) == 1

    def test_detector_finds_modified_entries(self):
        """Test detector identifies modified entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeDetector,
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "original content")
        tracker.track("entry_2", "content 2")
        
        new_entries = {
            "entry_1": "modified content",  # Changed!
            "entry_2": "content 2",
        }
        
        detector = ChangeDetector(tracker)
        changes = detector.detect_changes(new_entries)
        
        assert "entry_1" in changes.modified
        assert len(changes.modified) == 1

    def test_detector_finds_deleted_entries(self):
        """Test detector identifies deleted entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeDetector,
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "content 1")
        tracker.track("entry_2", "content 2")
        tracker.track("entry_3", "content 3")
        
        # entry_3 is now missing
        new_entries = {
            "entry_1": "content 1",
            "entry_2": "content 2",
        }
        
        detector = ChangeDetector(tracker)
        changes = detector.detect_changes(new_entries)
        
        assert "entry_3" in changes.deleted
        assert len(changes.deleted) == 1

    def test_detector_finds_unchanged_entries(self):
        """Test detector identifies unchanged entries."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeDetector,
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "content 1")
        tracker.track("entry_2", "content 2")
        
        new_entries = {
            "entry_1": "content 1",
            "entry_2": "content 2",
        }
        
        detector = ChangeDetector(tracker)
        changes = detector.detect_changes(new_entries)
        
        assert "entry_1" in changes.unchanged
        assert "entry_2" in changes.unchanged
        assert len(changes.unchanged) == 2


class TestChangeResult:
    """Test change detection result object."""

    def test_change_result_exists(self):
        """Test ChangeResult class exists."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeResult,
        )
        
        result = ChangeResult(
            added={"a"},
            modified={"b"},
            deleted={"c"},
            unchanged={"d"},
        )
        assert result is not None

    def test_change_result_properties(self):
        """Test ChangeResult computed properties."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeResult,
        )
        
        result = ChangeResult(
            added={"a", "b"},
            modified={"c"},
            deleted={"d"},
            unchanged={"e", "f", "g"},
        )
        
        assert result.total_changed == 4  # 2 added + 1 modified + 1 deleted
        assert result.total_unchanged == 3
        assert result.change_ratio == pytest.approx(4 / 7)

    def test_change_result_savings_estimate(self):
        """Test ChangeResult estimates computation savings."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ChangeResult,
        )
        
        # 5% change rate should give ~95% savings
        result = ChangeResult(
            added=set(range(5)),
            modified=set(),
            deleted=set(),
            unchanged=set(range(5, 100)),
        )
        
        # Savings = unchanged / total
        assert result.estimated_savings_pct == pytest.approx(95.0)


# =============================================================================
# INCREMENTAL UPDATE MANAGER TESTS
# =============================================================================


class TestIncrementalUpdateManager:
    """Test incremental update manager."""

    def test_manager_class_exists(self):
        """Test IncrementalUpdateManager class exists."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        assert manager is not None

    def test_manager_processes_initial_load(self):
        """Test manager handles initial load (all entries are new)."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        
        entries = {
            "entry_1": "content 1",
            "entry_2": "content 2",
            "entry_3": "content 3",
        }
        
        result = manager.compute_changes(entries)
        
        # All entries should be added on first load
        assert len(result.changes.added) == 3
        assert len(result.changes.modified) == 0
        assert len(result.changes.deleted) == 0

    def test_manager_tracks_after_apply(self):
        """Test manager tracks entries after apply."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        
        entries = {"entry_1": "content 1"}
        result = manager.compute_changes(entries)
        manager.apply_changes(result)
        
        # Now entry_1 should be tracked
        result2 = manager.compute_changes(entries)
        assert len(result2.changes.unchanged) == 1
        assert len(result2.changes.added) == 0

    def test_manager_detects_incremental_changes(self):
        """Test manager detects incremental changes."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        
        # Initial load
        initial = {
            "entry_1": "content 1",
            "entry_2": "content 2",
        }
        result1 = manager.compute_changes(initial)
        manager.apply_changes(result1)
        
        # Incremental update
        updated = {
            "entry_1": "content 1",  # Unchanged
            "entry_2": "modified content",  # Modified
            "entry_3": "new content",  # Added
        }
        result2 = manager.compute_changes(updated)
        
        assert "entry_1" in result2.changes.unchanged
        assert "entry_2" in result2.changes.modified
        assert "entry_3" in result2.changes.added

    def test_manager_returns_entries_to_process(self):
        """Test manager returns only entries that need processing."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        
        # Initial load with 100 entries
        initial = {f"entry_{i}": f"content {i}" for i in range(100)}
        result1 = manager.compute_changes(initial)
        manager.apply_changes(result1)
        
        # Update with 5% changes
        updated = initial.copy()
        updated["entry_0"] = "modified content 0"
        updated["entry_1"] = "modified content 1"
        updated["entry_2"] = "modified content 2"
        updated["entry_3"] = "modified content 3"
        updated["entry_4"] = "modified content 4"
        
        result2 = manager.compute_changes(updated)
        
        # Should only need to process 5 entries
        assert len(result2.entries_to_process) == 5
        assert result2.changes.estimated_savings_pct == pytest.approx(95.0)


class TestUpdateResult:
    """Test update result object."""

    def test_update_result_exists(self):
        """Test UpdateResult class exists."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            UpdateResult,
            ChangeResult,
        )
        
        changes = ChangeResult(
            added={"a"},
            modified=set(),
            deleted=set(),
            unchanged={"b", "c"},
        )
        
        result = UpdateResult(
            changes=changes,
            entries_to_process={"a": "content a"},
        )
        
        assert result is not None
        assert result.changes == changes
        assert "a" in result.entries_to_process

    def test_update_result_has_statistics(self):
        """Test UpdateResult includes statistics."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            UpdateResult,
            ChangeResult,
        )
        
        changes = ChangeResult(
            added=set(range(5)),
            modified=set(range(5, 10)),
            deleted=set(range(10, 12)),
            unchanged=set(range(12, 100)),
        )
        
        result = UpdateResult(
            changes=changes,
            entries_to_process={str(i): f"content {i}" for i in range(10)},
        )
        
        assert result.added_count == 5
        assert result.modified_count == 5
        assert result.deleted_count == 2
        assert result.unchanged_count == 88
        assert result.process_count == 10


# =============================================================================
# PERSISTENCE TESTS
# =============================================================================


class TestTrackerPersistence:
    """Test tracker persistence for cross-session tracking."""

    def test_tracker_can_serialize(self):
        """Test tracker can serialize state."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        tracker = ContentHashTracker()
        tracker.track("entry_1", "content 1")
        tracker.track("entry_2", "content 2")
        
        state = tracker.to_dict()
        
        assert isinstance(state, dict)
        assert "entry_1" in state
        assert "entry_2" in state

    def test_tracker_can_deserialize(self):
        """Test tracker can deserialize state."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
        )
        
        # Create and serialize
        tracker1 = ContentHashTracker()
        tracker1.track("entry_1", "content 1")
        state = tracker1.to_dict()
        
        # Deserialize into new tracker
        tracker2 = ContentHashTracker.from_dict(state)
        
        assert tracker2.has_hash("entry_1")
        assert not tracker2.has_changed("entry_1", "content 1")

    def test_manager_can_save_and_load(self):
        """Test manager can save and load state."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        import tempfile
        import os
        
        manager1 = IncrementalUpdateManager()
        entries = {"entry_1": "content 1", "entry_2": "content 2"}
        result = manager1.compute_changes(entries)
        manager1.apply_changes(result)
        
        # Save to temp file
        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
            temp_path = f.name
        
        try:
            manager1.save_state(temp_path)
            
            # Load into new manager
            manager2 = IncrementalUpdateManager.load_state(temp_path)
            
            # Should recognize existing entries
            result2 = manager2.compute_changes(entries)
            assert len(result2.changes.unchanged) == 2
            assert len(result2.changes.added) == 0
        finally:
            os.unlink(temp_path)


# =============================================================================
# PERFORMANCE TESTS
# =============================================================================


class TestIncrementalPerformance:
    """Test incremental update performance."""

    def test_change_detection_is_fast(self):
        """Test change detection is fast for large datasets."""
        import time
        from nexus_matcher.infrastructure.adapters.incremental import (
            IncrementalUpdateManager,
        )
        
        manager = IncrementalUpdateManager()
        
        # Initial load with 10,000 entries
        entries = {f"entry_{i}": f"content {i}" for i in range(10_000)}
        result1 = manager.compute_changes(entries)
        manager.apply_changes(result1)
        
        # Time the change detection
        start = time.perf_counter()
        
        # Update with 1% changes (100 entries)
        updated = entries.copy()
        for i in range(100):
            updated[f"entry_{i}"] = f"modified content {i}"
        
        result2 = manager.compute_changes(updated)
        
        elapsed_ms = (time.perf_counter() - start) * 1000
        
        # Performance threshold depends on hash algorithm
        # BLAKE3: < 500ms, SHA256 fallback: < 2000ms (4x slower is acceptable)
        max_time_ms = 500 if HAS_BLAKE3 else 2000
        assert elapsed_ms < max_time_ms, f"Change detection took {elapsed_ms:.2f}ms (limit: {max_time_ms}ms)"
        assert result2.changes.estimated_savings_pct == pytest.approx(99.0)


# =============================================================================
# EXPORT TESTS
# =============================================================================


class TestIncrementalExports:
    """Test module exports are correct."""

    def test_exports_from_init(self):
        """Test classes are exported from adapters package."""
        from nexus_matcher.infrastructure.adapters.incremental import (
            ContentHashTracker,
            ChangeDetector,
            ChangeResult,
            IncrementalUpdateManager,
            UpdateResult,
        )
        
        assert ContentHashTracker is not None
        assert ChangeDetector is not None
        assert ChangeResult is not None
        assert IncrementalUpdateManager is not None
        assert UpdateResult is not None
