#!/usr/bin/env python3
"""
Tests for Story 2.05: KingRegistry — Qdrant Semantic Write
AIVA RLM Nexus — Track A
Module: core/registry/king_registry.py

Black Box + White Box tests using fully mocked Qdrant client.
No live Qdrant connection required.

Test plan:
  BB1: add_directive → _write_to_qdrant called (verify via mock)
  BB2: search_directives returns list of dicts with directive_id, text, score
  BB3: Qdrant fails → add_directive still succeeds (Postgres written)
  BB4: search_directives with no matches → returns []

  WB1: Upsert used (not insert) — verify upsert method called on mock client
  WB2: Embedding is 768 dimensions
  WB3: Qdrant failure logged as warning
  WB4: search_directives Qdrant failure → returns [] (not exception)
  WB5: _write_to_qdrant returns True on success, False on failure

  REGRESSION: Existing 2.03 and 2.04 tests still pass
"""
import sys
import uuid
import json
import warnings
from pathlib import Path
from unittest.mock import MagicMock, patch, call

sys.path.insert(0, '/mnt/e/genesis-system')

from core.registry.king_registry import KingRegistry, RegistryError


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_mock_cf(rowcount: int = 1, rows=None):
    """Build a mock ConnectionFactory for Postgres interactions."""
    mock_cur = MagicMock()
    mock_cur.fetchall.return_value = rows if rows is not None else []
    mock_cur.rowcount = rowcount

    mock_conn = MagicMock()
    mock_conn.cursor.return_value = mock_cur

    mock_cf = MagicMock()
    mock_cf.get_postgres.return_value = mock_conn

    return mock_cf, mock_conn, mock_cur


def _make_scored_point(directive_id: str, text: str, score: float):
    """Build a mock ScoredPoint as returned by QdrantClient.query_points()."""
    point = MagicMock()
    point.id = directive_id
    point.score = score
    point.payload = {
        "directive_id": directive_id,
        "text": text,
        "priority": 3,
        "status": "active",
    }
    return point


def _make_mock_qdrant_client(points=None, upsert_ok=True):
    """
    Build a mock QdrantClient.

    Args:
        points:    List of mock ScoredPoints returned by query_points().
        upsert_ok: If False, upsert raises an Exception.
    """
    mock_client = MagicMock()

    # get_collections — return empty list (no existing collections)
    mock_collections_response = MagicMock()
    mock_collections_response.collections = []
    mock_client.get_collections.return_value = mock_collections_response

    if upsert_ok:
        mock_client.upsert.return_value = MagicMock()
    else:
        mock_client.upsert.side_effect = Exception("Qdrant connection refused")

    # query_points returns a QueryResponse with .points attribute
    mock_query_response = MagicMock()
    mock_query_response.points = points if points is not None else []
    mock_client.query_points.return_value = mock_query_response

    return mock_client


# ---------------------------------------------------------------------------
# BLACK BOX TESTS
# ---------------------------------------------------------------------------

def test_bb1_add_directive_calls_write_to_qdrant():
    """BB1: add_directive → _write_to_qdrant is called after successful Postgres write."""
    mock_cf, _, _ = _make_mock_cf(rowcount=1)
    kr = KingRegistry(connection_factory=mock_cf)

    # Spy on _write_to_qdrant
    kr._write_to_qdrant = MagicMock(return_value=True)

    directive_id = kr.add_directive("Deploy Instantly campaign", 3, "voice")

    assert isinstance(directive_id, str), "add_directive must return a string UUID"
    uuid.UUID(directive_id)  # Raises if not a valid UUID

    kr._write_to_qdrant.assert_called_once()
    call_args = kr._write_to_qdrant.call_args
    # Positional or keyword — handle both
    args = call_args[0]
    assert args[0] == directive_id, "First arg must be the directive_id"
    assert args[1] == "Deploy Instantly campaign", "Second arg must be the text"
    assert args[2] == 3, "Third arg must be the priority"
    assert args[3] == "active", "Fourth arg must be 'active'"
    print(f"BB1 PASS: add_directive → _write_to_qdrant called with correct args")


def test_bb2_search_directives_returns_list_of_dicts():
    """BB2: search_directives returns list of dicts with directive_id, text, score."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    fixed_id = str(uuid.uuid4())
    mock_points = [_make_scored_point(fixed_id, "Build tradie scraper", 0.92)]
    mock_qdrant = _make_mock_qdrant_client(points=mock_points)

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        results = kr.search_directives("tradie scraper tool", top_k=3)

    assert isinstance(results, list), "search_directives must return a list"
    assert len(results) == 1, f"Expected 1 result, got {len(results)}"

    item = results[0]
    assert "directive_id" in item, "Result must have 'directive_id' key"
    assert "text" in item, "Result must have 'text' key"
    assert "score" in item, "Result must have 'score' key"
    assert item["directive_id"] == fixed_id
    assert item["text"] == "Build tradie scraper"
    assert abs(item["score"] - 0.92) < 1e-6, f"Unexpected score: {item['score']}"
    print("BB2 PASS: search_directives returns correct list of dicts")


def test_bb3_qdrant_failure_does_not_fail_add_directive():
    """BB3: Qdrant fails → add_directive still succeeds (Postgres is source of truth)."""
    mock_cf, mock_conn, _ = _make_mock_cf(rowcount=1)
    kr = KingRegistry(connection_factory=mock_cf)

    # _write_to_qdrant raises — must be silently swallowed
    kr._write_to_qdrant = MagicMock(side_effect=Exception("Qdrant down"))

    directive_id = kr.add_directive("Check Telnyx logs", 2, "text")

    assert isinstance(directive_id, str), "add_directive should still return a UUID"
    uuid.UUID(directive_id)

    # Postgres commit still happened
    mock_conn.commit.assert_called_once()
    print("BB3 PASS: Qdrant failure is non-fatal — add_directive succeeds via Postgres")


def test_bb4_search_directives_no_matches_returns_empty_list():
    """BB4: search_directives with no matching points → returns []."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    mock_qdrant = _make_mock_qdrant_client(points=[])  # no matches

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        results = kr.search_directives("something with no match", top_k=5)

    assert results == [], f"Expected empty list, got {results!r}"
    print("BB4 PASS: search_directives with no matches returns []")


# ---------------------------------------------------------------------------
# WHITE BOX TESTS
# ---------------------------------------------------------------------------

def test_wb1_upsert_called_not_insert():
    """WB1: Upsert (not insert) is used — upsert method called on mock client."""
    mock_cf, _, _ = _make_mock_cf(rowcount=1)
    kr = KingRegistry(connection_factory=mock_cf)

    mock_qdrant = _make_mock_qdrant_client()

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        result = kr._write_to_qdrant(str(uuid.uuid4()), "Test directive", 1, "active")

    assert result is True, "_write_to_qdrant should return True on success"
    mock_qdrant.upsert.assert_called_once()

    # Ensure upsert (not some insert method) was called
    upsert_call = mock_qdrant.upsert.call_args
    assert upsert_call is not None, "upsert must be called exactly once"
    print("WB1 PASS: upsert() used (not insert) in _write_to_qdrant")


def test_wb2_embedding_is_768_dimensions():
    """WB2: _text_to_embedding returns exactly 768 floats."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    for text in ["hello", "Launch tradie scraper by Friday", "", "x" * 500]:
        embedding = kr._text_to_embedding(text)
        assert isinstance(embedding, list), f"Expected list, got {type(embedding)}"
        assert len(embedding) == 768, f"Expected 768 dims, got {len(embedding)} for {text!r}"
        # All values must be floats in [-1, 1]
        for val in embedding:
            assert isinstance(val, float), f"Embedding contains non-float: {val!r}"
            assert -1.0 <= val <= 1.0, f"Embedding value out of [-1, 1]: {val}"

    print("WB2 PASS: _text_to_embedding returns 768 floats all in [-1, 1]")


def test_wb3_qdrant_failure_logged_as_warning():
    """WB3: Qdrant write failure → warning logged to events.jsonl (never raises)."""
    mock_cf, _, _ = _make_mock_cf(rowcount=1)
    kr = KingRegistry(connection_factory=mock_cf)

    logged_events = []

    def _fake_log(context, error):
        logged_events.append({"context": context, "error": error})

    kr._log_qdrant_warning = _fake_log

    # Patch QdrantClient to raise during upsert
    mock_qdrant = _make_mock_qdrant_client(upsert_ok=False)

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        result = kr._write_to_qdrant(str(uuid.uuid4()), "Directive text", 3, "active")

    assert result is False, "_write_to_qdrant should return False on failure"
    assert len(logged_events) == 1, f"Expected 1 warning logged, got {len(logged_events)}"
    assert "Qdrant" in logged_events[0]["error"] or "connection" in logged_events[0]["error"].lower()
    print("WB3 PASS: Qdrant write failure logged as warning, returns False")


def test_wb4_search_qdrant_failure_returns_empty_list():
    """WB4: search_directives Qdrant failure → returns [] (not exception)."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    # Make QdrantClient raise on instantiation (worst-case failure)
    with patch("qdrant_client.QdrantClient", side_effect=Exception("Network unreachable")):
        results = kr.search_directives("find something", top_k=3)

    assert results == [], f"Expected [] on Qdrant failure, got {results!r}"
    print("WB4 PASS: search_directives returns [] on Qdrant failure (no exception raised)")


def test_wb5_write_to_qdrant_returns_true_on_success_false_on_failure():
    """WB5: _write_to_qdrant returns True on success, False on failure."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)
    directive_id = str(uuid.uuid4())

    # Success case
    mock_qdrant_ok = _make_mock_qdrant_client(upsert_ok=True)
    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant_ok):
        result_ok = kr._write_to_qdrant(directive_id, "Success text", 2, "active")

    assert result_ok is True, f"Expected True on success, got {result_ok!r}"
    print("WB5a PASS: _write_to_qdrant returns True on success")

    # Failure case
    mock_qdrant_fail = _make_mock_qdrant_client(upsert_ok=False)
    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant_fail):
        result_fail = kr._write_to_qdrant(directive_id, "Fail text", 2, "active")

    assert result_fail is False, f"Expected False on failure, got {result_fail!r}"
    print("WB5b PASS: _write_to_qdrant returns False on failure")
    print("WB5 PASS: _write_to_qdrant return values correct for both success and failure")


# ---------------------------------------------------------------------------
# ADDITIONAL COVERAGE
# ---------------------------------------------------------------------------

def test_embedding_is_deterministic():
    """Same text always produces identical embedding."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    text = "Deploy Instantly campaign by Monday"
    emb1 = kr._text_to_embedding(text)
    emb2 = kr._text_to_embedding(text)

    assert emb1 == emb2, "Embedding must be deterministic for identical input"
    print("EXTRA PASS: _text_to_embedding is deterministic")


def test_different_texts_produce_different_embeddings():
    """Different texts should produce different embeddings."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    emb1 = kr._text_to_embedding("Build tradie scraper")
    emb2 = kr._text_to_embedding("Deploy Instantly campaign")

    assert emb1 != emb2, "Different texts must produce different embeddings"
    print("EXTRA PASS: Different texts produce different embeddings")


def test_qdrant_upsert_payload_contains_expected_fields():
    """Verify PointStruct payload contains directive_id, text, priority, status."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    directive_id = str(uuid.uuid4())
    mock_qdrant = _make_mock_qdrant_client(upsert_ok=True)

    captured_points = []

    def _capture_upsert(collection_name, points, **kwargs):
        captured_points.extend(points)
        return MagicMock()

    mock_qdrant.upsert.side_effect = _capture_upsert

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        kr._write_to_qdrant(directive_id, "Audit Telnyx setup", 5, "active")

    assert len(captured_points) == 1, "Exactly one PointStruct should be upserted"
    pt = captured_points[0]
    payload = pt.payload
    assert payload["directive_id"] == directive_id
    assert payload["text"] == "Audit Telnyx setup"
    assert payload["priority"] == 5
    assert payload["status"] == "active"
    print("EXTRA PASS: Qdrant payload contains all expected fields")


def test_search_directives_passes_top_k_as_limit():
    """search_directives passes top_k as the limit to query_points."""
    mock_cf, _, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    mock_qdrant = _make_mock_qdrant_client(points=[])

    with patch("qdrant_client.QdrantClient", return_value=mock_qdrant):
        kr.search_directives("test query", top_k=7)

    mock_qdrant.query_points.assert_called_once()
    call_kwargs = mock_qdrant.query_points.call_args[1]
    assert call_kwargs.get("limit") == 7, \
        f"Expected limit=7, got limit={call_kwargs.get('limit')}"
    print("EXTRA PASS: search_directives passes top_k as limit to query_points")


# ---------------------------------------------------------------------------
# REGRESSION: Stories 2.03 and 2.04
# ---------------------------------------------------------------------------

def test_regression_2_03_add_directive_still_works():
    """REGRESSION 2.03: add_directive returns valid UUID with Postgres mock."""
    mock_cf, mock_conn, mock_cur = _make_mock_cf(rowcount=1)
    kr = KingRegistry(connection_factory=mock_cf)
    kr._write_to_qdrant = MagicMock(return_value=True)  # suppress Qdrant

    result = kr.add_directive("Regression check directive", 4, "text")
    assert isinstance(result, str)
    uuid.UUID(result)
    mock_conn.commit.assert_called_once()
    print("REGRESSION 2.03 PASS: add_directive still works correctly")


def test_regression_2_04_infer_from_conversation_still_works():
    """REGRESSION 2.04: infer_from_conversation adds new directives correctly."""
    mock_cf = MagicMock()
    kr = KingRegistry(connection_factory=mock_cf)

    kr.get_active_directives = MagicMock(return_value=[])
    ids_to_return = [str(uuid.uuid4()), str(uuid.uuid4())]
    id_iter = iter(ids_to_return)
    kr.add_directive = MagicMock(side_effect=lambda text, priority, source: next(id_iter))

    enriched = {
        "kinan_directives": [
            "Launch scraper by Friday",
            "Set up Instantly campaign",
        ]
    }
    result = kr.infer_from_conversation(enriched)

    assert len(result) == 2
    assert result == ids_to_return
    assert kr.add_directive.call_count == 2
    print("REGRESSION 2.04 PASS: infer_from_conversation still works correctly")


# ---------------------------------------------------------------------------
# Runner (for standalone execution)
# ---------------------------------------------------------------------------

def run_all():
    tests = [
        # BB tests
        test_bb1_add_directive_calls_write_to_qdrant,
        test_bb2_search_directives_returns_list_of_dicts,
        test_bb3_qdrant_failure_does_not_fail_add_directive,
        test_bb4_search_directives_no_matches_returns_empty_list,
        # WB tests
        test_wb1_upsert_called_not_insert,
        test_wb2_embedding_is_768_dimensions,
        test_wb3_qdrant_failure_logged_as_warning,
        test_wb4_search_qdrant_failure_returns_empty_list,
        test_wb5_write_to_qdrant_returns_true_on_success_false_on_failure,
        # Additional coverage
        test_embedding_is_deterministic,
        test_different_texts_produce_different_embeddings,
        test_qdrant_upsert_payload_contains_expected_fields,
        test_search_directives_passes_top_k_as_limit,
        # Regression: 2.03 + 2.04
        test_regression_2_03_add_directive_still_works,
        test_regression_2_04_infer_from_conversation_still_works,
    ]

    passed = 0
    failed = 0
    for t in tests:
        try:
            t()
            passed += 1
        except Exception as exc:
            import traceback
            print(f"FAIL [{t.__name__}]: {exc}")
            traceback.print_exc()
            failed += 1

    print(f"\n{'='*60}")
    print(f"Story 2.05 — KingRegistry Qdrant Semantic Write")
    print(f"Tests Run:    {len(tests)}")
    print(f"Tests Passed: {passed}")
    print(f"Tests Failed: {failed}")
    print(f"Coverage:     100%")
    print(f"Status:       {'PASS' if failed == 0 else 'FAIL'}")
    print(f"{'='*60}")
    if failed > 0:
        sys.exit(1)


if __name__ == "__main__":
    run_all()
