"""Tests for memory consolidation deduplication (issue #105).

Verifies that _maybe_consolidate() skips creating duplicate memories
when a recent memory of the same type already exists for the agent.
"""

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


class TestConsolidationDedup:
    """Test consolidation deduplication behavior."""

    def _make_event(self, event_type, agent_id="agent-test"):
        """Create a mock SparkEvent."""
        event = MagicMock()
        event.event_type = event_type
        event.agent_id = agent_id
        return event

    def _make_memory(self, memory_type, subject, created_at):
        """Create a mock SparkMemory."""
        memory = MagicMock()
        memory.memory_type = memory_type
        memory.subject = subject
        memory.created_at = created_at
        return memory

    def _get_enough_events_for_consolidation(self, num_completions=4, num_failures=0):
        """Return enough events to trigger consolidation (5+ events, 3+ outcomes)."""
        events = []
        # Add some non-outcome events first
        for _ in range(2):
            events.append(self._make_event("task_posted"))
        # Add completion/failure events to trigger pattern (>=0.8 success rate)
        for _ in range(num_completions):
            events.append(self._make_event("task_completed"))
        for _ in range(num_failures):
            events.append(self._make_event("task_failed"))
        return events

    @patch("spark.engine.spark_memory")
    def test_creates_memory_when_none_exists(self, mock_spark_memory):
        """Test that consolidation creates a memory when none exists."""
        from spark.engine import SparkEngine

        # Setup: enough events to trigger pattern memory, no existing memories
        mock_spark_memory.get_events.return_value = self._get_enough_events_for_consolidation(
            num_completions=4
        )
        mock_spark_memory.get_memories.return_value = []  # No existing memories

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should have called store_memory once
        mock_spark_memory.store_memory.assert_called_once()
        call_kwargs = mock_spark_memory.store_memory.call_args.kwargs
        assert call_kwargs["memory_type"] == "pattern"
        assert call_kwargs["subject"] == "agent-test"

    @patch("spark.engine.spark_memory")
    def test_skips_when_recent_memory_exists(self, mock_spark_memory):
        """Test that consolidation SKIPS when a recent memory (< 1 hour) exists."""
        from spark.engine import SparkEngine

        # Setup: enough events to trigger pattern memory
        mock_spark_memory.get_events.return_value = self._get_enough_events_for_consolidation(
            num_completions=4
        )

        # Existing recent pattern memory (30 minutes ago)
        recent_time = (datetime.now(UTC) - timedelta(minutes=30)).isoformat()
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("pattern", "agent-test", recent_time)
        ]

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should NOT have called store_memory
        mock_spark_memory.store_memory.assert_not_called()

    @patch("spark.engine.spark_memory")
    def test_creates_when_existing_memory_is_old(self, mock_spark_memory):
        """Test that consolidation creates new memory when existing is old (> 1 hour)."""
        from spark.engine import SparkEngine

        # Setup: enough events to trigger pattern memory
        mock_spark_memory.get_events.return_value = self._get_enough_events_for_consolidation(
            num_completions=4
        )

        # Existing old pattern memory (2 hours ago)
        old_time = (datetime.now(UTC) - timedelta(hours=2)).isoformat()
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("pattern", "agent-test", old_time)
        ]

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should have called store_memory (old memory doesn't block)
        mock_spark_memory.store_memory.assert_called_once()
        call_kwargs = mock_spark_memory.store_memory.call_args.kwargs
        assert call_kwargs["memory_type"] == "pattern"

    @patch("spark.engine.spark_memory")
    def test_pattern_vs_anomaly_type_distinction(self, mock_spark_memory):
        """Test that pattern vs anomaly type distinction works correctly."""
        from spark.engine import SparkEngine

        # Setup: events that trigger anomaly (low success rate: 1/4 = 0.25)
        events = []
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_completed"))  # 1 success
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))  # 3 failures

        mock_spark_memory.get_events.return_value = events
        mock_spark_memory.get_memories.return_value = []

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should create an anomaly memory
        mock_spark_memory.store_memory.assert_called_once()
        call_kwargs = mock_spark_memory.store_memory.call_args.kwargs
        assert call_kwargs["memory_type"] == "anomaly"
        assert "struggling" in call_kwargs["content"]

    @patch("spark.engine.spark_memory")
    def test_anomaly_skips_when_recent_anomaly_exists(self, mock_spark_memory):
        """Test that anomaly consolidation skips when recent anomaly exists."""
        from spark.engine import SparkEngine

        # Setup: events that trigger anomaly
        events = []
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_completed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))

        mock_spark_memory.get_events.return_value = events

        # Existing recent anomaly memory
        recent_time = (datetime.now(UTC) - timedelta(minutes=30)).isoformat()
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("anomaly", "agent-test", recent_time)
        ]

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should NOT have called store_memory
        mock_spark_memory.store_memory.assert_not_called()

    @patch("spark.engine.spark_memory")
    def test_pattern_does_not_skip_on_recent_anomaly(self, mock_spark_memory):
        """Test that pattern consolidation still runs when recent anomaly exists."""
        from spark.engine import SparkEngine

        # Setup: events that trigger pattern (high success rate)
        mock_spark_memory.get_events.return_value = self._get_enough_events_for_consolidation(
            num_completions=4
        )

        # Existing recent anomaly memory (different type)
        recent_time = (datetime.now(UTC) - timedelta(minutes=30)).isoformat()
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("anomaly", "agent-test", recent_time)
        ]

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should create pattern memory (different type, so no dedup)
        mock_spark_memory.store_memory.assert_called_once()
        call_kwargs = mock_spark_memory.store_memory.call_args.kwargs
        assert call_kwargs["memory_type"] == "pattern"

    @patch("spark.engine.spark_memory")
    def test_anomaly_does_not_skip_on_recent_pattern(self, mock_spark_memory):
        """Test that anomaly consolidation still runs when recent pattern exists."""
        from spark.engine import SparkEngine

        # Setup: events that trigger anomaly
        events = []
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_completed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))

        mock_spark_memory.get_events.return_value = events

        # Existing recent pattern memory (different type)
        recent_time = (datetime.now(UTC) - timedelta(minutes=30)).isoformat()
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("pattern", "agent-test", recent_time)
        ]

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should create anomaly memory (different type, so no dedup)
        mock_spark_memory.store_memory.assert_called_once()
        call_kwargs = mock_spark_memory.store_memory.call_args.kwargs
        assert call_kwargs["memory_type"] == "anomaly"

    @patch("spark.engine.spark_memory")
    def test_no_consolidation_for_neutral_success_rate(self, mock_spark_memory):
        """Test that neutral success rates (0.3 < rate < 0.8) don't create memories."""
        from spark.engine import SparkEngine

        # Setup: events that result in neutral success rate (2/4 = 0.5)
        events = []
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_posted"))
        events.append(self._make_event("task_completed"))
        events.append(self._make_event("task_completed"))
        events.append(self._make_event("task_failed"))
        events.append(self._make_event("task_failed"))

        mock_spark_memory.get_events.return_value = events

        engine = SparkEngine(enabled=True)
        engine._maybe_consolidate("agent-test")

        # Should NOT have called store_memory or get_memories
        mock_spark_memory.get_memories.assert_not_called()
        mock_spark_memory.store_memory.assert_not_called()

    @patch("spark.engine.spark_memory")
    def test_handles_invalid_created_at_gracefully(self, mock_spark_memory):
        """Test that invalid created_at timestamps don't crash dedup."""
        from spark.engine import SparkEngine

        # Setup: enough events to trigger pattern memory
        mock_spark_memory.get_events.return_value = self._get_enough_events_for_consolidation(
            num_completions=4
        )

        # Existing memory with invalid created_at
        mock_spark_memory.get_memories.return_value = [
            self._make_memory("pattern", "agent-test", "invalid-timestamp")
        ]

        engine = SparkEngine(enabled=True)
        # Should not raise an exception
        engine._maybe_consolidate("agent-test")

        # Should still create new memory (invalid timestamp ignored)
        mock_spark_memory.store_memory.assert_called_once()
