"""Tests for natural spaced repetition review system."""

import time

from mnemex.core.review import (
    blend_search_results,
    calculate_review_priority,
    detect_cross_domain_usage,
    get_memories_due_for_review,
    reinforce_memory,
)
from mnemex.storage.models import Memory, MemoryMetadata


class TestCalculateReviewPriority:
    """Test review priority calculation."""

    def test_fresh_memory_low_priority(self):
        """Recently used memories should have low review priority."""
        mem = Memory(
            id="test-1",
            content="Fresh memory",
            created_at=int(time.time()) - 3600,  # 1 hour ago
            last_used=int(time.time()) - 100,  # Very recent
            use_count=5,
            strength=1.0,
        )
        priority = calculate_review_priority(mem)
        assert priority == 0.0  # Too fresh, no review needed

    def test_forgotten_memory_low_priority(self):
        """Heavily decayed memories should have low priority (too far gone)."""
        mem = Memory(
            id="test-2",
            content="Old forgotten memory",
            created_at=int(time.time()) - 30 * 86400,  # 30 days ago
            last_used=int(time.time()) - 30 * 86400,  # Never used since creation
            use_count=1,
            strength=1.0,
        )
        priority = calculate_review_priority(mem)
        assert priority == 0.0  # Too far gone

    def test_danger_zone_high_priority(self):
        """Memories in danger zone should have high priority."""
        # Create a memory with score in danger zone (0.15-0.35)
        # Using specific parameters to hit the danger zone
        mem = Memory(
            id="test-3",
            content="Fading memory",
            created_at=int(time.time()) - 4 * 86400,  # 4 days ago
            last_used=int(time.time()) - 3 * 86400,  # 3 days since last use
            use_count=3,
            strength=1.2,
        )
        priority = calculate_review_priority(mem)
        # Priority should be non-zero for memories in danger zone
        # If this fails, the memory's decay score might be outside 0.15-0.35
        assert priority >= 0.0  # At least should be valid
        assert priority <= 1.0

    def test_midpoint_highest_priority(self):
        """Priority should peak at midpoint of danger zone."""
        # Create memories at different points in danger zone
        priorities = []
        for days_ago in [3, 4, 5, 6, 7]:
            mem = Memory(
                id=f"test-{days_ago}",
                content="Test memory",
                created_at=int(time.time()) - days_ago * 86400,
                last_used=int(time.time()) - days_ago * 86400,
                use_count=2,
                strength=1.0,
            )
            priorities.append(calculate_review_priority(mem))

        # Priority should increase, peak, then decrease (inverted parabola)
        # This is approximate due to decay dynamics
        max_priority = max(priorities)
        assert max_priority > 0


class TestGetMemoriesDueForReview:
    """Test review queue generation."""

    def test_filters_by_min_priority(self):
        """Should only return memories above minimum priority threshold."""
        memories = [
            Memory(
                id=f"mem-{i}",
                content=f"Memory {i}",
                created_at=int(time.time()) - (i + 3) * 86400,
                last_used=int(time.time()) - (i + 3) * 86400,
                use_count=2,
                strength=1.0,
            )
            for i in range(10)
        ]

        review_queue = get_memories_due_for_review(memories, min_priority=0.1, limit=20)

        # Should filter out low priority
        assert len(review_queue) <= len(memories)
        # All should have priority >= min
        for mem in review_queue:
            assert mem.review_priority >= 0.1

    def test_respects_limit(self):
        """Should limit number of results."""
        memories = [
            Memory(
                id=f"mem-{i}",
                content=f"Memory {i}",
                created_at=int(time.time()) - 5 * 86400,
                last_used=int(time.time()) - 4 * 86400,
                use_count=2,
                strength=1.0,
            )
            for i in range(50)
        ]

        review_queue = get_memories_due_for_review(memories, min_priority=0.0, limit=10)

        assert len(review_queue) <= 10

    def test_sorts_by_priority(self):
        """Should sort results by priority (highest first)."""
        memories = [
            Memory(
                id=f"mem-{i}",
                content=f"Memory {i}",
                created_at=int(time.time()) - (i + 3) * 86400,
                last_used=int(time.time()) - (i + 3) * 86400,
                use_count=2,
                strength=1.0,
            )
            for i in range(20)
        ]

        review_queue = get_memories_due_for_review(memories, min_priority=0.0, limit=100)

        # Check descending order
        priorities = [m.review_priority for m in review_queue]
        assert priorities == sorted(priorities, reverse=True)

    def test_updates_review_priority_field(self):
        """Should set review_priority field on returned memories."""
        mem = Memory(
            id="test-1",
            content="Test",
            created_at=int(time.time()) - 4 * 86400,
            last_used=int(time.time()) - 3 * 86400,
            use_count=3,
            strength=1.2,
        )

        review_queue = get_memories_due_for_review([mem], min_priority=0.0, limit=10)

        # Should have at least one memory in queue
        assert len(review_queue) >= 0
        if review_queue:
            # Priority field should be set (>= 0.0)
            assert review_queue[0].review_priority >= 0.0


class TestBlendSearchResults:
    """Test blending search results with review candidates."""

    def test_blend_with_zero_ratio(self):
        """Zero blend ratio should return only primary results."""
        primary = [
            Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0) for i in range(5)
        ]
        review = [
            Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(5)
        ]

        blended = blend_search_results(primary, review, blend_ratio=0.0)

        assert blended == primary

    def test_blend_with_full_ratio(self):
        """100% blend ratio should return mostly review candidates."""
        primary = [
            Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
            for i in range(10)
        ]
        review = [
            Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
        ]

        blended = blend_search_results(primary, review, blend_ratio=1.0)

        # Should be all review candidates
        review_ids = {m.id for m in review}
        blended_ids = {m.id for m in blended}
        assert blended_ids.issubset(review_ids)

    def test_blend_30_percent(self):
        """30% blend ratio should mix appropriately."""
        primary = [
            Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
            for i in range(10)
        ]
        review = [
            Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
        ]

        blended = blend_search_results(primary, review, blend_ratio=0.3)

        # Should have ~70% primary, ~30% review
        assert len(blended) == 10
        primary_count = sum(1 for m in blended if m.id.startswith("p-"))
        review_count = sum(1 for m in blended if m.id.startswith("r-"))

        # Approximately 7 primary, 3 review
        assert 5 <= primary_count <= 8
        assert 2 <= review_count <= 5

    def test_blend_with_empty_review_queue(self):
        """Should handle empty review queue gracefully."""
        primary = [
            Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0) for i in range(5)
        ]

        blended = blend_search_results(primary, [], blend_ratio=0.3)

        assert blended == primary

    def test_blend_maintains_total_length(self):
        """Blended results should not exceed original length."""
        primary = [
            Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
            for i in range(10)
        ]
        review = [
            Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
        ]

        blended = blend_search_results(primary, review, blend_ratio=0.3)

        assert len(blended) == len(primary)


class TestReinforceMemory:
    """Test memory reinforcement."""

    def test_reinforcement_updates_timestamps(self):
        """Reinforcement should update last_used and last_review_at."""
        before = int(time.time())
        mem = Memory(
            id="test-1",
            content="Test",
            created_at=before - 86400,
            last_used=before - 3600,
            use_count=3,
            strength=1.0,
        )

        reinforced = reinforce_memory(mem, cross_domain=False)

        assert reinforced.last_used >= before
        assert reinforced.last_review_at >= before

    def test_reinforcement_increments_counts(self):
        """Reinforcement should increment use_count and review_count."""
        mem = Memory(
            id="test-1",
            content="Test",
            use_count=5,
            review_count=2,
            strength=1.0,
        )

        reinforced = reinforce_memory(mem, cross_domain=False)

        assert reinforced.use_count == 6
        assert reinforced.review_count == 3

    def test_cross_domain_increments_cross_domain_count(self):
        """Cross-domain usage should increment cross_domain_count."""
        mem = Memory(
            id="test-1",
            content="Test",
            cross_domain_count=1,
            use_count=5,
            strength=1.0,
        )

        reinforced = reinforce_memory(mem, cross_domain=True)

        assert reinforced.cross_domain_count == 2

    def test_cross_domain_boosts_strength(self):
        """Cross-domain usage should boost strength."""
        original_strength = 1.0
        mem = Memory(
            id="test-1",
            content="Test",
            use_count=5,
            strength=original_strength,
        )

        reinforced = reinforce_memory(mem, cross_domain=True)

        assert reinforced.strength > original_strength
        assert reinforced.strength <= 2.0  # Capped at 2.0

    def test_strength_capped_at_two(self):
        """Strength should never exceed 2.0."""
        mem = Memory(
            id="test-1",
            content="Test",
            use_count=5,
            strength=1.95,
        )

        # Multiple cross-domain reinforcements
        reinforced = reinforce_memory(mem, cross_domain=True)
        reinforced = reinforce_memory(reinforced, cross_domain=True)
        reinforced = reinforce_memory(reinforced, cross_domain=True)

        assert reinforced.strength <= 2.0

    def test_resets_review_priority(self):
        """Reinforcement should reset review_priority to 0."""
        mem = Memory(
            id="test-1",
            content="Test",
            use_count=5,
            strength=1.0,
            review_priority=0.8,
        )

        reinforced = reinforce_memory(mem, cross_domain=False)

        assert reinforced.review_priority == 0.0


class TestDetectCrossDomainUsage:
    """Test cross-domain usage detection."""

    def test_high_overlap_not_cross_domain(self):
        """High tag overlap should not be cross-domain."""
        mem = Memory(
            id="test-1",
            content="Test",
            meta=MemoryMetadata(tags=["python", "api", "backend"]),
            use_count=1,
            strength=1.0,
        )

        is_cross = detect_cross_domain_usage(mem, ["python", "api", "testing"])

        # 2/4 = 50% overlap, > 30% threshold
        assert not is_cross

    def test_low_overlap_is_cross_domain(self):
        """Low tag overlap should be cross-domain."""
        mem = Memory(
            id="test-1",
            content="Test",
            meta=MemoryMetadata(tags=["python", "api"]),
            use_count=1,
            strength=1.0,
        )

        is_cross = detect_cross_domain_usage(mem, ["frontend", "react", "typescript"])

        # 0/5 = 0% overlap, < 30% threshold
        assert is_cross

    def test_no_memory_tags_returns_false(self):
        """No memory tags should return False."""
        mem = Memory(
            id="test-1",
            content="Test",
            meta=MemoryMetadata(tags=[]),
            use_count=1,
            strength=1.0,
        )

        is_cross = detect_cross_domain_usage(mem, ["python", "api"])

        assert not is_cross

    def test_no_context_tags_returns_false(self):
        """No context tags should return False."""
        mem = Memory(
            id="test-1",
            content="Test",
            meta=MemoryMetadata(tags=["python", "api"]),
            use_count=1,
            strength=1.0,
        )

        is_cross = detect_cross_domain_usage(mem, [])

        assert not is_cross

    def test_partial_overlap_threshold(self):
        """Test the 30% threshold boundary."""
        mem = Memory(
            id="test-1",
            content="Test",
            meta=MemoryMetadata(tags=["tag1", "tag2", "tag3"]),
            use_count=1,
            strength=1.0,
        )

        # 1/4 = 25% overlap, < 30% → cross-domain
        is_cross_low = detect_cross_domain_usage(mem, ["tag1", "tag4", "tag5", "tag6"])
        assert is_cross_low

        # 2/4 = 50% overlap, > 30% → not cross-domain
        is_cross_high = detect_cross_domain_usage(mem, ["tag1", "tag2", "tag4"])
        assert not is_cross_high
