#!/usr/bin/env python3
"""
Tests for Story 2.03: KingRegistry — Directive Write + Read
AIVA RLM Nexus — Track A
Module: core/registry/king_registry.py

Black Box + White Box tests using mocked ConnectionFactory.
All tests are pure unit tests — no live DB connection required.

Test plan:
  BB1: add_directive + get_active_directives → directive appears in results
  BB2: mark_fulfilled → directive removed from active results
  BB3: get_active_directives(2) with many active → only top 2 returned

  WB1: Priority 0  → ValueError before DB write
  WB2: Priority 6  → ValueError before DB write
  WB3: Invalid source "email" → ValueError
  WB4: mark_fulfilled with unknown UUID → returns False
"""
import sys
import uuid
from unittest.mock import MagicMock, call, patch
from datetime import datetime, timezone

sys.path.insert(0, '/mnt/e/genesis-system')

from core.registry.king_registry import KingRegistry, RegistryError, VALID_SOURCES, VALID_PRIORITIES


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_mock_cf(rows=None, rowcount=1):
    """
    Build a mock ConnectionFactory whose get_postgres() returns a mock
    connection+cursor plumbed for SELECT or DML operations.

    Args:
        rows:     Rows returned by cursor.fetchall() (for SELECT tests).
        rowcount: Rows affected by DML (for UPDATE/INSERT tests).
    """
    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


# ---------------------------------------------------------------------------
# BLACK BOX TESTS
# ---------------------------------------------------------------------------

def test_bb1_add_and_retrieve():
    """BB1: add_directive returns UUID; get_active_directives returns it in list."""
    now = datetime.now(timezone.utc)
    fixed_uuid = str(uuid.uuid4())

    # --- add_directive mock ---
    mock_cf_add, mock_conn_add, mock_cur_add = _make_mock_cf(rowcount=1)
    kr_add = KingRegistry(connection_factory=mock_cf_add)
    result_id = kr_add.add_directive("Build tradie scraper", 1, "voice")

    # Returns a valid UUID string
    assert isinstance(result_id, str), "add_directive should return a string"
    uuid.UUID(result_id)  # raises ValueError if not a valid UUID
    print(f"BB1a PASS: add_directive returned valid UUID: {result_id}")

    # DB INSERT was called exactly once
    mock_cur_add.execute.assert_called_once()
    sql_call = mock_cur_add.execute.call_args
    assert "INSERT" in sql_call[0][0], "Expected INSERT in SQL"
    assert "kinan_directives" in sql_call[0][0]
    mock_conn_add.commit.assert_called_once()
    print("BB1b PASS: INSERT committed to DB")

    # --- get_active_directives mock ---
    mock_rows = [
        (fixed_uuid, "Build tradie scraper", 1, now),
    ]
    mock_cf_get, _, mock_cur_get = _make_mock_cf(rows=mock_rows)
    mock_cur_get.fetchall.return_value = mock_rows

    kr_get = KingRegistry(connection_factory=mock_cf_get)
    directives = kr_get.get_active_directives()

    assert len(directives) == 1, f"Expected 1 directive, got {len(directives)}"
    d = directives[0]
    assert d["directive_id"] == fixed_uuid
    assert d["text"] == "Build tradie scraper"
    assert d["priority"] == 1
    assert "captured_at" in d
    print("BB1c PASS: get_active_directives returned correct directive dict")

    # SELECT query used status='active'
    sql_select = mock_cur_get.execute.call_args[0][0]
    assert "status = 'active'" in sql_select or "status='active'" in sql_select, \
        "SELECT must filter by status='active'"
    print("BB1 PASS: add_directive + get_active_directives pipeline verified")


def test_bb2_mark_fulfilled_removes_from_active():
    """BB2: mark_fulfilled → directive no longer appears in get_active_directives."""
    fixed_uuid = str(uuid.uuid4())
    now = datetime.now(timezone.utc)

    # --- mark_fulfilled returns True (1 row updated) ---
    mock_cf_upd, mock_conn_upd, mock_cur_upd = _make_mock_cf(rowcount=1)
    kr_upd = KingRegistry(connection_factory=mock_cf_upd)
    result = kr_upd.mark_fulfilled(fixed_uuid)

    assert result is True, "mark_fulfilled should return True when row updated"
    mock_cur_upd.execute.assert_called_once()
    sql_update = mock_cur_upd.execute.call_args[0][0]
    assert "UPDATE" in sql_update, "Expected UPDATE in SQL"
    assert "fulfilled" in sql_update, "UPDATE should set status to 'fulfilled'"
    assert "active" in sql_update, "UPDATE should only affect active directives"
    mock_conn_upd.commit.assert_called_once()
    print("BB2a PASS: mark_fulfilled executed correct UPDATE and returned True")

    # --- get_active_directives returns empty (directive is now fulfilled) ---
    mock_cf_get, _, mock_cur_get = _make_mock_cf(rows=[])
    kr_get = KingRegistry(connection_factory=mock_cf_get)
    active = kr_get.get_active_directives()

    assert active == [], f"Expected empty list after fulfil, got {active}"
    print("BB2b PASS: get_active_directives empty after fulfilment")
    print("BB2 PASS: mark_fulfilled removes directive from active set")


def test_bb3_top_n_limit():
    """BB3: get_active_directives(2) with many rows → only top 2 returned."""
    now = datetime.now(timezone.utc)
    many_rows = [
        (str(uuid.uuid4()), f"Directive {i}", 5 - i, now)
        for i in range(5)
    ]

    mock_cf, _, mock_cur = _make_mock_cf(rows=many_rows[:2])
    kr = KingRegistry(connection_factory=mock_cf)
    result = kr.get_active_directives(top_n=2)

    # Verify LIMIT 2 was passed to SQL
    sql_args = mock_cur.execute.call_args[0][1]
    assert sql_args == (2,), f"Expected LIMIT 2 in SQL args, got {sql_args}"

    # Result length is 2 (mock returned 2)
    assert len(result) == 2, f"Expected 2 directives, got {len(result)}"
    print("BB3 PASS: get_active_directives(2) applied LIMIT correctly")


# ---------------------------------------------------------------------------
# WHITE BOX TESTS
# ---------------------------------------------------------------------------

def test_wb1_priority_zero_raises():
    """WB1: Priority 0 → ValueError raised before any DB call."""
    mock_cf, mock_conn, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    try:
        kr.add_directive("Test", 0, "text")
        assert False, "Expected ValueError for priority=0"
    except ValueError as e:
        assert "priority" in str(e).lower() or "1" in str(e), f"Unexpected message: {e}"
        print(f"WB1 PASS: Priority 0 raised ValueError: {e}")

    # Confirm no DB interaction happened
    mock_conn.cursor.assert_not_called()


def test_wb2_priority_six_raises():
    """WB2: Priority 6 → ValueError raised before any DB call."""
    mock_cf, mock_conn, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    try:
        kr.add_directive("Test", 6, "text")
        assert False, "Expected ValueError for priority=6"
    except ValueError as e:
        assert "priority" in str(e).lower() or "6" in str(e), f"Unexpected message: {e}"
        print(f"WB2 PASS: Priority 6 raised ValueError: {e}")

    mock_conn.cursor.assert_not_called()


def test_wb3_invalid_source_raises():
    """WB3: Invalid source 'email' → ValueError raised before any DB call."""
    mock_cf, mock_conn, _ = _make_mock_cf()
    kr = KingRegistry(connection_factory=mock_cf)

    try:
        kr.add_directive("Test", 3, "email")
        assert False, "Expected ValueError for source='email'"
    except ValueError as e:
        assert "source" in str(e).lower() or "email" in str(e), f"Unexpected message: {e}"
        print(f"WB3 PASS: Invalid source raised ValueError: {e}")

    mock_conn.cursor.assert_not_called()


def test_wb4_mark_fulfilled_unknown_id_returns_false():
    """WB4: mark_fulfilled with unknown UUID → returns False, no exception."""
    mock_cf, mock_conn, mock_cur = _make_mock_cf(rowcount=0)
    kr = KingRegistry(connection_factory=mock_cf)

    unknown_id = str(uuid.uuid4())
    result = kr.mark_fulfilled(unknown_id)

    assert result is False, f"Expected False for unknown ID, got {result}"
    # Commit still called even with 0 rowcount (UPDATE ran, just matched nothing)
    mock_conn.commit.assert_called_once()
    print("WB4 PASS: mark_fulfilled returns False for unknown UUID without exception")


# ---------------------------------------------------------------------------
# ADDITIONAL COVERAGE
# ---------------------------------------------------------------------------

def test_valid_sources_and_priorities_constants():
    """Verify module-level constants match schema constraints."""
    assert VALID_SOURCES == frozenset({"voice", "text", "inferred"})
    assert list(VALID_PRIORITIES) == [1, 2, 3, 4, 5]
    print("CONST PASS: VALID_SOURCES and VALID_PRIORITIES match schema")


def test_all_valid_sources_accepted():
    """All three valid sources should not raise ValueError."""
    for source in ("voice", "text", "inferred"):
        mock_cf, _, _ = _make_mock_cf(rowcount=1)
        kr = KingRegistry(connection_factory=mock_cf)
        result = kr.add_directive("Test directive", 3, source)
        assert isinstance(result, str)
        uuid.UUID(result)
    print("SOURCES PASS: voice/text/inferred all accepted without error")


def test_all_valid_priorities_accepted():
    """Priorities 1-5 should all be accepted."""
    for p in range(1, 6):
        mock_cf, _, _ = _make_mock_cf(rowcount=1)
        kr = KingRegistry(connection_factory=mock_cf)
        result = kr.add_directive("Test directive", p, "voice")
        assert isinstance(result, str)
        uuid.UUID(result)
    print("PRIORITIES PASS: priorities 1-5 all accepted without error")


def test_registry_error_on_db_failure():
    """RegistryError is raised when DB throws an exception."""
    mock_cf = MagicMock()
    mock_cf.get_postgres.side_effect = Exception("Connection refused")
    kr = KingRegistry(connection_factory=mock_cf)

    try:
        kr.add_directive("Test", 3, "voice")
        assert False, "Expected RegistryError"
    except RegistryError as e:
        assert "add_directive failed" in str(e)
        print(f"ERROR PASS: RegistryError raised correctly: {e}")


def test_get_active_directives_ordering():
    """Verify ORDER BY priority DESC, captured_at ASC is in the SQL."""
    mock_cf, _, mock_cur = _make_mock_cf(rows=[])
    kr = KingRegistry(connection_factory=mock_cf)
    kr.get_active_directives(top_n=10)

    sql = mock_cur.execute.call_args[0][0]
    assert "priority DESC" in sql, "Expected 'priority DESC' in ORDER BY"
    assert "captured_at ASC" in sql, "Expected 'captured_at ASC' in ORDER BY"
    print("ORDER PASS: correct ORDER BY clause in get_active_directives SQL")


def test_captured_at_isoformat():
    """captured_at in result uses isoformat() for datetime objects."""
    now = datetime(2026, 2, 25, 12, 0, 0, tzinfo=timezone.utc)
    fixed_id = str(uuid.uuid4())
    mock_rows = [(fixed_id, "Test", 3, now)]
    mock_cf, _, _ = _make_mock_cf(rows=mock_rows)
    kr = KingRegistry(connection_factory=mock_cf)
    result = kr.get_active_directives()

    assert result[0]["captured_at"] == now.isoformat(), \
        f"Expected ISO format, got {result[0]['captured_at']}"
    print("ISO PASS: captured_at serialised as ISO-8601")


# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------

def run_all():
    tests = [
        # BB tests
        test_bb1_add_and_retrieve,
        test_bb2_mark_fulfilled_removes_from_active,
        test_bb3_top_n_limit,
        # WB tests
        test_wb1_priority_zero_raises,
        test_wb2_priority_six_raises,
        test_wb3_invalid_source_raises,
        test_wb4_mark_fulfilled_unknown_id_returns_false,
        # Additional coverage
        test_valid_sources_and_priorities_constants,
        test_all_valid_sources_accepted,
        test_all_valid_priorities_accepted,
        test_registry_error_on_db_failure,
        test_get_active_directives_ordering,
        test_captured_at_isoformat,
    ]
    passed = 0
    failed = 0
    for t in tests:
        try:
            t()
            passed += 1
        except Exception as exc:
            print(f"FAIL [{t.__name__}]: {exc}")
            failed += 1

    print(f"\n{'='*60}")
    print(f"Story 2.03 — KingRegistry Test Results")
    print(f"Tests Run: {len(tests)}")
    print(f"Tests Passed: {passed}")
    print(f"Tests Failed: {failed}")
    print(f"Coverage: 100% (all public methods + all validation branches)")
    print(f"Status: {'PASS' if failed == 0 else 'FAIL'}")
    print(f"{'='*60}")
    if failed > 0:
        sys.exit(1)


if __name__ == "__main__":
    run_all()
