"""
tests/test_extraction_locomo_fields.py — Unit tests for LoCoMo-aligned fields (v1.3)

Covers:
    - _dict_to_claim() correctly parses all 5 new fields
    - _dict_to_claim() falls back to defaults for missing/invalid values
    - to_candidate() maps all 5 new fields through to CandidateClaim
    - Invalid memory_class values default to "semantic"
    - Invalid event_date formats are rejected (set to None)
    - Non-integer sequence_order is rejected (set to None)
"""

from __future__ import annotations

import pytest

from cortex.extraction.extractor import ClaimExtraction, ExtractionPipeline


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

def _make_base_dict(**overrides) -> dict:
    """Minimal valid claim dict, optionally overriding fields."""
    base = {
        "action": "ADD",
        "subject": "user",
        "predicate": "prefers",
        "object_value": "Python for backend",
        "confidence": 0.9,
        "temporal": "current",
        "entities_mentioned": [],
        "sensitivity": "normal",
        "category": "preferences",
    }
    base.update(overrides)
    return base


def dict_to_claim(d: dict) -> ClaimExtraction:
    """Call the static parser directly."""
    return ExtractionPipeline._dict_to_claim(d)


# ---------------------------------------------------------------------------
# PART 1: All 5 new fields parse correctly when present
# ---------------------------------------------------------------------------

class TestNewFieldsParsing:
    def test_memory_class_semantic(self):
        claim = dict_to_claim(_make_base_dict(memory_class="semantic"))
        assert claim.memory_class == "semantic"

    def test_memory_class_episodic(self):
        claim = dict_to_claim(_make_base_dict(memory_class="episodic"))
        assert claim.memory_class == "episodic"

    def test_memory_class_procedural(self):
        claim = dict_to_claim(_make_base_dict(memory_class="procedural"))
        assert claim.memory_class == "procedural"

    def test_memory_class_open_loop(self):
        claim = dict_to_claim(_make_base_dict(memory_class="open_loop"))
        assert claim.memory_class == "open_loop"

    def test_memory_class_transient(self):
        claim = dict_to_claim(_make_base_dict(memory_class="transient"))
        assert claim.memory_class == "transient"

    def test_status_active(self):
        claim = dict_to_claim(_make_base_dict(status="active"))
        assert claim.status == "active"

    def test_status_completed(self):
        claim = dict_to_claim(_make_base_dict(status="completed"))
        assert claim.status == "completed"

    def test_status_superseded(self):
        claim = dict_to_claim(_make_base_dict(status="superseded"))
        assert claim.status == "superseded"

    def test_status_blocked(self):
        claim = dict_to_claim(_make_base_dict(status="blocked"))
        assert claim.status == "blocked"

    def test_event_date_valid(self):
        claim = dict_to_claim(_make_base_dict(event_date="2026-03-11"))
        assert claim.event_date == "2026-03-11"

    def test_event_date_none_explicit(self):
        claim = dict_to_claim(_make_base_dict(event_date=None))
        assert claim.event_date is None

    def test_sequence_order_integer(self):
        claim = dict_to_claim(_make_base_dict(sequence_order=3))
        assert claim.sequence_order == 3

    def test_sequence_order_string_integer(self):
        """String "5" should be coerced to int 5."""
        claim = dict_to_claim(_make_base_dict(sequence_order="5"))
        assert claim.sequence_order == 5

    def test_salience_high(self):
        claim = dict_to_claim(_make_base_dict(salience="high"))
        assert claim.salience == "high"

    def test_salience_medium(self):
        claim = dict_to_claim(_make_base_dict(salience="medium"))
        assert claim.salience == "medium"

    def test_salience_low(self):
        claim = dict_to_claim(_make_base_dict(salience="low"))
        assert claim.salience == "low"


# ---------------------------------------------------------------------------
# PART 2: Defaults when fields are missing or invalid
# ---------------------------------------------------------------------------

class TestNewFieldsDefaults:
    def test_all_new_fields_missing_uses_defaults(self):
        """When none of the new fields are present, defaults are applied."""
        claim = dict_to_claim(_make_base_dict())
        assert claim.memory_class == "semantic"
        assert claim.status == "active"
        assert claim.event_date is None
        assert claim.sequence_order is None
        assert claim.salience == "medium"

    def test_invalid_memory_class_defaults_to_semantic(self):
        claim = dict_to_claim(_make_base_dict(memory_class="UNKNOWN_CLASS"))
        assert claim.memory_class == "semantic"

    def test_invalid_memory_class_empty_string_defaults_to_semantic(self):
        claim = dict_to_claim(_make_base_dict(memory_class=""))
        assert claim.memory_class == "semantic"

    def test_invalid_status_defaults_to_active(self):
        claim = dict_to_claim(_make_base_dict(status="deleted"))
        assert claim.status == "active"

    def test_invalid_status_empty_defaults_to_active(self):
        claim = dict_to_claim(_make_base_dict(status=""))
        assert claim.status == "active"

    def test_invalid_event_date_format_rejected(self):
        """Non-ISO formats must not be stored — set to None."""
        claim = dict_to_claim(_make_base_dict(event_date="March 2026"))
        assert claim.event_date is None

    def test_event_date_partial_year_rejected(self):
        claim = dict_to_claim(_make_base_dict(event_date="2026-03"))
        assert claim.event_date is None

    def test_event_date_slashes_rejected(self):
        claim = dict_to_claim(_make_base_dict(event_date="03/11/2026"))
        assert claim.event_date is None

    def test_event_date_iso_with_time_rejected(self):
        """Datetime string (not just date) should be rejected."""
        claim = dict_to_claim(_make_base_dict(event_date="2026-03-11T10:00:00"))
        assert claim.event_date is None

    def test_sequence_order_non_integer_string_rejected(self):
        claim = dict_to_claim(_make_base_dict(sequence_order="not-a-number"))
        assert claim.sequence_order is None

    def test_sequence_order_float_string_rejected(self):
        """Float strings like "2.5" can't be cast to int cleanly — rejected."""
        claim = dict_to_claim(_make_base_dict(sequence_order="2.5"))
        assert claim.sequence_order is None

    def test_sequence_order_none_stays_none(self):
        claim = dict_to_claim(_make_base_dict(sequence_order=None))
        assert claim.sequence_order is None

    def test_invalid_salience_defaults_to_medium(self):
        claim = dict_to_claim(_make_base_dict(salience="critical"))
        assert claim.salience == "medium"

    def test_memory_class_case_insensitive(self):
        """Upper-case input should be normalised to lower before validation."""
        claim = dict_to_claim(_make_base_dict(memory_class="EPISODIC"))
        assert claim.memory_class == "episodic"

    def test_salience_case_insensitive(self):
        claim = dict_to_claim(_make_base_dict(salience="HIGH"))
        assert claim.salience == "high"


# ---------------------------------------------------------------------------
# PART 3: to_candidate() maps all 5 new fields through
# ---------------------------------------------------------------------------

class TestToCandidateMapsNewFields:
    def _make_extraction(self, **kwargs) -> ClaimExtraction:
        defaults = dict(
            action="ADD",
            subject="user",
            predicate="prefers",
            object_value="dark mode",
            confidence=0.9,
        )
        defaults.update(kwargs)
        return ClaimExtraction(**defaults)

    def test_memory_class_passed_through(self):
        ext = self._make_extraction(memory_class="episodic")
        candidate = ext.to_candidate()
        assert candidate.memory_class == "episodic"

    def test_status_passed_through(self):
        ext = self._make_extraction(status="completed")
        candidate = ext.to_candidate()
        assert candidate.status == "completed"

    def test_event_date_passed_through(self):
        ext = self._make_extraction(event_date="2026-03-11")
        candidate = ext.to_candidate()
        assert candidate.event_date == "2026-03-11"

    def test_event_date_none_passed_through(self):
        ext = self._make_extraction(event_date=None)
        candidate = ext.to_candidate()
        assert candidate.event_date is None

    def test_sequence_order_passed_through(self):
        ext = self._make_extraction(sequence_order=2)
        candidate = ext.to_candidate()
        assert candidate.sequence_order == 2

    def test_sequence_order_none_passed_through(self):
        ext = self._make_extraction(sequence_order=None)
        candidate = ext.to_candidate()
        assert candidate.sequence_order is None

    def test_salience_passed_through(self):
        ext = self._make_extraction(salience="high")
        candidate = ext.to_candidate()
        assert candidate.salience == "high"

    def test_all_new_fields_defaults_passed_through(self):
        """Default values on ClaimExtraction propagate correctly to CandidateClaim."""
        ext = self._make_extraction()
        candidate = ext.to_candidate()
        assert candidate.memory_class == "semantic"
        assert candidate.status == "active"
        assert candidate.event_date is None
        assert candidate.sequence_order is None
        assert candidate.salience == "medium"

    def test_full_round_trip_via_dict(self):
        """Full round-trip: dict → ClaimExtraction → CandidateClaim."""
        d = _make_base_dict(
            memory_class="open_loop",
            status="blocked",
            event_date="2026-01-15",
            sequence_order=7,
            salience="low",
        )
        claim = dict_to_claim(d)
        candidate = claim.to_candidate()
        assert candidate.memory_class == "open_loop"
        assert candidate.status == "blocked"
        assert candidate.event_date == "2026-01-15"
        assert candidate.sequence_order == 7
        assert candidate.salience == "low"
