"""Contract tests for sdk_adapter.py result schemas.

Validates that _adapt_write_result() and _adapt_retrieve_result() produce
dicts with ALL keys that advisor_api consumers access.

== Consumer Key Mapping (source of truth) ==

WRITE RESULT — consumed by:
  main.py:         result["event_id"], result["facts_added"], result["facts_updated"],
                   result["facts_unchanged"], result["facts_deleted"],
                   result["entities_resolved"], result["duration_ms"]
  chat_router.py:  same keys as main.py
  process_memory_write_with_trace adds: result["trace"]

RETRIEVE RESULT — consumed by (all via .get() with defaults):
  main.py:         context_text, facts, events, has_active_facts, context_used,
                   retrieval_agent, adaptive_retrieval, facts_db_rows,
                   facts_in_context, event_scan_used, stale_facts_in_context,
                   facts_dropped_by_decay
  chat_router.py:  same as main.py + clusters_used, clusters_facts_injected,
                   emotional_insights_enabled, emotional_trend_injected,
                   emotional_dominant_emotion, emotional_trend_direction,
                   facts_dropped_by_sim_threshold, events_dropped_by_sim_threshold,
                   has_events_only
  retrieval_agent sub-keys: strategy, reason, latency_ms

RETRIEVE RESULT > facts[] item keys:
  fact, score, entity, attribute, value, date, source

RETRIEVE RESULT > context_used sub-keys:
  facts_used, events_used
"""

from __future__ import annotations

from dataclasses import dataclass, field

# ---------------------------------------------------------------------------
# Minimal stubs — replicate SDK result types for isolated contract testing
# (no dependency on advisor_api or its settings)
# ---------------------------------------------------------------------------


@dataclass
class _WriteResult:
    event_id: str = ""
    facts_added: list = field(default_factory=list)
    facts_updated: list = field(default_factory=list)
    facts_unchanged: int = 0
    facts_deleted: int = 0
    entities_resolved: list = field(default_factory=list)
    duration_ms: float = 0.0


@dataclass
class _ScoredFact:
    fact_id: str = ""
    entity_name: str = ""
    attribute_key: str = ""
    value: str = ""
    score: float = 0.0
    scores: dict = field(default_factory=dict)


@dataclass
class _RetrieveResult:
    facts: list[_ScoredFact] = field(default_factory=list)
    context: str = ""
    total_candidates: int = 0
    duration_ms: float = 0.0


# ---------------------------------------------------------------------------
# Adapter logic under test — extracted inline to avoid importing advisor_api
# ---------------------------------------------------------------------------


def _adapt_write_result(result: _WriteResult) -> dict:
    """Exact copy of sdk_adapter._adapt_write_result logic."""
    return {
        "event_id": result.event_id,
        "facts_added": result.facts_added,
        "facts_updated": result.facts_updated,
        "facts_unchanged": result.facts_unchanged,
        "facts_deleted": result.facts_deleted,
        "entities_resolved": result.entities_resolved,
        "duration_ms": result.duration_ms,
    }


def _adapt_retrieve_result(result: _RetrieveResult) -> dict:
    """Exact copy of sdk_adapter._adapt_retrieve_result logic."""
    facts_for_context = [
        {
            "fact": None,
            "score": f.score,
            "entity": f.entity_name,
            "attribute": f.attribute_key or f.value,
            "value": f.value,
            "date": "",
            "source": "sdk",
        }
        for f in result.facts
    ]
    return {
        "facts": facts_for_context,
        "events": [],
        "context_text": result.context,
        "has_active_facts": len(result.facts) > 0,
        "has_events_only": False,
        "context_used": {
            "facts_used": [{"fact_id": f.fact_id, "entity": f.entity_name, "value": f.value} for f in result.facts],
            "events_used": [],
        },
        "facts_db_rows": result.total_candidates,
        "facts_in_context": len(result.facts),
        "event_scan_used": 0,
        "stale_facts_in_context": 0,
        "facts_dropped_by_decay": 0,
        "retrieval_agent": {
            "strategy": "sdk_v0",
            "reason": "sdk_adapter",
            "latency_ms": result.duration_ms,
        },
    }


# ---------------------------------------------------------------------------
# Required keys — canonical registry of what advisor_api consumers access
# ---------------------------------------------------------------------------

WRITE_RESULT_REQUIRED_KEYS = {
    "event_id",
    "facts_added",
    "facts_updated",
    "facts_unchanged",
    "facts_deleted",
    "entities_resolved",
    "duration_ms",
}

RETRIEVE_RESULT_REQUIRED_KEYS = {
    "facts",
    "events",
    "context_text",
    "has_active_facts",
    "has_events_only",
    "context_used",
    "facts_db_rows",
    "facts_in_context",
    "event_scan_used",
    "stale_facts_in_context",
    "facts_dropped_by_decay",
    "retrieval_agent",
}

RETRIEVE_FACT_ITEM_REQUIRED_KEYS = {
    "fact",
    "score",
    "entity",
    "attribute",
    "value",
    "date",
    "source",
}

RETRIEVE_CONTEXT_USED_REQUIRED_KEYS = {"facts_used", "events_used"}

RETRIEVAL_AGENT_REQUIRED_KEYS = {"strategy", "reason", "latency_ms"}


# ===========================================================================
# Write result contract tests
# ===========================================================================


def test_adapt_write_result_has_all_required_keys():
    result = _adapt_write_result(_WriteResult())
    assert WRITE_RESULT_REQUIRED_KEYS.issubset(result.keys()), (
        f"Missing keys: {WRITE_RESULT_REQUIRED_KEYS - result.keys()}"
    )


def test_adapt_write_result_event_id_is_str():
    result = _adapt_write_result(_WriteResult(event_id="evt-123"))
    assert isinstance(result["event_id"], str)


def test_adapt_write_result_facts_are_lists():
    result = _adapt_write_result(
        _WriteResult(
            facts_added=[{"id": "a"}],
            facts_updated=[{"id": "b"}],
        )
    )
    assert isinstance(result["facts_added"], list)
    assert isinstance(result["facts_updated"], list)
    assert isinstance(result["entities_resolved"], list)


def test_adapt_write_result_counts_are_numeric():
    result = _adapt_write_result(_WriteResult(facts_unchanged=3, facts_deleted=1))
    assert isinstance(result["facts_unchanged"], int)
    assert isinstance(result["facts_deleted"], int)
    assert isinstance(result["duration_ms"], float)


def test_adapt_write_result_with_trace_adds_trace_key():
    """process_memory_write_with_trace adds result["trace"] = None."""
    result = _adapt_write_result(_WriteResult())
    result["trace"] = None  # simulates what the adapter does
    assert "trace" in result
    assert result["trace"] is None


# ===========================================================================
# Retrieve result contract tests
# ===========================================================================


def test_adapt_retrieve_result_has_all_required_keys():
    result = _adapt_retrieve_result(_RetrieveResult())
    assert RETRIEVE_RESULT_REQUIRED_KEYS.issubset(result.keys()), (
        f"Missing keys: {RETRIEVE_RESULT_REQUIRED_KEYS - result.keys()}"
    )


def test_adapt_retrieve_result_context_text_is_str():
    result = _adapt_retrieve_result(_RetrieveResult(context="hello world"))
    assert isinstance(result["context_text"], str)
    assert result["context_text"] == "hello world"


def test_adapt_retrieve_result_facts_list_structure():
    facts = [_ScoredFact(fact_id="f1", entity_name="Alice", value="likes dogs", score=0.9)]
    result = _adapt_retrieve_result(_RetrieveResult(facts=facts))
    assert isinstance(result["facts"], list)
    assert len(result["facts"]) == 1
    fact_item = result["facts"][0]
    assert RETRIEVE_FACT_ITEM_REQUIRED_KEYS.issubset(fact_item.keys()), (
        f"Missing fact keys: {RETRIEVE_FACT_ITEM_REQUIRED_KEYS - fact_item.keys()}"
    )


def test_adapt_retrieve_result_fact_item_types():
    facts = [
        _ScoredFact(
            fact_id="f1",
            entity_name="Alice",
            attribute_key="pet",
            value="dog",
            score=0.85,
        )
    ]
    result = _adapt_retrieve_result(_RetrieveResult(facts=facts))
    item = result["facts"][0]
    assert item["fact"] is None
    assert isinstance(item["score"], float)
    assert isinstance(item["entity"], str)
    assert isinstance(item["attribute"], str)
    assert isinstance(item["value"], str)
    assert isinstance(item["date"], str)
    assert isinstance(item["source"], str)


def test_adapt_retrieve_result_context_used_structure():
    facts = [_ScoredFact(fact_id="f1", entity_name="Alice", value="dog")]
    result = _adapt_retrieve_result(_RetrieveResult(facts=facts))
    context_used = result["context_used"]
    assert RETRIEVE_CONTEXT_USED_REQUIRED_KEYS.issubset(context_used.keys())
    assert isinstance(context_used["facts_used"], list)
    assert isinstance(context_used["events_used"], list)
    assert len(context_used["facts_used"]) == 1
    assert "fact_id" in context_used["facts_used"][0]
    assert "entity" in context_used["facts_used"][0]
    assert "value" in context_used["facts_used"][0]


def test_adapt_retrieve_result_retrieval_agent_structure():
    result = _adapt_retrieve_result(_RetrieveResult(duration_ms=42.5))
    agent = result["retrieval_agent"]
    assert RETRIEVAL_AGENT_REQUIRED_KEYS.issubset(agent.keys()), (
        f"Missing agent keys: {RETRIEVAL_AGENT_REQUIRED_KEYS - agent.keys()}"
    )
    assert isinstance(agent["strategy"], str)
    assert isinstance(agent["reason"], str)
    assert isinstance(agent["latency_ms"], float)
    assert agent["latency_ms"] == 42.5


def test_adapt_retrieve_result_boolean_fields():
    result = _adapt_retrieve_result(_RetrieveResult())
    assert isinstance(result["has_active_facts"], bool)
    assert isinstance(result["has_events_only"], bool)
    assert result["has_active_facts"] is False  # no facts
    assert result["has_events_only"] is False


def test_adapt_retrieve_result_has_active_facts_true_when_facts_present():
    facts = [_ScoredFact(fact_id="f1")]
    result = _adapt_retrieve_result(_RetrieveResult(facts=facts))
    assert result["has_active_facts"] is True


def test_adapt_retrieve_result_integer_fields():
    result = _adapt_retrieve_result(_RetrieveResult(total_candidates=50))
    assert isinstance(result["facts_db_rows"], int)
    assert isinstance(result["facts_in_context"], int)
    assert isinstance(result["event_scan_used"], int)
    assert isinstance(result["stale_facts_in_context"], int)
    assert isinstance(result["facts_dropped_by_decay"], int)
    assert result["facts_db_rows"] == 50


def test_adapt_retrieve_result_empty_events():
    result = _adapt_retrieve_result(_RetrieveResult())
    assert result["events"] == []


def test_adapt_retrieve_result_optional_keys_safely_absent():
    """Keys accessed via .get() in advisor_api — safe if absent from adapter output."""
    result = _adapt_retrieve_result(_RetrieveResult())
    # These keys are consumed via .get() with defaults, so absence is safe.
    # The adapter does NOT produce them — this test documents that contract.
    optional_keys = [
        "adaptive_retrieval",
        "clusters_used",
        "clusters_facts_injected",
        "emotional_insights_enabled",
        "emotional_trend_injected",
        "emotional_dominant_emotion",
        "emotional_trend_direction",
        "facts_dropped_by_sim_threshold",
        "events_dropped_by_sim_threshold",
    ]
    for key in optional_keys:
        # Consumers use .get() so absence is fine — just document it
        assert result.get(key) is None or key not in result


# ===========================================================================
# SDK result type contract tests (WriteResult, RetrieveResult, ScoredFact)
# ===========================================================================


def test_sdk_write_result_has_required_fields():
    from arandu.client import WriteResult

    wr = WriteResult()
    for field_name in [
        "event_id",
        "facts_added",
        "facts_updated",
        "facts_unchanged",
        "facts_deleted",
        "entities_resolved",
        "duration_ms",
    ]:
        assert hasattr(wr, field_name), f"WriteResult missing field: {field_name}"


def test_sdk_retrieve_result_has_required_fields():
    from arandu.client import RetrieveResult

    rr = RetrieveResult()
    for field_name in ["facts", "context", "total_candidates", "duration_ms"]:
        assert hasattr(rr, field_name), f"RetrieveResult missing field: {field_name}"


def test_sdk_scored_fact_has_required_fields():
    from arandu.client import ScoredFact

    sf = ScoredFact()
    for field_name in ["fact_id", "entity_name", "attribute_key", "value", "score"]:
        assert hasattr(sf, field_name), f"ScoredFact missing field: {field_name}"
