#!/usr/bin/env python3
"""
Tests for Story 2.02: QueenRegistry — Capability Logging
AIVA RLM Nexus — Track A
Module: core/registry/queen_registry.py

Black Box + White Box tests — all fully mocked (no live infra required).

Test matrix:
  BB1: log_capability_gain with valid args → UUID returned
  BB2: get_capability_history(3) → returns max 3 items, newest first
  BB3: Invalid capability_type → ValueError raised before DB write
  WB1: Cache invalidation → invalidate_cache() called after successful log
  WB2: metrics=None → stored as {} (JSONB-safe)
  WB3: DB write fails → RegistryError raised
  EDGE1: All four valid capability_type values are accepted
  EDGE2: get_capability_history returns empty list when table is empty

Run with:
    python -m pytest tests/track_a/test_story_2_02.py -v
"""
import sys
import json
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch, call, ANY

sys.path.insert(0, '/mnt/e/genesis-system')

import pytest

from core.registry.queen_registry import (
    QueenRegistry,
    RegistryError,
    VALID_CAPABILITY_TYPES,
)


# ---------------------------------------------------------------------------
# Helpers / fixtures
# ---------------------------------------------------------------------------

def _make_mock_cf(
    pg_raises=None,
    history_rows=None,
):
    """
    Build a mock ConnectionFactory suitable for Story 2.02 tests.

    Args:
        pg_raises:     If set, conn.cursor() raises this exception on first call.
        history_rows:  Rows returned by get_capability_history SELECT.
                       Each row: (log_id, description, capability_type, logged_at, metrics)
    """
    ts = datetime(2026, 2, 25, 9, 0, 0, tzinfo=timezone.utc)

    if history_rows is None:
        history_rows = [
            ("uuid-1", "Learned voice routing", "new_skill",        ts, {"accuracy": 0.95}),
            ("uuid-2", "Improved memory",        "improved_recall",  ts, {}),
            ("uuid-3", "Fixed null pointer",     "bug_fixed",        ts, {}),
        ]

    # -- Redis mock (always healthy — we test cache invalidation separately) --
    redis_mock = MagicMock()
    redis_mock.get.return_value = None   # always cache miss — irrelevant for these tests
    redis_mock.ping.return_value = True

    # -- Postgres mock --
    cur_mock = MagicMock()
    if pg_raises:
        cur_mock.execute.side_effect = pg_raises
    else:
        cur_mock.fetchall.return_value = history_rows

    conn_mock = MagicMock()
    if pg_raises:
        conn_mock.cursor.side_effect = pg_raises
    else:
        conn_mock.cursor.return_value = cur_mock

    cf = MagicMock()
    cf.get_redis.return_value = redis_mock
    cf.get_postgres.return_value = conn_mock

    return cf, redis_mock, conn_mock, cur_mock


# ---------------------------------------------------------------------------
# BB1: log_capability_gain with valid args → UUID returned
# ---------------------------------------------------------------------------

def test_bb1_log_capability_gain_valid_args_returns_uuid():
    """BB1: log_capability_gain() with all valid args returns a UUID string."""
    cf, redis_mock, conn_mock, cur_mock = _make_mock_cf()

    registry = QueenRegistry(connection_factory=cf)

    log_id = registry.log_capability_gain(
        description="Learned to parse JSON payloads",
        capability_type="new_skill",
        metrics={"accuracy": 0.98, "latency_ms": 120},
        epoch_id="epoch-007",
    )

    # Return value must be a non-empty UUID string
    assert isinstance(log_id, str), f"Expected str, got {type(log_id)}"
    assert len(log_id) == 36, f"Expected UUID length 36, got {len(log_id)}"

    # Validate UUID format: 8-4-4-4-12
    parts = log_id.split("-")
    assert len(parts) == 5, f"UUID must have 5 parts, got {len(parts)}: {log_id}"
    assert [len(p) for p in parts] == [8, 4, 4, 4, 12], f"UUID part lengths wrong: {log_id}"

    # DB INSERT was executed
    cur_mock.execute.assert_called_once()
    conn_mock.commit.assert_called_once()

    print(f"BB1 PASS: log_capability_gain returned UUID '{log_id}'")


# ---------------------------------------------------------------------------
# BB2: get_capability_history(3) → returns max 3 items, newest first
# ---------------------------------------------------------------------------

def test_bb2_get_capability_history_returns_correct_structure():
    """BB2: get_capability_history(3) returns at most 3 items with correct keys, newest first."""
    ts = datetime(2026, 2, 25, 9, 0, 0, tzinfo=timezone.utc)
    rows = [
        ("uuid-newest", "Newest capability",  "new_skill",       ts, {"score": 1}),
        ("uuid-middle", "Middle capability",  "improved_recall", ts, {}),
        ("uuid-oldest", "Oldest capability",  "bug_fixed",       ts, None),
    ]
    cf, _, _, cur_mock = _make_mock_cf(history_rows=rows)
    cur_mock.fetchall.return_value = rows

    registry = QueenRegistry(connection_factory=cf)
    history = registry.get_capability_history(last_n=3)

    # Returns a list
    assert isinstance(history, list), f"Expected list, got {type(history)}"

    # At most 3 items
    assert len(history) <= 3, f"Expected at most 3 items, got {len(history)}"
    assert len(history) == 3, f"Expected exactly 3 items from mocked rows, got {len(history)}"

    # Each item has the 5 required keys
    required_keys = {"log_id", "description", "capability_type", "logged_at", "metrics"}
    for item in history:
        missing = required_keys - item.keys()
        assert not missing, f"Item missing keys: {missing}"

    # First item is the newest (uuid-newest)
    assert history[0]["log_id"] == "uuid-newest", (
        f"Expected newest first, got: {history[0]['log_id']}"
    )

    # metrics=None row should come back as {}
    assert history[2]["metrics"] == {}, (
        f"Expected empty dict for None metrics, got: {history[2]['metrics']}"
    )

    # logged_at is a string (ISO format)
    assert isinstance(history[0]["logged_at"], str), "logged_at must be a string"

    # LIMIT was passed correctly to the query
    execute_args = cur_mock.execute.call_args
    assert 3 in execute_args[0][1] or (3,) == execute_args[0][1], (
        f"Expected LIMIT 3 in query params, got: {execute_args}"
    )

    print("BB2 PASS: get_capability_history returned 3 items with correct structure")


# ---------------------------------------------------------------------------
# BB3: Invalid capability_type → ValueError raised before DB write
# ---------------------------------------------------------------------------

def test_bb3_invalid_capability_type_raises_value_error():
    """BB3: Passing an invalid capability_type raises ValueError BEFORE touching the DB."""
    cf, _, conn_mock, cur_mock = _make_mock_cf()

    registry = QueenRegistry(connection_factory=cf)

    with pytest.raises(ValueError) as exc_info:
        registry.log_capability_gain(
            description="Some description",
            capability_type="INVALID_TYPE",
        )

    # Error message references the bad value
    assert "INVALID_TYPE" in str(exc_info.value), (
        f"ValueError message should mention 'INVALID_TYPE': {exc_info.value}"
    )

    # DB was NEVER touched — no cursor, no execute, no commit
    conn_mock.cursor.assert_not_called()
    cur_mock.execute.assert_not_called()
    conn_mock.commit.assert_not_called()

    print("BB3 PASS: ValueError raised for invalid capability_type; DB not touched")


# ---------------------------------------------------------------------------
# WB1: Cache invalidation → invalidate_cache() called after successful log
# ---------------------------------------------------------------------------

def test_wb1_cache_invalidated_after_successful_log():
    """WB1: invalidate_cache() is called exactly once after a successful log_capability_gain()."""
    cf, redis_mock, _, _ = _make_mock_cf()

    registry = QueenRegistry(connection_factory=cf)

    registry.log_capability_gain(
        description="Improved embedding model",
        capability_type="improved_recall",
    )

    # Redis.delete(CACHE_KEY) was called exactly once (via invalidate_cache)
    redis_mock.delete.assert_called_once()
    delete_args = redis_mock.delete.call_args[0]
    assert delete_args[0] == "aiva:identity", (
        f"Expected redis.delete('aiva:identity'), got delete({delete_args})"
    )

    print("WB1 PASS: invalidate_cache() called once after successful log — redis.delete('aiva:identity') confirmed")


# ---------------------------------------------------------------------------
# WB2: metrics=None → stored as {} (JSONB-safe, not NULL)
# ---------------------------------------------------------------------------

def test_wb2_metrics_none_stored_as_empty_dict():
    """WB2: When metrics=None, the INSERT stores '{}' as JSON, not NULL."""
    cf, _, _, cur_mock = _make_mock_cf()

    registry = QueenRegistry(connection_factory=cf)

    registry.log_capability_gain(
        description="Pattern detected in conversations",
        capability_type="pattern_learned",
        metrics=None,   # explicit None
    )

    # Inspect the INSERT call arguments
    execute_args = cur_mock.execute.call_args
    params = execute_args[0][1]   # positional args tuple: (log_id, logged_at, type, desc, epoch_id, metrics_json)

    # params[5] is the metrics argument — should be JSON string of {}
    metrics_arg = params[5]
    assert metrics_arg == json.dumps({}), (
        f"Expected metrics to be stored as '{{}}', got: {metrics_arg!r}"
    )

    print(f"WB2 PASS: metrics=None stored as '{json.dumps({})}' in Postgres")


# ---------------------------------------------------------------------------
# WB3: DB write fails → RegistryError raised
# ---------------------------------------------------------------------------

def test_wb3_db_write_failure_raises_registry_error():
    """WB3: When Postgres raises during INSERT, log_capability_gain() raises RegistryError."""
    cf, _, _, _ = _make_mock_cf(pg_raises=Exception("Postgres connection lost"))

    registry = QueenRegistry(connection_factory=cf)

    with pytest.raises(RegistryError) as exc_info:
        registry.log_capability_gain(
            description="Should fail",
            capability_type="bug_fixed",
        )

    assert "Failed to log capability gain" in str(exc_info.value), (
        f"RegistryError message unexpected: {exc_info.value}"
    )

    print("WB3 PASS: RegistryError raised when Postgres write fails")


# ---------------------------------------------------------------------------
# EDGE1: All four valid capability_type values are accepted without ValueError
# ---------------------------------------------------------------------------

def test_edge1_all_valid_capability_types_accepted():
    """EDGE1: Each of the 4 valid capability_type values passes validation and reaches the DB."""
    for cap_type in sorted(VALID_CAPABILITY_TYPES):
        cf, _, _, cur_mock = _make_mock_cf()
        registry = QueenRegistry(connection_factory=cf)

        log_id = registry.log_capability_gain(
            description=f"Testing type: {cap_type}",
            capability_type=cap_type,
        )

        # Must return a UUID string
        assert isinstance(log_id, str) and len(log_id) == 36, (
            f"Expected UUID for type '{cap_type}', got: {log_id!r}"
        )

        # DB was written
        cur_mock.execute.assert_called_once()

    print(f"EDGE1 PASS: All {len(VALID_CAPABILITY_TYPES)} valid capability types accepted: "
          f"{sorted(VALID_CAPABILITY_TYPES)}")


# ---------------------------------------------------------------------------
# EDGE2: get_capability_history returns empty list when table is empty
# ---------------------------------------------------------------------------

def test_edge2_get_capability_history_empty_table():
    """EDGE2: When aiva_capability_log is empty, get_capability_history() returns []."""
    cf, _, _, cur_mock = _make_mock_cf(history_rows=[])
    cur_mock.fetchall.return_value = []

    registry = QueenRegistry(connection_factory=cf)
    history = registry.get_capability_history(last_n=10)

    assert history == [], f"Expected empty list, got: {history}"
    assert isinstance(history, list), f"Expected list type, got {type(history)}"

    print("EDGE2 PASS: get_capability_history returns [] when table is empty")


# ---------------------------------------------------------------------------
# Bonus EDGE3: get_capability_history DB failure → RegistryError
# ---------------------------------------------------------------------------

def test_edge3_get_capability_history_db_failure_raises_registry_error():
    """EDGE3: When Postgres fails during SELECT, get_capability_history() raises RegistryError."""
    cf, _, _, _ = _make_mock_cf(pg_raises=Exception("SELECT failed"))

    registry = QueenRegistry(connection_factory=cf)

    with pytest.raises(RegistryError) as exc_info:
        registry.get_capability_history()

    assert "Failed to fetch capability history" in str(exc_info.value), (
        f"RegistryError message unexpected: {exc_info.value}"
    )

    print("EDGE3 PASS: RegistryError raised when get_capability_history SELECT fails")


# ---------------------------------------------------------------------------
# Standalone runner
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    test_bb1_log_capability_gain_valid_args_returns_uuid()
    test_bb2_get_capability_history_returns_correct_structure()
    test_bb3_invalid_capability_type_raises_value_error()
    test_wb1_cache_invalidated_after_successful_log()
    test_wb2_metrics_none_stored_as_empty_dict()
    test_wb3_db_write_failure_raises_registry_error()
    test_edge1_all_valid_capability_types_accepted()
    test_edge2_get_capability_history_empty_table()
    test_edge3_get_capability_history_db_failure_raises_registry_error()

    print("\nALL TESTS PASSED — Story 2.02")
    print("Tests: 9/9 PASS (BB1, BB2, BB3, WB1, WB2, WB3, EDGE1, EDGE2, EDGE3)")
