"""
Tests for ForgettingCurve (M.2)

Tests: decay calculation, half-life by importance, archive/deprioritize thresholds.
"""

import pytest
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock

from app.services.memory.forgetting_curve import ForgettingCurve


def create_mock_memory(
    last_accessed_at=None,
    last_recalled_at=None,
    importance_level: str = "medium",
):
    """Create a mock UserMemory for testing."""
    memory = MagicMock()
    memory.last_accessed_at = last_accessed_at
    memory.last_recalled_at = last_recalled_at
    memory.importance_level = importance_level
    memory.decay_score = 1.0
    memory.is_archived = False
    return memory


class TestForgettingCurveCalculateDecay:
    """Test decay score calculation"""

    def test_fresh_memory_full_retention(self):
        """Never accessed memory returns full retention (1.0)."""
        curve = ForgettingCurve()
        memory = create_mock_memory(last_accessed_at=None, last_recalled_at=None)

        decay = curve.calculate_decay(memory)

        assert decay == 1.0

    def test_just_accessed_high_retention(self):
        """Recently accessed memory has high retention."""
        curve = ForgettingCurve()
        recent = datetime.now(UTC) - timedelta(hours=1)
        memory = create_mock_memory(last_accessed_at=recent)

        decay = curve.calculate_decay(memory)

        assert decay > 0.95

    def test_30_days_medium_decay(self):
        """~30 days old memory should have ~0.5 decay for medium importance."""
        curve = ForgettingCurve()
        old = datetime.now(UTC) - timedelta(days=30)
        memory = create_mock_memory(last_accessed_at=old, importance_level="medium")

        decay = curve.calculate_decay(memory)

        # exp(-30/30) = exp(-1) ≈ 0.368, but capped at min 0.0 max 1.0
        assert 0.3 < decay < 0.5

    def test_90_days_high_importance_slower_decay(self):
        """High importance memory decays slower - 90 days should still be > 0.3."""
        curve = ForgettingCurve()
        old = datetime.now(UTC) - timedelta(days=90)
        memory = create_mock_memory(last_accessed_at=old, importance_level="high")

        decay = curve.calculate_decay(memory)

        # exp(-90/90) = exp(-1) ≈ 0.368 for high importance (half_life = 90)
        assert 0.3 < decay < 0.5

    def test_90_days_low_importance_faster_decay(self):
        """Low importance memory decays faster - 90 days should be near 0."""
        curve = ForgettingCurve()
        old = datetime.now(UTC) - timedelta(days=90)
        memory = create_mock_memory(last_accessed_at=old, importance_level="low")

        decay = curve.calculate_decay(memory)

        # exp(-90/15) = exp(-6) ≈ 0.0025
        assert decay < 0.1

    def test_uses_last_recalled_at_if_last_accessed_missing(self):
        """Falls back to last_recalled_at when last_accessed_at is None."""
        curve = ForgettingCurve()
        recent = datetime.now(UTC) - timedelta(hours=2)
        memory = create_mock_memory(last_accessed_at=None, last_recalled_at=recent)

        decay = curve.calculate_decay(memory)

        assert decay > 0.9

    def test_naive_datetime_converted_to_utc(self):
        """Naive datetime (no tzinfo) should be converted to UTC."""
        curve = ForgettingCurve()
        recent = datetime.now() - timedelta(hours=1)  # naive
        memory = create_mock_memory(last_accessed_at=recent)

        decay = curve.calculate_decay(memory)

        assert decay > 0.9

    def test_decay_capped_at_one(self):
        """Decay score should never exceed 1.0."""
        curve = ForgettingCurve()
        very_recent = datetime.now(UTC) + timedelta(hours=1)  # future
        memory = create_mock_memory(last_accessed_at=very_recent)

        decay = curve.calculate_decay(memory)

        assert decay <= 1.0

    def test_decay_never_negative(self):
        """Decay score should never go below 0.0."""
        curve = ForgettingCurve()
        very_old = datetime.now(UTC) - timedelta(days=1000)
        memory = create_mock_memory(last_accessed_at=very_old)

        decay = curve.calculate_decay(memory)

        assert decay >= 0.0


class TestForgettingCurveHalfLife:
    """Test half-life calculation by importance level."""

    def test_high_importance_half_life_90_days(self):
        """High importance: half_life = 30 * 3 = 90 days."""
        curve = ForgettingCurve()
        memory = create_mock_memory(importance_level="high")

        half_life = curve.get_half_life(memory)

        assert half_life == 90.0

    def test_medium_importance_half_life_30_days(self):
        """Medium importance: half_life = 30 * 1 = 30 days."""
        curve = ForgettingCurve()
        memory = create_mock_memory(importance_level="medium")

        half_life = curve.get_half_life(memory)

        assert half_life == 30.0

    def test_low_importance_half_life_15_days(self):
        """Low importance: half_life = 30 * 0.5 = 15 days."""
        curve = ForgettingCurve()
        memory = create_mock_memory(importance_level="low")

        half_life = curve.get_half_life(memory)

        assert half_life == 15.0

    def test_unknown_importance_defaults_to_medium(self):
        """Unknown importance level defaults to medium multiplier (1.0)."""
        curve = ForgettingCurve()
        memory = create_mock_memory(importance_level="unknown")

        half_life = curve.get_half_life(memory)

        assert half_life == 30.0


class TestForgettingCurveShouldArchive:
    """Test archive threshold (decay < 0.2)."""

    def test_high_decay_not_archived(self):
        """Memory with high decay score (> 0.2) should NOT be archived."""
        curve = ForgettingCurve()
        recent = datetime.now(UTC) - timedelta(days=5)
        memory = create_mock_memory(last_accessed_at=recent)

        should = curve.should_archive(memory)

        assert should is False

    def test_low_decay_archived(self):
        """Memory with decay < 0.2 should be archived."""
        curve = ForgettingCurve()
        # ~100 days for medium importance: exp(-100/30) ≈ 0.035 < 0.2
        old = datetime.now(UTC) - timedelta(days=100)
        memory = create_mock_memory(last_accessed_at=old, importance_level="medium")

        should = curve.should_archive(memory)

        assert should is True

    def test_boundary_decay_not_archived(self):
        """At exactly 0.2 decay, should NOT be archived (strict < 0.2)."""
        curve = ForgettingCurve()
        # Create memory with known decay = 0.2
        memory = create_mock_memory(importance_level="low")
        memory.last_accessed_at = datetime.now(UTC) - timedelta(days=int(15 * 4.605))  # 69 days

        decay = curve.calculate_decay(memory)
        should = curve.should_archive(memory)

        # exp(-69/15) ≈ 0.010 < 0.2
        assert decay < 0.2
        assert should is True


class TestForgettingCurveShouldDeprioritize:
    """Test deprioritize threshold (decay < 0.5)."""

    def test_high_decay_not_deprioritized(self):
        """Memory with high decay score (> 0.5) should NOT be deprioritized."""
        curve = ForgettingCurve()
        recent = datetime.now(UTC) - timedelta(days=10)
        memory = create_mock_memory(last_accessed_at=recent)

        should = curve.should_deprioritize(memory)

        assert should is False

    def test_medium_decay_deprioritized(self):
        """Memory with decay < 0.5 should be deprioritized."""
        curve = ForgettingCurve()
        # ~42 days for medium: exp(-42/30) ≈ 0.25 < 0.5
        old = datetime.now(UTC) - timedelta(days=42)
        memory = create_mock_memory(last_accessed_at=old, importance_level="medium")

        should = curve.should_deprioritize(memory)

        assert should is True

    def test_boundary_deprioritize_strict(self):
        """At exactly 0.5 decay, should NOT be deprioritized (strict < 0.5)."""
        curve = ForgettingCurve()
        # For high importance: exp(-x/90) = 0.5 → x = 90 * ln(2) ≈ 62.4 days
        memory = create_mock_memory(importance_level="high")
        memory.last_accessed_at = datetime.now(UTC) - timedelta(days=62)

        decay = curve.calculate_decay(memory)
        should = curve.should_deprioritize(memory)

        assert 0.4 < decay < 0.6
        assert should is False


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
