"""Tests for R-308 extraction filter pipeline bug fixes.

Covers:
- P0: Conflicting BLOCKED_PREDICATES removed
- P0: Tautology filter reflexive check tightened
- P1: cap_engineering_claims max=8
- P1: Category-aware subject filtering
- P2: Priority-sort before claim cap
- P2: max_claims_per_extraction=40
"""
import pytest
from dataclasses import dataclass

from cortex.extraction.predicate_filter import (
    BLOCKED_PREDICATES,
    filter_claims,
    filter_unknown_subjects,
    cap_engineering_claims,
)
from cortex.extraction.tautology_filter import (
    AGENT_ALIASES,
    _is_reflexive,
    filter_tautologies,
)
from cortex.extraction.models import ClaimExtraction


# ===================================================================
# P0: Previously-conflicting predicates now survive predicate_filter
# ===================================================================

class TestBlockedPredicatesRemoved:
    """Predicates that were in ALLOWED_PREDICATES (prompts_v2) should no longer be blocked."""

    UNBLOCKED = [
        'proposed', 'needs', 'needs_to', 'created', 'completed',
        'experiences', 'confirms', 'explains', 'resolved',
        'applied', 'changed', 'creates', 'updated',
    ]

    @pytest.mark.parametrize("pred", UNBLOCKED)
    def test_previously_blocked_predicate_now_survives(self, pred):
        claims = [{"subject": "Eva", "predicate": pred, "object_value": "something"}]
        result = filter_claims(claims)
        assert len(result) == 1, f"Predicate '{pred}' was incorrectly blocked"

    def test_unblocked_not_in_blocked_set(self):
        for pred in self.UNBLOCKED:
            assert pred not in BLOCKED_PREDICATES, f"'{pred}' still in BLOCKED_PREDICATES"

    def test_sprint_noise_still_blocked(self):
        """Sprint/task noise predicates should remain blocked."""
        still_blocked = ['implements', 'fixed', 'merged', 'deploys', 'spawns']
        for pred in still_blocked:
            claims = [{"subject": "Eva", "predicate": pred, "object_value": "something"}]
            result = filter_claims(claims)
            assert len(result) == 0, f"Predicate '{pred}' should still be blocked"


# ===================================================================
# P0: Tautology filter reflexive check tightened
# ===================================================================

class TestReflexiveCheckFix:
    """The reflexive check should not kill claims where subject word
    appears in a multi-word object with meaningful content."""

    _ALIASES = AGENT_ALIASES | {"eva"}

    def test_eva_experienced_desire_survives(self):
        """'Eva experienced desire' should NOT be reflexive — 'desire' is meaningful."""
        assert _is_reflexive("Eva", "desire", self._ALIASES) is False

    def test_eva_experienced_desire_survives_filter(self):
        """Full filter: 'Eva experienced desire' should survive."""
        claims = [{"subject": "Eva", "predicate": "experienced", "object_value": "desire"}]
        result = filter_tautologies(claims, agent_name="eva")
        assert len(result) == 1

    def test_eva_is_eva_still_caught(self):
        """Exact match: 'Eva is eva' should still be caught."""
        assert _is_reflexive("Eva", "eva", self._ALIASES) is True

    def test_eva_is_eva_caught_by_filter(self):
        claims = [{"subject": "Eva", "predicate": "is", "object_value": "eva"}]
        result = filter_tautologies(claims, agent_name="eva")
        assert len(result) == 0

    def test_single_word_object_same_as_subject_caught(self):
        """'user' subject with 'user' object should still be caught."""
        assert _is_reflexive("user", "user", self._ALIASES) is True

    def test_two_word_object_with_meaningful_remainder(self):
        """'Eva' subject with 'eva project' — 'project' is meaningful, should NOT be reflexive."""
        assert _is_reflexive("Eva", "eva project", self._ALIASES) is False

    def test_two_word_object_only_subject_word(self):
        """'Eva' subject with 'eva eva' — no meaningful remainder, should be reflexive."""
        assert _is_reflexive("Eva", "eva eva", self._ALIASES) is True

    def test_long_object_with_subject_word_survives(self):
        """Multi-word objects (>2 words) should never be caught by word containment."""
        assert _is_reflexive("Eva", "eva loves building systems", self._ALIASES) is False

    def test_alias_group_still_works(self):
        """Same alias group should still be caught regardless of word count."""
        assert _is_reflexive("agent", "assistant", self._ALIASES) is True


# ===================================================================
# P1: cap_engineering_claims max=8
# ===================================================================

class TestCapEngineeringMax8:
    def test_default_cap_allows_8(self):
        """Default cap should now be 8, not 2."""
        claims = [
            {"category": "decisions", "confidence": 0.5 + i * 0.05, "predicate": f"chose_{i}"}
            for i in range(8)
        ]
        result = cap_engineering_claims(claims)
        assert len(result) == 8

    def test_9_decisions_capped_to_8(self):
        claims = [
            {"category": "decisions", "confidence": 0.5 + i * 0.05, "predicate": f"chose_{i}"}
            for i in range(9)
        ]
        result = cap_engineering_claims(claims)
        eng = [c for c in result if c["category"] == "decisions"]
        assert len(eng) == 8

    def test_non_engineering_unaffected(self):
        claims = [
            {"category": "decisions", "confidence": 0.5 + i * 0.05, "predicate": f"d_{i}"}
            for i in range(10)
        ] + [
            {"category": "personal", "confidence": 0.9, "predicate": "likes"}
        ]
        result = cap_engineering_claims(claims)
        personal = [c for c in result if c["category"] == "personal"]
        assert len(personal) == 1


# ===================================================================
# P1: Category-aware subject filtering
# ===================================================================

class TestCategoryAwareSubjectFilter:
    def test_relational_category_exempted(self):
        """Claims with category=relational should pass even with unknown subject."""
        claims = [
            {"subject": "Jackie", "predicate": "has_relationship", "object_value": "first_customer", "category": "relational"},
        ]
        result = filter_unknown_subjects(claims, known_subjects={"Andrew", "Eva"})
        assert len(result) == 1

    def test_episodic_category_exempted(self):
        claims = [
            {"subject": "Mom", "predicate": "visited", "object_value": "Bangkok", "category": "episodic"},
        ]
        result = filter_unknown_subjects(claims, known_subjects={"Andrew", "Eva"})
        assert len(result) == 1

    def test_personal_category_exempted(self):
        claims = [
            {"subject": "Jackie", "predicate": "likes", "object_value": "coffee", "category": "personal"},
        ]
        result = filter_unknown_subjects(claims, known_subjects={"Andrew", "Eva"})
        assert len(result) == 1

    def test_decisions_category_still_blocked(self):
        """Unknown subjects in non-exempt categories should still be blocked."""
        claims = [
            {"subject": "RandomBot", "predicate": "chose", "object_value": "option_a", "category": "decisions"},
        ]
        result = filter_unknown_subjects(claims, known_subjects={"Andrew", "Eva"})
        assert len(result) == 0

    def test_known_subject_always_passes(self):
        claims = [
            {"subject": "Andrew", "predicate": "chose", "object_value": "option_a", "category": "decisions"},
        ]
        result = filter_unknown_subjects(claims, known_subjects={"Andrew", "Eva"})
        assert len(result) == 1

    def test_empty_known_subjects_passes_all(self):
        claims = [
            {"subject": "Anyone", "predicate": "did", "object_value": "thing", "category": "decisions"},
        ]
        result = filter_unknown_subjects(claims, known_subjects=set())
        assert len(result) == 1


# ===================================================================
# P2: Priority-sort before claim cap
# ===================================================================

class TestPrioritySortCap:
    """Test that high-salience claims survive cap over low-salience."""

    def test_high_salience_survives_over_low(self):
        """When capping, high-salience claims should be kept over low-salience."""
        from cortex.extraction.pipeline import ExtractionPipeline

        # Create claims with different salience levels
        high = ClaimExtraction(
            action="ADD", subject="Eva", predicate="loves", object_value="building",
            confidence=0.9, salience="high",
        )
        medium = ClaimExtraction(
            action="ADD", subject="Eva", predicate="likes", object_value="testing",
            confidence=0.8, salience="medium",
        )
        low = ClaimExtraction(
            action="ADD", subject="Eva", predicate="noticed", object_value="something",
            confidence=0.7, salience="low",
        )

        # Simulate what pipeline does: sort + cap
        all_claims = [low, medium, high]
        max_cap = 2

        SALIENCE_ORDER = {"high": 0, "medium": 1, "low": 2}
        all_claims.sort(key=lambda c: (
            SALIENCE_ORDER.get(getattr(c, 'salience', 'medium'), 1),
            -getattr(c, 'confidence', 0.5),
        ))
        result = all_claims[:max_cap]

        assert result[0].salience == "high"
        assert result[1].salience == "medium"

    def test_same_salience_sorted_by_confidence(self):
        """Within same salience, higher confidence should come first."""
        high_conf = ClaimExtraction(
            action="ADD", subject="Eva", predicate="loves", object_value="building",
            confidence=0.95, salience="medium",
        )
        low_conf = ClaimExtraction(
            action="ADD", subject="Eva", predicate="likes", object_value="testing",
            confidence=0.5, salience="medium",
        )

        all_claims = [low_conf, high_conf]
        SALIENCE_ORDER = {"high": 0, "medium": 1, "low": 2}
        all_claims.sort(key=lambda c: (
            SALIENCE_ORDER.get(getattr(c, 'salience', 'medium'), 1),
            -getattr(c, 'confidence', 0.5),
        ))

        assert all_claims[0].confidence == 0.95
        assert all_claims[1].confidence == 0.5


# ===================================================================
# P2: max_claims_per_extraction = 40
# ===================================================================

class TestMaxClaimsDefault:
    def test_default_is_40(self):
        """Pipeline default max_claims_per_extraction should be 40."""
        import inspect
        from cortex.extraction.pipeline import ExtractionPipeline
        sig = inspect.signature(ExtractionPipeline.__init__)
        default = sig.parameters['max_claims_per_extraction'].default
        assert default == 40
