"""Tests for causal linking (#518) and state transition (#519) fields.

Verifies that cause, effect, state_before, state_after flow through the
full pipeline: extraction → parsing → reconciliation → storage → retrieval.
"""

import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock

from cortex.extraction.extractor import ExtractionPipeline, ClaimExtraction
from cortex.types import CandidateClaim, SemanticClaim


# ---------------------------------------------------------------------------
# 1. _dict_to_claim() parses all 4 fields
# ---------------------------------------------------------------------------

class TestDictToClaim:
    """Test _dict_to_claim() parsing of causal/state fields."""

    def test_parses_all_four_fields(self):
        d = {
            "action": "ADD",
            "subject": "user",
            "predicate": "uses_for_caching",
            "object_value": "Memcached",
            "confidence": 0.9,
            "cause": "Redis had poor memory efficiency",
            "effect": "migrated to Memcached",
            "state_before": "Redis",
            "state_after": "Memcached",
        }
        claim = ExtractionPipeline._dict_to_claim(d)
        assert claim.cause == "Redis had poor memory efficiency"
        assert claim.effect == "migrated to Memcached"
        assert claim.state_before == "Redis"
        assert claim.state_after == "Memcached"

    def test_missing_fields_default_to_none(self):
        d = {
            "action": "ADD",
            "subject": "user",
            "predicate": "prefers",
            "object_value": "Python",
            "confidence": 0.8,
        }
        claim = ExtractionPipeline._dict_to_claim(d)
        assert claim.cause is None
        assert claim.effect is None
        assert claim.state_before is None
        assert claim.state_after is None

    def test_empty_string_stripped_to_none(self):
        d = {
            "action": "ADD",
            "subject": "user",
            "predicate": "prefers",
            "object_value": "Python",
            "confidence": 0.8,
            "cause": "",
            "effect": "  ",
            "state_before": "  ",
            "state_after": "",
        }
        claim = ExtractionPipeline._dict_to_claim(d)
        assert claim.cause is None
        assert claim.effect is None
        assert claim.state_before is None
        assert claim.state_after is None

    def test_whitespace_trimmed(self):
        d = {
            "action": "ADD",
            "subject": "user",
            "predicate": "uses",
            "object_value": "Postgres",
            "confidence": 0.9,
            "cause": "  scaling issues  ",
            "effect": "  switched to Postgres  ",
            "state_before": " MongoDB ",
            "state_after": " Postgres ",
        }
        claim = ExtractionPipeline._dict_to_claim(d)
        assert claim.cause == "scaling issues"
        assert claim.effect == "switched to Postgres"
        assert claim.state_before == "MongoDB"
        assert claim.state_after == "Postgres"

    def test_null_values_become_none(self):
        d = {
            "action": "ADD",
            "subject": "user",
            "predicate": "prefers",
            "object_value": "Python",
            "confidence": 0.8,
            "cause": None,
            "effect": None,
            "state_before": None,
            "state_after": None,
        }
        claim = ExtractionPipeline._dict_to_claim(d)
        assert claim.cause is None
        assert claim.effect is None
        assert claim.state_before is None
        assert claim.state_after is None


# ---------------------------------------------------------------------------
# 4. to_candidate() maps all 4 fields
# ---------------------------------------------------------------------------

class TestToCandidate:
    """Test ClaimExtraction.to_candidate() mapping of causal/state fields."""

    def test_maps_all_four_fields(self):
        claim = ClaimExtraction(
            action="UPDATE",
            subject="user",
            predicate="uses_for_caching",
            object_value="Memcached",
            confidence=0.9,
            cause="poor Redis memory efficiency",
            effect="migrated to Memcached",
            state_before="Redis",
            state_after="Memcached",
        )
        candidate = claim.to_candidate()
        assert isinstance(candidate, CandidateClaim)
        assert candidate.cause == "poor Redis memory efficiency"
        assert candidate.effect == "migrated to Memcached"
        assert candidate.state_before == "Redis"
        assert candidate.state_after == "Memcached"

    def test_none_fields_preserved(self):
        claim = ClaimExtraction(
            action="ADD",
            subject="user",
            predicate="prefers",
            object_value="Python",
            confidence=0.8,
        )
        candidate = claim.to_candidate()
        assert candidate.cause is None
        assert candidate.effect is None
        assert candidate.state_before is None
        assert candidate.state_after is None


# ---------------------------------------------------------------------------
# 5. Round-trip: create_claim() with all 4 → read back → verify
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_storage_round_trip():
    """Create a claim with all 4 fields via storage, read back, verify."""
    from cortex.storage import SQLiteAdapter

    store = SQLiteAdapter(db_path=":memory:", busy_timeout_ms=1000, enable_wal=False)
    await store.initialize()

    claim = await store.create_claim(
        subject="user",
        predicate="uses_for_caching",
        object_value="Memcached",
        owner_id="test-owner",
        cause="Redis had poor memory efficiency",
        effect="migrated to Memcached",
        state_before="Redis",
        state_after="Memcached",
    )

    assert claim.cause == "Redis had poor memory efficiency"
    assert claim.effect == "migrated to Memcached"
    assert claim.state_before == "Redis"
    assert claim.state_after == "Memcached"

    # Read back
    retrieved = await store.get_claim(claim.id)
    assert retrieved is not None
    assert retrieved.cause == "Redis had poor memory efficiency"
    assert retrieved.effect == "migrated to Memcached"
    assert retrieved.state_before == "Redis"
    assert retrieved.state_after == "Memcached"

    await store.close()


@pytest.mark.asyncio
async def test_storage_round_trip_none_fields():
    """Claims without causal/state fields should round-trip with None."""
    from cortex.storage import SQLiteAdapter

    store = SQLiteAdapter(db_path=":memory:", busy_timeout_ms=1000, enable_wal=False)
    await store.initialize()

    claim = await store.create_claim(
        subject="user",
        predicate="prefers",
        object_value="Python",
        owner_id="test-owner",
    )

    assert claim.cause is None
    assert claim.effect is None
    assert claim.state_before is None
    assert claim.state_after is None

    retrieved = await store.get_claim(claim.id)
    assert retrieved is not None
    assert retrieved.cause is None
    assert retrieved.effect is None
    assert retrieved.state_before is None
    assert retrieved.state_after is None

    await store.close()


# ---------------------------------------------------------------------------
# 6. Reconciliation forwards all 4 from CandidateClaim
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_reconciliation_forwards_causal_state_fields():
    """ReconciliationEngine._add_claim() should forward cause/effect/state_before/state_after.

    We test by verifying the kwargs dict is built correctly, then that
    create_claim() stores them. Uses a real store with a session record
    to satisfy FK constraints.
    """
    from cortex.storage import SQLiteAdapter
    from cortex.core.reconciliation import ReconciliationEngine

    store = SQLiteAdapter(db_path=":memory:", busy_timeout_ms=1000, enable_wal=False)
    await store.initialize()

    # Create a session record so the FK constraint is satisfied
    session = await store.create_session(session_id="test-session", owner_id="test-owner")

    config = MagicMock()
    config.session_confidence_default = 0.5
    config.similarity_threshold = 0.85
    config.max_claims_per_session = 100

    engine = ReconciliationEngine(
        storage=store, config=config,
        llm=MagicMock(), entity_resolver=MagicMock(),
    )

    candidate = CandidateClaim(
        subject="user",
        predicate="uses_database",
        object_value="CockroachDB",
        confidence=0.9,
        cause="Postgres connection pooling bottleneck",
        effect="migrated to CockroachDB",
        state_before="Postgres",
        state_after="CockroachDB",
    )

    claim = await engine._add_claim(
        candidate=candidate,
        session_id=session.id,
        owner_id="test-owner",
    )

    assert claim.cause == "Postgres connection pooling bottleneck"
    assert claim.effect == "migrated to CockroachDB"
    assert claim.state_before == "Postgres"
    assert claim.state_after == "CockroachDB"

    await store.close()


# ---------------------------------------------------------------------------
# Dataclass defaults
# ---------------------------------------------------------------------------

class TestDataclassDefaults:
    """Verify Optional[str] = None defaults on all dataclasses."""

    def test_claim_extraction_defaults(self):
        c = ClaimExtraction(
            action="ADD", subject="s", predicate="p",
            object_value="v", confidence=0.5,
        )
        assert c.cause is None
        assert c.effect is None
        assert c.state_before is None
        assert c.state_after is None

    def test_candidate_claim_defaults(self):
        c = CandidateClaim(subject="s", predicate="p", object_value="v")
        assert c.cause is None
        assert c.effect is None
        assert c.state_before is None
        assert c.state_after is None

    def test_semantic_claim_defaults(self):
        c = SemanticClaim(
            id="test-id", owner_id="test-owner",
            subject="s", predicate="p", object_value="v",
        )
        assert c.cause is None
        assert c.effect is None
        assert c.state_before is None
        assert c.state_after is None
