"""
Story 7.04 — Test Suite
=======================
POST /conversations — create royal_conversations row on call start

BB Tests (4):
  BB1: Valid payload → 201, conversation_id returned in UUID4 format
  BB2: Duplicate call_id → 409 Conflict
  BB3: Missing required field (no session_id) → 422 Unprocessable Entity
  BB4: Response always contains status="active"

WB Tests (4):
  WB1: started_at is set — INSERT includes NOW() in the executed SQL
  WB2: Postgres INSERT uses the conversation_id UUID returned to the caller
  WB3: 409 originates from duplicate/unique keyword in the DB exception message
  WB4: No DB connection (pg_conn=None) → 201 with fresh UUID (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 re
from unittest.mock import MagicMock, call, patch

import pytest
from fastapi.testclient import TestClient

from api.aiva_memory_api import app, get_pg_connection

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

UUID4_RE = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
    re.IGNORECASE,
)

VALID_PAYLOAD = {
    "session_id": "sess-001",
    "call_id": "call-abc-123",
    "participants": {"caller": "Kinan", "receiver": "AIVA"},
}


def make_pg_conn() -> MagicMock:
    """Return a mock psycopg2 connection with cursor, execute, commit, close."""
    cursor = MagicMock()
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


def make_pg_conn_duplicate() -> MagicMock:
    """Return a mock psycopg2 connection whose execute raises a duplicate error."""
    cursor = MagicMock()
    cursor.execute.side_effect = Exception(
        "duplicate key value violates unique constraint"
    )
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


def make_pg_conn_generic_error() -> MagicMock:
    """Return a mock psycopg2 connection that raises a non-duplicate error."""
    cursor = MagicMock()
    cursor.execute.side_effect = RuntimeError("connection lost")
    conn = MagicMock()
    conn.cursor.return_value = cursor
    return conn


# ---------------------------------------------------------------------------
# BB1 — Valid payload → 201, UUID conversation_id
# ---------------------------------------------------------------------------


def test_bb1_valid_payload_returns_201():
    """BB1: POST /conversations with all required fields returns HTTP 201."""
    conn = make_pg_conn()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201, response.text
    finally:
        app.dependency_overrides.clear()


def test_bb1_response_has_uuid_conversation_id():
    """BB1: conversation_id in the response is a valid UUID4 string."""
    conn = make_pg_conn()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        body = response.json()
        assert "conversation_id" in body, "conversation_id missing from response"
        assert UUID4_RE.match(body["conversation_id"]), (
            f"conversation_id '{body['conversation_id']}' is not a valid UUID4"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB2 — Duplicate call_id → 409 Conflict
# ---------------------------------------------------------------------------


def test_bb2_duplicate_call_id_returns_409():
    """BB2: When call_id already exists (DB unique constraint) → 409 Conflict."""
    conn = make_pg_conn_duplicate()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 409, (
            f"Expected 409 for duplicate call_id, got {response.status_code}: {response.text}"
        )
    finally:
        app.dependency_overrides.clear()


def test_bb2_duplicate_response_has_detail():
    """BB2: 409 response includes a detail message mentioning the conflict."""
    conn = make_pg_conn_duplicate()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        body = response.json()
        assert "detail" in body, "409 response must contain a 'detail' key"
        assert body["detail"], "detail must be a non-empty string"
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB3 — Missing required field → 422 Unprocessable Entity
# ---------------------------------------------------------------------------


def test_bb3_missing_session_id_returns_422():
    """BB3: Omitting session_id (required field) → FastAPI returns 422."""
    conn = make_pg_conn()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        incomplete_payload = {"call_id": "call-no-session"}
        response = client.post("/conversations", json=incomplete_payload)
        assert response.status_code == 422, (
            f"Expected 422 for missing session_id, got {response.status_code}"
        )
    finally:
        app.dependency_overrides.clear()


def test_bb3_missing_call_id_returns_422():
    """BB3: Omitting call_id (required field) → FastAPI returns 422."""
    conn = make_pg_conn()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        incomplete_payload = {"session_id": "sess-001"}
        response = client.post("/conversations", json=incomplete_payload)
        assert response.status_code == 422, (
            f"Expected 422 for missing call_id, got {response.status_code}"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# BB4 — Response always has status="active"
# ---------------------------------------------------------------------------


def test_bb4_status_is_active_with_db():
    """BB4: Response body has status='active' when DB is available."""
    conn = make_pg_conn()
    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201
        assert response.json()["status"] == "active"
    finally:
        app.dependency_overrides.clear()


def test_bb4_status_is_active_without_db():
    """BB4: Response body has status='active' even when no DB (graceful degradation)."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201
        assert response.json()["status"] == "active"
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB1 — started_at is set (INSERT includes NOW())
# ---------------------------------------------------------------------------


def test_wb1_insert_uses_now_for_started_at():
    """
    WB1: The INSERT SQL passed to cursor.execute must include 'NOW()' so that
    Postgres sets started_at to the current UTC timestamp.
    """
    conn = make_pg_conn()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201

        # Verify cursor.execute was called and inspect the SQL
        assert cursor.execute.called, "cursor.execute should have been called"
        sql_arg = cursor.execute.call_args[0][0]
        assert "NOW()" in sql_arg, (
            f"INSERT SQL must contain NOW() for started_at, got: {sql_arg!r}"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB2 — Postgres INSERT uses the UUID returned to the caller
# ---------------------------------------------------------------------------


def test_wb2_insert_uses_same_uuid_as_response():
    """
    WB2: The conversation_id inserted into Postgres is the SAME UUID that the
    endpoint returns to the caller — no mismatch between DB row and response.
    """
    conn = make_pg_conn()
    cursor = conn.cursor.return_value

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201

        returned_uuid = response.json()["conversation_id"]

        # The first positional argument tuple of cursor.execute should contain
        # the same UUID as the value at index 0.
        params = cursor.execute.call_args[0][1]
        inserted_uuid = str(params[0])
        assert inserted_uuid == returned_uuid, (
            f"DB row UUID ({inserted_uuid}) != response UUID ({returned_uuid})"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB3 — 409 from DB constraint detection (unique/duplicate in error message)
# ---------------------------------------------------------------------------


def test_wb3_409_triggered_by_unique_keyword_in_exception():
    """
    WB3: The 409 is raised specifically because the exception message contains
    'unique' — not from any other error type.
    """
    cursor = MagicMock()
    cursor.execute.side_effect = Exception("unique constraint violated")
    conn = MagicMock()
    conn.cursor.return_value = cursor

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 409
    finally:
        app.dependency_overrides.clear()


def test_wb3_409_triggered_by_duplicate_keyword_in_exception():
    """
    WB3: The 409 is also raised when the exception message contains 'duplicate'
    (Postgres standard duplicate key wording).
    """
    cursor = MagicMock()
    cursor.execute.side_effect = Exception(
        "duplicate key value violates unique constraint"
    )
    conn = MagicMock()
    conn.cursor.return_value = cursor

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 409
    finally:
        app.dependency_overrides.clear()


def test_wb3_non_duplicate_exception_is_not_409():
    """
    WB3: A generic DB error (not duplicate/unique) must NOT be swallowed as 409;
    it should propagate and produce a 500 from FastAPI.
    """
    conn = make_pg_conn_generic_error()

    app.dependency_overrides[get_pg_connection] = lambda: conn
    try:
        client = TestClient(app, raise_server_exceptions=False)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 500, (
            f"Generic DB error should produce 500, got {response.status_code}"
        )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# WB4 — No DB connection → 201 with UUID (graceful degradation)
# ---------------------------------------------------------------------------


def test_wb4_no_db_returns_201():
    """WB4: When pg_conn is None the endpoint degrades gracefully → 201."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        assert response.status_code == 201
    finally:
        app.dependency_overrides.clear()


def test_wb4_no_db_returns_valid_uuid():
    """WB4: Graceful degradation still returns a valid UUID4 conversation_id."""
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        body = response.json()
        assert "conversation_id" in body
        assert UUID4_RE.match(body["conversation_id"]), (
            f"Offline UUID '{body['conversation_id']}' is not a valid UUID4"
        )
    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 — confirming no attempt to use the None reference as a cursor.
    """
    # We pass a sentinel MagicMock as the override but return None from it so
    # the endpoint sees None and takes the degraded path.
    app.dependency_overrides[get_pg_connection] = lambda: None
    try:
        client = TestClient(app)
        response = client.post("/conversations", json=VALID_PAYLOAD)
        # If any cursor call happened we'd get an AttributeError on None → 500
        assert response.status_code == 201
    finally:
        app.dependency_overrides.clear()
