"""
tests/test_claim_extraction_enum_validation.py

Tests for the __post_init__ enum validation added to ClaimExtraction (#1299).

Coverage:
- All 10 validated fields: action, temporal, sensitivity, category,
  intrinsic_type, explicitness, stability, memory_class, status, salience
- Valid values pass through unchanged
- Invalid values are clamped to the safe default
- A logger.warning is emitted on clamp
- CandidateClaim (via to_candidate) is not affected by this — it receives
  already-validated data
"""

import logging

import pytest

from cortex.extraction.models import (
    VALID_ACTIONS,
    VALID_CATEGORY,
    VALID_EXPLICITNESS,
    VALID_INTRINSIC_TYPE,
    VALID_MEMORY_CLASS,
    VALID_SALIENCE,
    VALID_SENSITIVITY,
    VALID_STABILITY,
    VALID_STATUS,
    VALID_TEMPORAL,
    ClaimExtraction,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_REQUIRED = dict(
    action="ADD",
    subject="user",
    predicate="likes",
    object_value="Python",
    confidence=0.9,
)


def _make(**kwargs) -> ClaimExtraction:
    """Build a ClaimExtraction with required fields, overriding any via kwargs."""
    params = {**_REQUIRED, **kwargs}
    return ClaimExtraction(**params)


# ---------------------------------------------------------------------------
# Valid values — should pass unchanged
# ---------------------------------------------------------------------------

class TestValidValues:
    @pytest.mark.parametrize("action", sorted(VALID_ACTIONS))
    def test_action_valid(self, action):
        c = _make(action=action)
        assert c.action == action

    @pytest.mark.parametrize("temporal", sorted(VALID_TEMPORAL))
    def test_temporal_valid(self, temporal):
        c = _make(temporal=temporal)
        assert c.temporal == temporal

    @pytest.mark.parametrize("sensitivity", sorted(VALID_SENSITIVITY))
    def test_sensitivity_valid(self, sensitivity):
        c = _make(sensitivity=sensitivity)
        assert c.sensitivity == sensitivity

    @pytest.mark.parametrize("category", sorted(VALID_CATEGORY))
    def test_category_valid(self, category):
        c = _make(category=category)
        assert c.category == category

    @pytest.mark.parametrize("intrinsic_type", sorted(VALID_INTRINSIC_TYPE))
    def test_intrinsic_type_valid(self, intrinsic_type):
        c = _make(intrinsic_type=intrinsic_type)
        assert c.intrinsic_type == intrinsic_type

    @pytest.mark.parametrize("explicitness", sorted(VALID_EXPLICITNESS))
    def test_explicitness_valid(self, explicitness):
        c = _make(explicitness=explicitness)
        assert c.explicitness == explicitness

    @pytest.mark.parametrize("stability", sorted(VALID_STABILITY))
    def test_stability_valid(self, stability):
        c = _make(stability=stability)
        assert c.stability == stability

    @pytest.mark.parametrize("memory_class", sorted(VALID_MEMORY_CLASS))
    def test_memory_class_valid(self, memory_class):
        c = _make(memory_class=memory_class)
        assert c.memory_class == memory_class

    @pytest.mark.parametrize("status", sorted(VALID_STATUS))
    def test_status_valid(self, status):
        c = _make(status=status)
        assert c.status == status

    @pytest.mark.parametrize("salience", sorted(VALID_SALIENCE))
    def test_salience_valid(self, salience):
        c = _make(salience=salience)
        assert c.salience == salience


# ---------------------------------------------------------------------------
# Invalid values — should clamp to default and emit a warning
# ---------------------------------------------------------------------------

class TestInvalidValuesClamped:

    def test_action_invalid_clamps_to_noop(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(action="BOGUS")
        assert c.action == "NOOP"
        assert "action" in caplog.text
        assert "BOGUS" in caplog.text

    def test_temporal_invalid_clamps_to_current(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(temporal="soon")
        assert c.temporal == "current"
        assert "temporal" in caplog.text

    def test_sensitivity_invalid_clamps_to_normal(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(sensitivity="confidential")
        assert c.sensitivity == "normal"
        assert "sensitivity" in caplog.text

    def test_category_invalid_clamps_to_misc(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(category="unknown_cat")
        assert c.category == "misc"
        assert "category" in caplog.text

    def test_intrinsic_type_invalid_clamps_to_fact(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(intrinsic_type="opinion")
        assert c.intrinsic_type == "fact"
        assert "intrinsic_type" in caplog.text

    def test_explicitness_invalid_clamps_to_inferred(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(explicitness="guessed")
        assert c.explicitness == "inferred"
        assert "explicitness" in caplog.text

    def test_stability_invalid_clamps_to_volatile(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(stability="unknown")
        assert c.stability == "volatile"
        assert "stability" in caplog.text

    def test_memory_class_invalid_clamps_to_semantic(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(memory_class="spatial")
        assert c.memory_class == "semantic"
        assert "memory_class" in caplog.text

    def test_status_invalid_clamps_to_active(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(status="pending")
        assert c.status == "active"
        assert "status" in caplog.text

    def test_salience_invalid_clamps_to_medium(self, caplog):
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(salience="critical")
        assert c.salience == "medium"
        assert "salience" in caplog.text

    def test_multiple_invalid_fields_all_clamped(self, caplog):
        """Multiple bad fields in one construction → all clamped, all warned."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(
                action="DESTROY",
                temporal="never",
                salience="extreme",
                status="pending",
            )
        assert c.action == "NOOP"
        assert c.temporal == "current"
        assert c.salience == "medium"
        assert c.status == "active"
        # Four warnings expected
        warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
        assert len(warnings) >= 4

    def test_no_warning_for_all_valid_defaults(self, caplog):
        """Constructing with all defaults emits no warnings."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make()  # all defaults are valid
        warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
        assert warnings == []


# ---------------------------------------------------------------------------
# to_candidate passes validated values through unchanged
# ---------------------------------------------------------------------------

class TestToCandidateWithValidation:

    def test_to_candidate_receives_clamped_action(self):
        c = _make(action="BAD_ACTION")
        assert c.action == "NOOP"
        cand = c.to_candidate()
        assert cand.action == "NOOP"

    def test_to_candidate_receives_clamped_salience(self):
        c = _make(salience="ULTRA")
        assert c.salience == "medium"
        cand = c.to_candidate()
        assert cand.salience == "medium"


# ---------------------------------------------------------------------------
# Non-string type guard (Issue #2 — unhashable types)
# ---------------------------------------------------------------------------

class TestNonStringTypeGuard:

    def test_dict_action_clamps_to_default(self, caplog):
        """Dict value for action should not raise TypeError."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(action={"type": "ADD"})
        assert c.action == "NOOP"
        assert "non-string" in caplog.text

    def test_list_temporal_clamps_to_default(self, caplog):
        """List value for temporal should not raise TypeError."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(temporal=["current", "past"])
        assert c.temporal == "current"
        assert "non-string" in caplog.text

    def test_int_salience_clamps_to_default(self, caplog):
        """Integer value should be caught by type guard."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(salience=42)
        assert c.salience == "medium"
        assert "non-string" in caplog.text

    def test_none_category_clamps_to_default(self, caplog):
        """None value should be caught by type guard."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(category=None)
        assert c.category == "misc"
        assert "non-string" in caplog.text


# ---------------------------------------------------------------------------
# Case-insensitive validation (Issue #3)
# ---------------------------------------------------------------------------

class TestCaseInsensitiveValidation:

    def test_action_lowercase_normalized(self):
        c = _make(action="add")
        assert c.action == "ADD"

    def test_action_mixed_case_normalized(self):
        c = _make(action="Add")
        assert c.action == "ADD"

    def test_temporal_uppercase_normalized(self):
        c = _make(temporal="CURRENT")
        assert c.temporal == "current"

    def test_temporal_mixed_case_normalized(self):
        c = _make(temporal="Current")
        assert c.temporal == "current"

    def test_salience_uppercase_normalized(self):
        c = _make(salience="HIGH")
        assert c.salience == "high"

    def test_sensitivity_mixed_case_normalized(self):
        c = _make(sensitivity="Normal")
        assert c.sensitivity == "normal"

    def test_category_uppercase_normalized(self):
        c = _make(category="IDENTITY")
        assert c.category == "identity"

    def test_case_normalization_no_warning(self, caplog):
        """Case-insensitive match should NOT emit a warning."""
        with caplog.at_level(logging.WARNING, logger="cortex.extraction.models"):
            c = _make(action="add", temporal="CURRENT", salience="High")
        assert c.action == "ADD"
        assert c.temporal == "current"
        assert c.salience == "high"
        warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
        assert warnings == []
