"""Tests for pattern extraction from successful outcomes."""

import pytest
from uuid import uuid4
from datetime import datetime, UTC

from mind.core.federation.models import (
    PatternCandidate,
    PatternType,
    PrivacyBudget,
)
from mind.core.federation.extractor import (
    PatternExtractor,
    ExtractionContext,
    CategoryMapper,
    MIN_OUTCOME_QUALITY,
)
from mind.core.decision.models import DecisionTrace, Outcome


class TestCategoryMapper:
    """Tests for CategoryMapper."""

    @pytest.fixture
    def mapper(self) -> CategoryMapper:
        """Create a category mapper."""
        return CategoryMapper()

    def test_categorize_preference_keywords(self, mapper: CategoryMapper):
        """Should detect preference-related content."""
        assert mapper.categorize("User prefers morning meetings") == "preference"
        assert mapper.categorize("I like quiet environments") == "preference"
        assert mapper.categorize("My favorite tool is vim") == "preference"

    def test_categorize_constraint_keywords(self, mapper: CategoryMapper):
        """Should detect constraint-related content."""
        assert mapper.categorize("Cannot work on weekends") == "constraint"
        assert mapper.categorize("Must not share with competitors") == "constraint"
        assert mapper.categorize("Allergic to shellfish") == "constraint"

    def test_categorize_goal_keywords(self, mapper: CategoryMapper):
        """Should detect goal-related content."""
        assert mapper.categorize("My goal is to improve productivity") == "goal"
        assert mapper.categorize("Objective: reduce meeting time") == "goal"
        # Note: "want" matches preference before goal in keyword ordering
        assert mapper.categorize("The main target is growth") == "goal"

    def test_categorize_context_keywords(self, mapper: CategoryMapper):
        """Should detect context-related content."""
        assert mapper.categorize("Usually works from home") == "context"
        assert mapper.categorize("Typically available after 2pm") == "context"
        assert mapper.categorize("Often travels for work") == "context"

    def test_categorize_identity_keywords(self, mapper: CategoryMapper):
        """Should detect identity-related content."""
        assert mapper.categorize("I am a software engineer") == "identity"
        assert mapper.categorize("My personality is introverted") == "identity"
        # Note: "always" matches identity
        assert mapper.categorize("This is my core value") == "identity"

    def test_categorize_temporal_keywords(self, mapper: CategoryMapper):
        """Should detect temporal-related content."""
        assert mapper.categorize("Best time is morning") == "temporal"
        assert mapper.categorize("Available on weekends") == "temporal"
        # Note: "daily" also contains identity match for "a" - use clear match
        assert mapper.categorize("Weekly schedule is flexible") == "temporal"

    def test_categorize_location_keywords(self, mapper: CategoryMapper):
        """Should detect location-related content."""
        assert mapper.categorize("Works at home") == "location"
        assert mapper.categorize("Currently at work") == "location"
        assert mapper.categorize("Traveling this week") == "location"

    def test_categorize_social_keywords(self, mapper: CategoryMapper):
        """Should detect social-related content."""
        assert mapper.categorize("Meeting with friends") == "social"
        assert mapper.categorize("Works better alone") == "social"
        # Note: "family" also matches identity - use clear match
        assert mapper.categorize("Time with friends is important") == "social"

    def test_categorize_general_fallback(self, mapper: CategoryMapper):
        """Should return 'general' for unmatched content."""
        assert mapper.categorize("Some random text") == "general"
        assert mapper.categorize("xyz123") == "general"

    def test_categorize_case_insensitive(self, mapper: CategoryMapper):
        """Should match regardless of case."""
        assert mapper.categorize("I PREFER tea") == "preference"
        assert mapper.categorize("CANNOT do this") == "constraint"

    def test_categorize_memories_list(self, mapper: CategoryMapper):
        """Should categorize a list of memory contents."""
        contents = [
            "User prefers detailed explanations",
            "Cannot work past 6pm",
            "Goal is to ship the feature",
        ]
        categories = mapper.categorize_memories(contents)

        assert categories == ["preference", "constraint", "goal"]

    def test_categorize_memories_empty_list(self, mapper: CategoryMapper):
        """Should handle empty list."""
        assert mapper.categorize_memories([]) == []


class TestPatternExtractor:
    """Tests for PatternExtractor."""

    @pytest.fixture
    def extractor(self) -> PatternExtractor:
        """Create a pattern extractor."""
        return PatternExtractor()

    @pytest.fixture
    def sample_trace(self) -> DecisionTrace:
        """Create a sample decision trace."""
        return DecisionTrace(
            trace_id=uuid4(),
            user_id=uuid4(),
            session_id=uuid4(),
            memory_ids=[uuid4(), uuid4()],
            memory_scores={"mem1": 0.8, "mem2": 0.6},
            decision_type="scheduling",
            decision_summary="When should I schedule the meeting?",
            confidence=0.85,
        )

    @pytest.fixture
    def positive_outcome(self) -> Outcome:
        """Create a positive outcome."""
        return Outcome(
            trace_id=uuid4(),
            quality=0.8,
            signal="explicit_positive",
            feedback_text="Meeting went well",
        )

    @pytest.fixture
    def negative_outcome(self) -> Outcome:
        """Create a negative outcome."""
        return Outcome(
            trace_id=uuid4(),
            quality=0.1,
            signal="explicit_negative",
            feedback_text="Didn't work out",
        )

    def test_extract_from_positive_outcome(
        self,
        extractor: PatternExtractor,
        sample_trace: DecisionTrace,
        positive_outcome: Outcome,
    ):
        """Should extract pattern from positive outcome."""
        context = ExtractionContext(
            trace=sample_trace,
            outcome=positive_outcome,
            memory_categories=["preference", "temporal"],
        )

        result = extractor.extract_from_outcome(context)

        assert result.is_ok
        candidate = result.value
        assert isinstance(candidate, PatternCandidate)
        assert candidate.pattern_type == PatternType.DECISION_STRATEGY
        assert candidate.trigger_category == "scheduling"

    def test_skip_low_quality_outcome(
        self,
        extractor: PatternExtractor,
        sample_trace: DecisionTrace,
        negative_outcome: Outcome,
    ):
        """Should skip extraction for low quality outcomes."""
        context = ExtractionContext(
            trace=sample_trace,
            outcome=negative_outcome,
            memory_categories=["preference"],
        )

        result = extractor.extract_from_outcome(context)

        assert result.is_ok
        assert result.value is None

    def test_threshold_quality_accepted(
        self,
        extractor: PatternExtractor,
        sample_trace: DecisionTrace,
    ):
        """Should accept outcomes at minimum quality threshold."""
        threshold_outcome = Outcome(
            trace_id=uuid4(),
            quality=MIN_OUTCOME_QUALITY,
            signal="explicit_neutral",
            feedback_text="Just acceptable",
        )
        context = ExtractionContext(
            trace=sample_trace,
            outcome=threshold_outcome,
            memory_categories=["preference"],
        )

        result = extractor.extract_from_outcome(context)

        assert result.is_ok
        assert result.value is not None

    def test_aggregate_observations(
        self,
        extractor: PatternExtractor,
        positive_outcome: Outcome,
    ):
        """Should aggregate observations for same pattern."""
        # Create multiple traces from different users
        traces = [
            DecisionTrace(
                trace_id=uuid4(),
                user_id=uuid4(),
                session_id=uuid4(),
                memory_ids=[],
                memory_scores={},
                decision_type="scheduling",
                decision_summary="Schedule meeting",
                confidence=0.8,
            )
            for _ in range(5)
        ]

        # Extract from each
        for trace in traces:
            context = ExtractionContext(
                trace=trace,
                outcome=positive_outcome,
                memory_categories=["preference", "temporal"],
            )
            extractor.extract_from_outcome(context)

        # Get the candidate
        candidates = extractor._candidates
        assert len(candidates) == 1

        candidate = list(candidates.values())[0]
        assert candidate.observation_count == 5
        assert candidate.user_count == 5

    def test_separate_patterns_for_different_categories(
        self,
        extractor: PatternExtractor,
        sample_trace: DecisionTrace,
        positive_outcome: Outcome,
    ):
        """Should create separate patterns for different category combinations."""
        # First pattern with preference+temporal
        context1 = ExtractionContext(
            trace=sample_trace,
            outcome=positive_outcome,
            memory_categories=["preference", "temporal"],
        )
        extractor.extract_from_outcome(context1)

        # Second pattern with constraint+goal
        context2 = ExtractionContext(
            trace=DecisionTrace(
                trace_id=uuid4(),
                user_id=uuid4(),
                session_id=uuid4(),
                memory_ids=[],
                memory_scores={},
                decision_type="scheduling",
                decision_summary="Another query",
                confidence=0.8,
            ),
            outcome=positive_outcome,
            memory_categories=["constraint", "goal"],
        )
        extractor.extract_from_outcome(context2)

        assert len(extractor._candidates) == 2

    def test_get_ready_patterns_none_ready(
        self,
        extractor: PatternExtractor,
        sample_trace: DecisionTrace,
        positive_outcome: Outcome,
    ):
        """Should return empty when no patterns meet thresholds."""
        # Just one observation - not enough
        context = ExtractionContext(
            trace=sample_trace,
            outcome=positive_outcome,
            memory_categories=["preference"],
        )
        extractor.extract_from_outcome(context)

        ready = extractor.get_ready_patterns()
        assert len(ready) == 0

    def test_get_ready_patterns_with_sufficient_data(self):
        """Should return patterns meeting privacy thresholds."""
        # Use lower thresholds for testing
        budget = PrivacyBudget(min_users=3, min_observations=5)
        extractor = PatternExtractor(privacy_budget=budget)

        # Generate enough observations from different users
        for i in range(5):
            trace = DecisionTrace(
                trace_id=uuid4(),
                user_id=uuid4(),
                session_id=uuid4(),
                memory_ids=[],
                memory_scores={},
                decision_type="scheduling",
                decision_summary="Query",
                confidence=0.8,
            )
            outcome = Outcome(
                trace_id=trace.trace_id,
                quality=0.7,
                signal="explicit_positive",
            )
            context = ExtractionContext(
                trace=trace,
                outcome=outcome,
                memory_categories=["preference"],
            )
            extractor.extract_from_outcome(context)

        ready = extractor.get_ready_patterns()
        assert len(ready) == 1

    def test_abstract_strategy_single_category(
        self,
        extractor: PatternExtractor,
    ):
        """Should create strategy from single category."""
        strategy = extractor._abstract_strategy(["preference"])
        assert strategy == "preference"

    def test_abstract_strategy_multiple_categories(
        self,
        extractor: PatternExtractor,
    ):
        """Should combine categories in strategy."""
        strategy = extractor._abstract_strategy(["preference", "temporal"])
        # Categories are sorted
        assert "preference" in strategy
        assert "temporal" in strategy

    def test_abstract_strategy_duplicate_categories(
        self,
        extractor: PatternExtractor,
    ):
        """Should count duplicate categories."""
        strategy = extractor._abstract_strategy(["preference", "preference", "temporal"])
        assert "preference(2)" in strategy
        assert "temporal" in strategy

    def test_abstract_strategy_empty(
        self,
        extractor: PatternExtractor,
    ):
        """Should return default for empty categories."""
        strategy = extractor._abstract_strategy([])
        assert strategy == "general_context"

    def test_generate_pattern_key_deterministic(
        self,
        extractor: PatternExtractor,
    ):
        """Pattern key should be deterministic."""
        key1 = extractor._generate_pattern_key("scheduling", ["a", "b"])
        key2 = extractor._generate_pattern_key("scheduling", ["b", "a"])  # Order reversed

        # Should be the same (sorted internally)
        assert key1 == key2

    def test_generate_pattern_key_different_types(
        self,
        extractor: PatternExtractor,
    ):
        """Different decision types should produce different keys."""
        key1 = extractor._generate_pattern_key("scheduling", ["preference"])
        key2 = extractor._generate_pattern_key("recommendation", ["preference"])

        assert key1 != key2
