"""
tests/test_contradictions.py — Comprehensive tests for ContradictionTracker.

Coverage:
  - ContradictionRecord: construction, validation, field defaults
  - ContradictionTracker.record(): basic recording, idempotency, reason storage
  - ContradictionTracker.list_pending(): filters by resolution state
  - ContradictionTracker.list_all(): returns all records regardless of state
  - ContradictionTracker.resolve(): updates resolution + resolved_at timestamp
  - ContradictionTracker.clear(): resets the tracker
  - ContradictionTracker.__len__(): count semantics
  - Edge cases: resolve missing record, invalid resolution values, partial matches
  - History tracking: insertion order preserved, multiple records

Run: pytest tests/test_contradictions.py -v
"""

from __future__ import annotations

import pytest
from datetime import datetime, timezone

from cortex.core.contradictions import ContradictionRecord, ContradictionTracker


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


def _make_tracker(*records) -> ContradictionTracker:
    """Build a tracker pre-loaded with (claim_id, contradicts_id, resolution, reason) tuples."""
    tracker = ContradictionTracker()
    for args in records:
        tracker.record(*args)
    return tracker


# ===========================================================================
# ContradictionRecord — unit tests
# ===========================================================================


class TestContradictionRecord:
    def test_default_resolution_is_pending(self):
        rec = ContradictionRecord(claim_id="c1", contradicts_id="c0")
        assert rec.resolution == "pending"

    def test_explicit_resolution_superseded(self):
        rec = ContradictionRecord(claim_id="c1", contradicts_id="c0", resolution="superseded")
        assert rec.resolution == "superseded"

    def test_explicit_resolution_user_overridden(self):
        rec = ContradictionRecord(
            claim_id="c1", contradicts_id="c0", resolution="user_overridden"
        )
        assert rec.resolution == "user_overridden"

    def test_invalid_resolution_raises(self):
        with pytest.raises(ValueError, match="Invalid resolution"):
            ContradictionRecord(claim_id="c1", contradicts_id="c0", resolution="invalid")

    def test_detected_at_is_set_automatically(self):
        rec = ContradictionRecord(claim_id="c1", contradicts_id="c0")
        assert rec.detected_at is not None
        # Should be a valid ISO timestamp
        dt = datetime.fromisoformat(rec.detected_at)
        assert dt.tzinfo is not None

    def test_resolved_at_defaults_to_none(self):
        rec = ContradictionRecord(claim_id="c1", contradicts_id="c0")
        assert rec.resolved_at is None

    def test_reason_defaults_to_empty_string(self):
        rec = ContradictionRecord(claim_id="c1", contradicts_id="c0")
        assert rec.reason == ""

    def test_explicit_reason_stored(self):
        rec = ContradictionRecord(
            claim_id="c1", contradicts_id="c0", reason="CONTRADICTION heuristic"
        )
        assert rec.reason == "CONTRADICTION heuristic"

    def test_fields_preserved(self):
        rec = ContradictionRecord(
            claim_id="new-claim",
            contradicts_id="old-claim",
            resolution="superseded",
            reason="subject+predicate match",
        )
        assert rec.claim_id == "new-claim"
        assert rec.contradicts_id == "old-claim"


# ===========================================================================
# ContradictionTracker.record()
# ===========================================================================


class TestRecord:
    def test_record_returns_contradiction_record(self):
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0")
        assert isinstance(rec, ContradictionRecord)

    def test_record_stores_claim_ids(self):
        tracker = ContradictionTracker()
        rec = tracker.record("new-id", "old-id")
        assert rec.claim_id == "new-id"
        assert rec.contradicts_id == "old-id"

    def test_record_default_resolution_is_superseded(self):
        """record() defaults to 'superseded' (not 'pending' like the dataclass default)."""
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0")
        assert rec.resolution == "superseded"

    def test_record_explicit_resolution(self):
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0", resolution="pending")
        assert rec.resolution == "pending"

    def test_record_stores_reason(self):
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0", reason="temporal marker mismatch")
        assert rec.reason == "temporal marker mismatch"

    def test_record_increments_len(self):
        tracker = ContradictionTracker()
        assert len(tracker) == 0
        tracker.record("c1", "c0")
        assert len(tracker) == 1
        tracker.record("c2", "c1")
        assert len(tracker) == 2

    def test_record_idempotent_same_pair(self):
        """Calling record() twice with the same pair returns the existing record."""
        tracker = ContradictionTracker()
        rec1 = tracker.record("c1", "c0", reason="first")
        rec2 = tracker.record("c1", "c0", reason="second")
        assert rec1 is rec2
        assert len(tracker) == 1
        # Original reason is preserved (idempotent — no mutation on duplicate)
        assert rec1.reason == "first"

    def test_record_different_pairs_both_stored(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0")
        tracker.record("c2", "c0")
        assert len(tracker) == 2

    def test_record_same_claim_id_different_contradicts(self):
        """Same claim_id but different contradicts_id → two separate records."""
        tracker = ContradictionTracker()
        tracker.record("c1", "c0")
        tracker.record("c1", "c-other")
        assert len(tracker) == 2

    def test_record_invalid_resolution_raises(self):
        tracker = ContradictionTracker()
        with pytest.raises(ValueError):
            tracker.record("c1", "c0", resolution="bogus")


# ===========================================================================
# ContradictionTracker.list_pending()
# ===========================================================================


class TestListPending:
    def test_empty_tracker_returns_empty(self):
        tracker = ContradictionTracker()
        assert tracker.list_pending() == []

    def test_pending_records_returned(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.record("c2", "c1", resolution="pending")
        assert len(tracker.list_pending()) == 2

    def test_non_pending_records_excluded(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="superseded")
        tracker.record("c2", "c1", resolution="user_overridden")
        assert tracker.list_pending() == []

    def test_mixed_resolutions_only_pending_returned(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.record("c2", "c1", resolution="superseded")
        tracker.record("c3", "c2", resolution="pending")
        pending = tracker.list_pending()
        assert len(pending) == 2
        ids = {r.claim_id for r in pending}
        assert ids == {"c1", "c3"}

    def test_list_pending_preserves_insertion_order(self):
        tracker = ContradictionTracker()
        for i in range(5):
            tracker.record(f"c{i+1}", f"c{i}", resolution="pending")
        pending = tracker.list_pending()
        assert [r.claim_id for r in pending] == ["c1", "c2", "c3", "c4", "c5"]

    def test_list_pending_returns_new_list(self):
        """list_pending() should return a copy, not the internal list."""
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        p1 = tracker.list_pending()
        p1.clear()
        # Internal state should not be mutated
        assert len(tracker.list_pending()) == 1


# ===========================================================================
# ContradictionTracker.list_all()
# ===========================================================================


class TestListAll:
    def test_empty_tracker_returns_empty(self):
        assert ContradictionTracker().list_all() == []

    def test_all_records_returned(self):
        tracker = _make_tracker(
            ("c1", "c0", "pending", "r1"),
            ("c2", "c1", "superseded", "r2"),
            ("c3", "c2", "user_overridden", "r3"),
        )
        all_recs = tracker.list_all()
        assert len(all_recs) == 3

    def test_list_all_preserves_insertion_order(self):
        tracker = ContradictionTracker()
        ids = [f"c{i}" for i in range(1, 6)]
        for i, cid in enumerate(ids):
            tracker.record(cid, f"c{i}", resolution="superseded")
        assert [r.claim_id for r in tracker.list_all()] == ids

    def test_list_all_returns_copy(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0")
        all1 = tracker.list_all()
        all1.clear()
        assert len(tracker.list_all()) == 1


# ===========================================================================
# ContradictionTracker.resolve()
# ===========================================================================


class TestResolve:
    def test_resolve_updates_resolution(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        rec = tracker.resolve("c1", "superseded")
        assert rec is not None
        assert rec.resolution == "superseded"

    def test_resolve_sets_resolved_at(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        rec = tracker.resolve("c1", "superseded")
        assert rec.resolved_at is not None
        dt = datetime.fromisoformat(rec.resolved_at)
        assert dt.tzinfo is not None

    def test_resolve_user_overridden(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        rec = tracker.resolve("c1", "user_overridden")
        assert rec.resolution == "user_overridden"

    def test_resolve_returns_same_object(self):
        """resolve() mutates and returns the original record object."""
        tracker = ContradictionTracker()
        original = tracker.record("c1", "c0", resolution="pending")
        resolved = tracker.resolve("c1", "superseded")
        assert original is resolved

    def test_resolve_missing_claim_returns_none(self):
        tracker = ContradictionTracker()
        result = tracker.resolve("nonexistent", "superseded")
        assert result is None

    def test_resolve_invalid_resolution_raises(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        with pytest.raises(ValueError, match="Invalid resolution"):
            tracker.resolve("c1", "bad_value")

    def test_resolve_with_contradicts_id_narrows_match(self):
        """When claim_id has multiple records, contradicts_id narrows to the right one."""
        tracker = ContradictionTracker()
        rec_a = tracker.record("c1", "c0", resolution="pending")
        rec_b = tracker.record("c1", "c-other", resolution="pending")
        result = tracker.resolve("c1", "superseded", contradicts_id="c0")
        assert result is rec_a
        assert rec_a.resolution == "superseded"
        # rec_b should still be pending
        assert rec_b.resolution == "pending"

    def test_resolve_without_contradicts_id_matches_first(self):
        """Without contradicts_id, resolve() finds first record with matching claim_id."""
        tracker = ContradictionTracker()
        rec_a = tracker.record("c1", "c0", resolution="pending")
        tracker.record("c1", "c-other", resolution="pending")
        result = tracker.resolve("c1", "user_overridden")
        assert result is rec_a

    def test_resolved_record_no_longer_in_pending(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.resolve("c1", "superseded")
        assert tracker.list_pending() == []

    def test_resolve_does_not_change_len(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        assert len(tracker) == 1
        tracker.resolve("c1", "superseded")
        assert len(tracker) == 1


# ===========================================================================
# ContradictionTracker.clear()
# ===========================================================================


class TestClear:
    def test_clear_empties_tracker(self):
        tracker = _make_tracker(
            ("c1", "c0", "superseded", ""),
            ("c2", "c1", "pending", ""),
        )
        assert len(tracker) == 2
        tracker.clear()
        assert len(tracker) == 0

    def test_clear_empties_list_all(self):
        tracker = _make_tracker(("c1", "c0", "superseded", ""))
        tracker.clear()
        assert tracker.list_all() == []

    def test_clear_empties_list_pending(self):
        tracker = _make_tracker(("c1", "c0", "pending", ""))
        tracker.clear()
        assert tracker.list_pending() == []

    def test_record_after_clear(self):
        """Can add records again after clear()."""
        tracker = _make_tracker(("c1", "c0", "superseded", ""))
        tracker.clear()
        tracker.record("c2", "c1", resolution="pending")
        assert len(tracker) == 1
        assert tracker.list_pending()[0].claim_id == "c2"


# ===========================================================================
# ContradictionTracker.__len__()
# ===========================================================================


class TestLen:
    def test_empty_tracker_len_zero(self):
        assert len(ContradictionTracker()) == 0

    def test_len_increments_with_each_record(self):
        tracker = ContradictionTracker()
        for i in range(5):
            tracker.record(f"c{i+1}", f"c{i}")
            assert len(tracker) == i + 1

    def test_len_unchanged_after_resolve(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.resolve("c1", "superseded")
        assert len(tracker) == 1

    def test_len_returns_zero_after_clear(self):
        tracker = _make_tracker(
            ("c1", "c0", "superseded", ""),
            ("c2", "c1", "pending", ""),
        )
        tracker.clear()
        assert len(tracker) == 0


# ===========================================================================
# History tracking — multi-record workflows
# ===========================================================================


class TestHistoryTracking:
    def test_full_lifecycle_single_contradiction(self):
        """Record a contradiction as pending, then resolve it."""
        tracker = ContradictionTracker()
        rec = tracker.record("c-new", "c-old", resolution="pending", reason="temporal conflict")
        assert len(tracker.list_pending()) == 1

        tracker.resolve("c-new", "user_overridden")
        assert len(tracker.list_pending()) == 0
        all_recs = tracker.list_all()
        assert len(all_recs) == 1
        assert all_recs[0].resolution == "user_overridden"
        assert all_recs[0].resolved_at is not None

    def test_batch_detection_and_partial_resolution(self):
        """Simulate batch contradiction detection: detect 3, resolve 2."""
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.record("c2", "c0", resolution="pending")
        tracker.record("c3", "c1", resolution="pending")

        tracker.resolve("c1", "superseded")
        tracker.resolve("c3", "user_overridden")

        pending = tracker.list_pending()
        assert len(pending) == 1
        assert pending[0].claim_id == "c2"
        assert len(tracker.list_all()) == 3

    def test_auto_supersede_flow(self):
        """Engine auto-supersedes a claim: record with 'superseded', never goes pending."""
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0", resolution="superseded", reason="SUPERSEDE action")
        assert rec.resolution == "superseded"
        assert tracker.list_pending() == []
        assert len(tracker.list_all()) == 1

    def test_multiple_contradictions_for_same_winning_claim(self):
        """A single new claim can supersede multiple old claims."""
        tracker = ContradictionTracker()
        tracker.record("c-new", "c-old-1", resolution="superseded")
        tracker.record("c-new", "c-old-2", resolution="superseded")
        tracker.record("c-new", "c-old-3", resolution="superseded")
        assert len(tracker) == 3
        assert all(r.claim_id == "c-new" for r in tracker.list_all())

    def test_session_reset_via_clear(self):
        """Simulate session reset: populate tracker, then clear for next session."""
        tracker = ContradictionTracker()
        for i in range(10):
            tracker.record(f"c{i+1}", f"c{i}", resolution="superseded")
        assert len(tracker) == 10

        tracker.clear()
        assert len(tracker) == 0

        # New session starts fresh
        tracker.record("d1", "d0", resolution="pending")
        assert len(tracker) == 1

    def test_idempotency_across_repeated_calls(self):
        """Repeated record() calls for same pair never inflate the count."""
        tracker = ContradictionTracker()
        for _ in range(5):
            tracker.record("c1", "c0", resolution="superseded")
        assert len(tracker) == 1


# ===========================================================================
# Edge cases
# ===========================================================================


class TestEdgeCases:
    def test_empty_reason_stored(self):
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0")
        assert rec.reason == ""

    def test_claim_id_equals_contradicts_id(self):
        """Degenerate case: a claim contradicts itself. Should not crash."""
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c1")
        assert rec.claim_id == "c1"
        assert rec.contradicts_id == "c1"

    def test_unicode_claim_ids(self):
        tracker = ContradictionTracker()
        rec = tracker.record("claim-αβγ", "claim-δεζ", reason="unicode test")
        assert rec.claim_id == "claim-αβγ"

    def test_very_long_reason_stored(self):
        long_reason = "x" * 10_000
        tracker = ContradictionTracker()
        rec = tracker.record("c1", "c0", reason=long_reason)
        assert len(rec.reason) == 10_000

    def test_resolve_on_empty_tracker_returns_none(self):
        tracker = ContradictionTracker()
        assert tracker.resolve("c1", "superseded") is None

    def test_list_pending_after_all_resolved(self):
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.record("c2", "c1", resolution="pending")
        tracker.resolve("c1", "superseded")
        tracker.resolve("c2", "user_overridden")
        assert tracker.list_pending() == []

    def test_multiple_resolve_calls_update_each_time(self):
        """resolve() can be called again to update an already-resolved record."""
        tracker = ContradictionTracker()
        tracker.record("c1", "c0", resolution="pending")
        tracker.resolve("c1", "superseded")
        tracker.resolve("c1", "user_overridden")
        rec = tracker.list_all()[0]
        assert rec.resolution == "user_overridden"
