#!/usr/bin/env python3
"""
Tests for Story 2.04: KingRegistry — Conversation Inference
AIVA RLM Nexus — Track A
Module: core/registry/king_registry.py

Black Box + White Box tests using fully mocked ConnectionFactory.
No live DB connection required.

Test plan:
  BB1: 2 new directives   → 2 IDs returned
  BB2: 1 new + 1 duplicate (exact match) → 1 ID returned
  BB3: Empty list          → [] returned (no error)
  BB4: Missing key         → ValueError raised

  WB1: Duplicate detection is case-insensitive
  WB2: Substring match: "scraper" in "Build tradie scraper" = duplicate
  WB3: Reverse substring: "Build tradie scraper" containing "scraper" = duplicate
  WB4: source='inferred' is passed to add_directive
  WB5: priority=2 is used for all inferred directives
"""
import sys
import uuid
from datetime import datetime, timezone
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_directive_row(text: str, priority: int = 3) -> dict:
    """Return a directive dict in the same shape as get_active_directives output."""
    return {
        "directive_id": str(uuid.uuid4()),
        "text": text,
        "priority": priority,
        "captured_at": datetime.now(timezone.utc).isoformat(),
    }


def _make_registry_with_mocked_methods(
    existing_directives: list,
    add_return_ids: list = None,
) -> KingRegistry:
    """
    Build a KingRegistry whose get_active_directives and add_directive methods
    are replaced by mocks. This isolates infer_from_conversation from any real
    DB layer.

    Args:
        existing_directives: Returned by get_active_directives(100).
        add_return_ids:      Sequential UUIDs returned by successive
                             add_directive calls. Defaults to auto-generated.
    """
    # ConnectionFactory mock — not used directly but required by __init__
    mock_cf = MagicMock()
    kr = KingRegistry(connection_factory=mock_cf)

    # Patch instance methods
    kr.get_active_directives = MagicMock(return_value=existing_directives)

    if add_return_ids is None:
        # Generate fresh UUIDs on each call
        add_return_ids = [str(uuid.uuid4()) for _ in range(20)]

    add_id_iter = iter(add_return_ids)
    kr.add_directive = MagicMock(side_effect=lambda text, priority, source: next(add_id_iter))

    return kr


# ---------------------------------------------------------------------------
# BLACK BOX TESTS
# ---------------------------------------------------------------------------

def test_bb1_two_new_directives_return_two_ids():
    """BB1: 2 new directives → 2 IDs returned, add_directive called twice."""
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {
        "kinan_directives": [
            "Launch tradie scraper by Friday",
            "Set up Instantly campaign",
        ]
    }
    result = kr.infer_from_conversation(enriched)

    assert isinstance(result, list), "Should return a list"
    assert len(result) == 2, f"Expected 2 IDs, got {len(result)}"
    for rid in result:
        uuid.UUID(rid)  # raises ValueError if not a valid UUID
    assert kr.add_directive.call_count == 2, "add_directive should be called twice"
    print("BB1 PASS: 2 new directives → 2 IDs returned")


def test_bb2_one_new_one_duplicate_exact_match():
    """BB2: 1 new + 1 exact-duplicate → only 1 ID returned."""
    existing = [_make_directive_row("Build tradie scraper")]
    kr = _make_registry_with_mocked_methods(existing_directives=existing)

    enriched = {
        "kinan_directives": [
            "Build tradie scraper",          # exact duplicate — must be skipped
            "Set up Instantly campaign",     # new — must be added
        ]
    }
    result = kr.infer_from_conversation(enriched)

    assert len(result) == 1, f"Expected 1 new ID, got {len(result)}"
    assert kr.add_directive.call_count == 1, "add_directive should be called once only"
    print("BB2 PASS: exact duplicate skipped, new directive added")


def test_bb3_empty_list_returns_empty():
    """BB3: Empty kinan_directives list → [] returned, DB never touched."""
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {"kinan_directives": []}
    result = kr.infer_from_conversation(enriched)

    assert result == [], f"Expected empty list, got {result!r}"
    # get_active_directives should NOT be called — nothing to process
    kr.get_active_directives.assert_not_called()
    kr.add_directive.assert_not_called()
    print("BB3 PASS: empty list returns [] without touching DB")


def test_bb4_missing_key_raises_value_error():
    """BB4: Missing 'kinan_directives' key → ValueError raised."""
    mock_cf = MagicMock()
    kr = KingRegistry(connection_factory=mock_cf)

    try:
        kr.infer_from_conversation({"summary": "some summary"})
        assert False, "Expected ValueError"
    except ValueError as e:
        assert "kinan_directives" in str(e), \
            f"ValueError message should mention 'kinan_directives', got: {e}"
    print("BB4 PASS: missing 'kinan_directives' raises ValueError")


# ---------------------------------------------------------------------------
# WHITE BOX TESTS
# ---------------------------------------------------------------------------

def test_wb1_duplicate_detection_case_insensitive():
    """WB1: Duplicate detection is case-insensitive."""
    existing = [_make_directive_row("BUILD TRADIE SCRAPER")]
    kr = _make_registry_with_mocked_methods(existing_directives=existing)

    enriched = {
        "kinan_directives": [
            "build tradie scraper",   # lower-case version → must be detected as duplicate
        ]
    }
    result = kr.infer_from_conversation(enriched)

    assert result == [], f"Expected empty list (duplicate), got {result!r}"
    kr.add_directive.assert_not_called()
    print("WB1 PASS: case-insensitive duplicate detection works")


def test_wb2_substring_new_in_existing():
    """WB2: new_text is substring of existing → detected as duplicate."""
    # "scraper" is a substring of the existing "Build tradie scraper"
    existing = [_make_directive_row("Build tradie scraper")]
    kr = _make_registry_with_mocked_methods(existing_directives=existing)

    enriched = {"kinan_directives": ["scraper"]}
    result = kr.infer_from_conversation(enriched)

    assert result == [], f"Expected duplicate detection, got {result!r}"
    kr.add_directive.assert_not_called()
    print("WB2 PASS: 'scraper' in 'Build tradie scraper' detected as duplicate")


def test_wb3_substring_existing_in_new():
    """WB3: existing text is substring of new_text → detected as duplicate."""
    # "scraper" (existing) is a substring of "Build tradie scraper" (new)
    existing = [_make_directive_row("scraper")]
    kr = _make_registry_with_mocked_methods(existing_directives=existing)

    enriched = {"kinan_directives": ["Build tradie scraper"]}
    result = kr.infer_from_conversation(enriched)

    assert result == [], f"Expected duplicate detection (reverse direction), got {result!r}"
    kr.add_directive.assert_not_called()
    print("WB3 PASS: 'scraper' contained within new text detected as duplicate")


def test_wb4_source_inferred_passed_to_add_directive():
    """WB4: add_directive is always called with source='inferred'."""
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {
        "kinan_directives": [
            "Call agency leads by Monday",
            "Update GHL pipeline",
        ]
    }
    kr.infer_from_conversation(enriched)

    for call_args in kr.add_directive.call_args_list:
        # call_args.kwargs or call_args[1] for keyword; check both positional + keyword
        _, kwargs = call_args
        assert kwargs.get("source") == "inferred", \
            f"Expected source='inferred', got source={kwargs.get('source')!r}"
    print("WB4 PASS: source='inferred' used in all add_directive calls")


def test_wb5_priority_2_used_for_all_inferred_directives():
    """WB5: add_directive is always called with priority=2."""
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {
        "kinan_directives": [
            "Deploy Instantly campaign",
            "Send weekly report",
            "Check Telnyx logs",
        ]
    }
    kr.infer_from_conversation(enriched)

    assert kr.add_directive.call_count == 3
    for call_args in kr.add_directive.call_args_list:
        _, kwargs = call_args
        assert kwargs.get("priority") == 2, \
            f"Expected priority=2, got priority={kwargs.get('priority')!r}"
    print("WB5 PASS: priority=2 used in all add_directive calls")


# ---------------------------------------------------------------------------
# ADDITIONAL COVERAGE
# ---------------------------------------------------------------------------

def test_batch_deduplication_within_same_call():
    """
    Within a single call, if two incoming directives are near-duplicates of
    each other (one is substring of the other), the second is skipped after
    the first is added to the running pool.
    """
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {
        "kinan_directives": [
            "scraper",                # added first, goes into running pool
            "Build tradie scraper",   # "scraper" is in "Build tradie scraper" → duplicate
        ]
    }
    result = kr.infer_from_conversation(enriched)

    assert len(result) == 1, \
        f"Expected 1 ID (second is intra-batch duplicate), got {len(result)}"
    assert kr.add_directive.call_count == 1
    print("EXTRA PASS: intra-batch duplicate detected correctly")


def test_get_active_directives_called_with_100():
    """Verify get_active_directives(top_n=100) is called for the deduplication pool."""
    kr = _make_registry_with_mocked_methods(existing_directives=[])

    enriched = {"kinan_directives": ["Do something new"]}
    kr.infer_from_conversation(enriched)

    kr.get_active_directives.assert_called_once_with(top_n=100)
    print("EXTRA PASS: get_active_directives called with top_n=100")


def test_returned_ids_are_from_add_directive():
    """Returned UUIDs match exactly what add_directive returned."""
    fixed_ids = [str(uuid.uuid4()), str(uuid.uuid4())]
    kr = _make_registry_with_mocked_methods(
        existing_directives=[],
        add_return_ids=fixed_ids,
    )

    enriched = {
        "kinan_directives": ["Directive Alpha", "Directive Beta"]
    }
    result = kr.infer_from_conversation(enriched)

    assert result == fixed_ids, \
        f"Returned IDs don't match add_directive return values: {result}"
    print("EXTRA PASS: returned IDs exactly match add_directive outputs")


# ---------------------------------------------------------------------------
# Runner (for standalone execution)
# ---------------------------------------------------------------------------

def run_all():
    tests = [
        # BB tests
        test_bb1_two_new_directives_return_two_ids,
        test_bb2_one_new_one_duplicate_exact_match,
        test_bb3_empty_list_returns_empty,
        test_bb4_missing_key_raises_value_error,
        # WB tests
        test_wb1_duplicate_detection_case_insensitive,
        test_wb2_substring_new_in_existing,
        test_wb3_substring_existing_in_new,
        test_wb4_source_inferred_passed_to_add_directive,
        test_wb5_priority_2_used_for_all_inferred_directives,
        # Additional coverage
        test_batch_deduplication_within_same_call,
        test_get_active_directives_called_with_100,
        test_returned_ids_are_from_add_directive,
    ]

    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.04 — KingRegistry Conversation Inference")
    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()
