"""
Story 7.05 — Test Suite
=======================
PATCH /conversations/{conversation_id}/enrich
Triggered by PostCallEnricher to write enrichment data to an existing
royal_conversations row and set ended_at = NOW().

BB Tests (4):
  BB1: Valid enrich → 200, status="enriched", conversation_id echoed
  BB2: Unknown conversation_id (RETURNING id is None) → 404
  BB3: ended_at set in DB — UPDATE SQL includes NOW()
  BB4: All 8 enrichment fields present in the payload are written

WB Tests (4):
  WB1: UPDATE query used (not INSERT) — SQL contains UPDATE, not INSERT
  WB2: All 8 enrichment fields written in single UPDATE statement
  WB3: ended_at = NOW() present in the UPDATE SQL
  WB4: No pg_conn (None) → 200 with status="enriched" (graceful degradation)

All external services are mocked via app.dependency_overrides — no real
Postgres is touched during this test suite.
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

from api.aiva_memory_api import app, get_pg_connection

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

CONV_ID = "550e8400-e29b-41d4-a716-446655440000"

FULL_PAYLOAD = {
    "transcript_raw": "Kinan: Hello AIVA. AIVA: Hello Kinan.",
    "enriched_entities": ["Kinan", "AIVA", "Genesis"],
    "decisions_made": ["Launch TradiesVoice Q1 2026"],
    "action_items": ["Book demo", "Send pricing sheet"],
    "emotional_signal": "positive",
    "key_facts": ["Revenue target $500K ARR", "38 agency leads"],
    "kinan_directives": ["Never ask Kinan to do browser tasks"],
    "memory_vector_id": "vec-abc-001",
}


def make_pg_conn_found() -> MagicMock:
    """
    Returns a mock psycopg2 connection where RETURNING id yields a row —
    simulating a successful UPDATE (conversation found).
    """
    cursor = MagicMock()
    cursor.fetchone.return_value = (CONV_ID,)
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


def make_pg_conn_not_found() -> MagicMock:
    """
    Returns a mock psycopg2 connection where RETURNING id yields None —
    simulating a failed UPDATE because the conversation_id doesn't exist.
    """
    cursor = MagicMock()
    cursor.fetchone.return_value = None
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


def make_pg_conn_db_error() -> MagicMock:
    """Returns a mock where cursor.execute raises a generic DB error."""
    cursor = MagicMock()
    cursor.execute.side_effect = RuntimeError("connection lost")
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


# ---------------------------------------------------------------------------
# BB1 — Valid enrich → 200, status="enriched", conversation_id echoed
# ---------------------------------------------------------------------------


def test_bb1_valid_enrich_returns_200():
    """BB1: PATCH with a known conversation_id returns HTTP 200."""
    conn = make_pg_conn_found()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200, response.text
    finally:
        app.dependency_overrides.clear()


def test_bb1_status_is_enriched():
    """BB1: Response body contains status='enriched'."""
    conn = make_pg_conn_found()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200
        body = response.json()
        assert body["status"] == "enriched", (
            f"Expected status='enriched', got '{body['status']}'"
        )
    finally:
        app.dependency_overrides.clear()


def test_bb1_conversation_id_echoed():
    """BB1: Response body echoes the conversation_id from the URL."""
    conn = make_pg_conn_found()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200
        body = response.json()
        assert body["conversation_id"] == CONV_ID, (
            f"Expected conversation_id='{CONV_ID}', got '{body['conversation_id']}'"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB2 — Unknown conversation_id → 404
# ---------------------------------------------------------------------------


def test_bb2_unknown_conversation_id_returns_404():
    """BB2: When RETURNING id is None (row not found) → 404 Not Found."""
    conn = make_pg_conn_not_found()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            "/conversations/nonexistent-id/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 404, (
            f"Expected 404 for unknown conversation_id, got {response.status_code}: {response.text}"
        )
    finally:
        app.dependency_overrides.clear()


def test_bb2_404_response_has_detail():
    """BB2: The 404 response body contains a non-empty 'detail' key."""
    conn = make_pg_conn_not_found()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            "/conversations/nonexistent-id/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 404
        body = response.json()
        assert "detail" in body, "404 response must contain 'detail'"
        assert body["detail"], "detail must be non-empty"
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB3 — ended_at set in DB — UPDATE SQL includes NOW()
# ---------------------------------------------------------------------------


def test_bb3_ended_at_set_via_now():
    """
    BB3: The UPDATE SQL passed to cursor.execute must contain 'ended_at = NOW()'
    so that Postgres sets ended_at to the current UTC timestamp.
    """
    conn = make_pg_conn_found()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200

        assert cursor.execute.called, "cursor.execute must be called"
        sql_arg = cursor.execute.call_args[0][0]
        assert "ended_at" in sql_arg, "SQL must reference ended_at column"
        assert "NOW()" in sql_arg, (
            f"SQL must set ended_at = NOW(), got: {sql_arg!r}"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB4 — All 8 enrichment fields present in payload are written
# ---------------------------------------------------------------------------


def test_bb4_all_eight_fields_written():
    """
    BB4: The params tuple passed to cursor.execute contains all 8 enrichment
    field values from the payload (transcript_raw, enriched_entities,
    decisions_made, action_items, emotional_signal, key_facts,
    kinan_directives, memory_vector_id).
    """
    conn = make_pg_conn_found()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200

        params = cursor.execute.call_args[0][1]

        # params order: transcript_raw, enriched_entities, decisions_made,
        #               action_items, emotional_signal, key_facts,
        #               kinan_directives, memory_vector_id, conversation_id
        assert len(params) == 9, f"Expected 9 params (8 fields + id), got {len(params)}"

        # Index 0: transcript_raw (plain string)
        assert params[0] == FULL_PAYLOAD["transcript_raw"], "transcript_raw mismatch"

        # Indices 1–3 and 5–6: JSON-serialised list fields
        assert json.loads(params[1]) == FULL_PAYLOAD["enriched_entities"], "enriched_entities mismatch"
        assert json.loads(params[2]) == FULL_PAYLOAD["decisions_made"], "decisions_made mismatch"
        assert json.loads(params[3]) == FULL_PAYLOAD["action_items"], "action_items mismatch"

        # Index 4: emotional_signal (plain string)
        assert params[4] == FULL_PAYLOAD["emotional_signal"], "emotional_signal mismatch"

        assert json.loads(params[5]) == FULL_PAYLOAD["key_facts"], "key_facts mismatch"
        assert json.loads(params[6]) == FULL_PAYLOAD["kinan_directives"], "kinan_directives mismatch"

        # Index 7: memory_vector_id (plain string)
        assert params[7] == FULL_PAYLOAD["memory_vector_id"], "memory_vector_id mismatch"

        # Index 8: conversation_id (the WHERE clause)
        assert str(params[8]) == CONV_ID, "conversation_id (WHERE) mismatch"
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB1 — UPDATE query used (not INSERT)
# ---------------------------------------------------------------------------


def test_wb1_sql_uses_update_not_insert():
    """
    WB1: The SQL string passed to cursor.execute starts with UPDATE, confirming
    the endpoint modifies an existing row rather than creating a new one.
    """
    conn = make_pg_conn_found()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200

        sql_arg = cursor.execute.call_args[0][0]
        # Normalise whitespace for reliable assertion
        sql_normalised = " ".join(sql_arg.split()).upper()
        assert sql_normalised.startswith("UPDATE"), (
            f"SQL must start with UPDATE, got: {sql_arg!r}"
        )
        assert "INSERT" not in sql_normalised, (
            "SQL must NOT contain INSERT — this is an update, not a create"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB2 — All 8 enrichment fields written in single UPDATE statement
# ---------------------------------------------------------------------------


def test_wb2_single_update_contains_all_eight_column_names():
    """
    WB2: The single UPDATE SQL string references all 8 enrichment column names
    — confirming a single atomic write rather than multiple round-trips.
    """
    conn = make_pg_conn_found()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200

        # Only one execute call should have been made
        assert cursor.execute.call_count == 1, (
            f"Expected 1 execute call, got {cursor.execute.call_count}"
        )

        sql_arg = cursor.execute.call_args[0][0]
        required_columns = [
            "transcript_raw",
            "enriched_entities",
            "decisions_made",
            "action_items",
            "emotional_signal",
            "key_facts",
            "kinan_directives",
            "memory_vector_id",
        ]
        for col in required_columns:
            assert col in sql_arg, (
                f"Column '{col}' missing from UPDATE SQL: {sql_arg!r}"
            )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB3 — ended_at = NOW() in SQL
# ---------------------------------------------------------------------------


def test_wb3_sql_sets_ended_at_now():
    """
    WB3: The UPDATE SQL explicitly sets ended_at = NOW() — not a Python-side
    timestamp — ensuring Postgres uses the authoritative server time.
    """
    conn = make_pg_conn_found()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200

        sql_arg = cursor.execute.call_args[0][0]
        # Verify the literal NOW() appears (Postgres function call)
        assert "NOW()" in sql_arg, (
            f"SQL must contain NOW() for ended_at, got: {sql_arg!r}"
        )
        # Confirm ended_at and NOW() are in the same SET clause
        sql_upper = sql_arg.upper()
        ended_at_pos = sql_upper.index("ENDED_AT")
        now_pos = sql_upper.index("NOW()")
        # ended_at must appear before NOW() and both must be in SET block
        assert ended_at_pos < now_pos, (
            "ended_at column reference must appear before NOW() in the SQL"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB4 — No pg_conn → 200 with status="enriched" (graceful degradation)
# ---------------------------------------------------------------------------


def test_wb4_no_db_returns_200():
    """WB4: When pg_conn=None the endpoint degrades gracefully → 200."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        assert response.status_code == 200, response.text
    finally:
        app.dependency_overrides.clear()


def test_wb4_no_db_status_is_enriched():
    """WB4: Graceful degradation still returns status='enriched'."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        body = response.json()
        assert body["status"] == "enriched", (
            f"Expected status='enriched' in offline mode, got '{body['status']}'"
        )
    finally:
        app.dependency_overrides.clear()


def test_wb4_no_db_conversation_id_echoed():
    """WB4: Graceful degradation echoes the conversation_id from the URL."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        body = response.json()
        assert body["conversation_id"] == CONV_ID, (
            f"Expected conversation_id='{CONV_ID}', got '{body['conversation_id']}'"
        )
    finally:
        app.dependency_overrides.clear()


def test_wb4_no_db_skips_postgres_entirely():
    """
    WB4: When pg_conn=None the handler returns immediately WITHOUT calling any
    cursor methods — no AttributeError on None reference.
    """
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.patch(
            f"/conversations/{CONV_ID}/enrich", json=FULL_PAYLOAD
        )
        # If cursor was called on None we'd get a 500 — confirm we get 200
        assert response.status_code == 200
    finally:
        app.dependency_overrides.clear()
