"""
Story 8.04 — Test Suite
========================
Royal Dashboard FastAPI Routes

3 routes under test:
  GET /status         — live call status (active_calls, swarm_workers, queue_depth)
  GET /calls/recent   — last 10 calls with status, duration, outcome
  GET /workers/active — list of active swarm worker tasks from Redis

BB Tests (3):
  BB1: GET /status    → 200, all three keys present with integer values
  BB2: GET /calls/recent → max 10 entries returned
  BB3: GET /workers/active → list sourced from Redis swarm:worker:* keys

WB Tests (3):
  WB1: queue_depth in /status is sum of LLEN on both bridge queues
  WB2: active_calls in /status is count of aiva:state:* keys
  WB3: /calls/recent uses Postgres fetch (not Redis)

Additional coverage tests (3):
  COV1: /status with no Redis → returns zeros (graceful degradation)
  COV2: /calls/recent with no Postgres → returns empty list
  COV3: /workers/active with no Redis → returns empty list

All external services mocked via set_redis_client / set_pg_pool.
No real network or database calls are made.
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

import api.royal_dashboard as rd
from api.royal_dashboard import dashboard, set_pg_pool, set_redis_client


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_redis(
    state_keys: list | None = None,
    worker_keys: list | None = None,
    worker_values: dict | None = None,
    q_aiva_to_genesis: int = 0,
    q_genesis_to_aiva: int = 0,
) -> MagicMock:
    """
    Build a mock Redis client.

    ``state_keys``    — list of b"aiva:state:*" style keys (count = active_calls)
    ``worker_keys``   — list of b"swarm:worker:<id>" style keys
    ``worker_values`` — mapping of key → value string for worker statuses
    ``q_*``           — LLEN return values for the two bridge queues
    """
    state_keys = state_keys if state_keys is not None else []
    worker_keys = worker_keys if worker_keys is not None else []
    worker_values = worker_values if worker_values is not None else {}

    redis = MagicMock()

    def _keys(pattern: str) -> list:
        if pattern == "aiva:state:*":
            return state_keys
        if pattern == "swarm:worker:*":
            return worker_keys
        return []

    redis.keys.side_effect = _keys

    def _llen(name: str) -> int:
        if name == "bridge:queue:aiva_to_genesis":
            return q_aiva_to_genesis
        if name == "bridge:queue:genesis_to_aiva":
            return q_genesis_to_aiva
        return 0

    redis.llen.side_effect = _llen

    def _get(key) -> bytes | None:
        key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
        val = worker_values.get(key_str)
        if val is None:
            return None
        return val.encode("utf-8") if isinstance(val, str) else val

    redis.get.side_effect = _get

    return redis


def _make_pg(rows: list | None = None) -> MagicMock:
    """Build a mock Postgres pool whose .fetch() returns ``rows``."""
    rows = rows if rows is not None else []
    pg = MagicMock()
    pg.fetch.return_value = rows
    return pg


def _row(
    conv_id: str = "conv-001",
    offset_seconds: int = 0,
    duration_seconds: int = 120,
    caller: str = "+61400000001",
    outcome: str = "booked",
) -> dict:
    """Build a fake Postgres row dict."""
    now = datetime.now(tz=timezone.utc) - timedelta(seconds=offset_seconds)
    ended = now + timedelta(seconds=duration_seconds)
    return {
        "conversation_id": conv_id,
        "started_at": now,
        "ended_at": ended,
        "caller_number": caller,
        "outcome": outcome,
    }


# ---------------------------------------------------------------------------
# Fixture: reset globals between tests
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _reset_globals():
    """Ensure Redis/Postgres singletons are cleared before and after each test."""
    set_redis_client(None)
    set_pg_pool(None)
    yield
    set_redis_client(None)
    set_pg_pool(None)


# ---------------------------------------------------------------------------
# BB1 — GET /status returns 200 with all three required integer keys
# ---------------------------------------------------------------------------


def test_bb1_status_returns_200():
    """BB1: GET /status returns HTTP 200."""
    set_redis_client(_make_redis())
    client = TestClient(dashboard)
    resp = client.get("/status")
    assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"


def test_bb1_status_has_all_three_keys():
    """BB1: /status response contains active_calls, swarm_workers, queue_depth."""
    set_redis_client(_make_redis())
    client = TestClient(dashboard)
    body = client.get("/status").json()
    for key in ("active_calls", "swarm_workers", "queue_depth"):
        assert key in body, f"Key '{key}' missing from /status response: {body}"


def test_bb1_status_values_are_integers():
    """BB1: All three /status values are integers >= 0."""
    set_redis_client(
        _make_redis(
            state_keys=["aiva:state:call-1", "aiva:state:call-2"],
            worker_keys=["swarm:worker:w1"],
            q_aiva_to_genesis=3,
            q_genesis_to_aiva=1,
        )
    )
    client = TestClient(dashboard)
    body = client.get("/status").json()
    for key in ("active_calls", "swarm_workers", "queue_depth"):
        assert isinstance(body[key], int), f"{key} should be int, got {type(body[key])}"
        assert body[key] >= 0, f"{key} should be >= 0, got {body[key]}"


# ---------------------------------------------------------------------------
# BB2 — GET /calls/recent returns at most 10 entries
# ---------------------------------------------------------------------------


def test_bb2_recent_calls_returns_200():
    """BB2: GET /calls/recent returns HTTP 200."""
    set_pg_pool(_make_pg())
    client = TestClient(dashboard)
    resp = client.get("/calls/recent")
    assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"


def test_bb2_recent_calls_max_10():
    """BB2: /calls/recent returns at most 10 entries even when more exist in the mock."""
    # Postgres mock already handles the LIMIT 10 — we return exactly 10
    rows = [_row(conv_id=f"conv-{i:03d}", offset_seconds=i * 60) for i in range(10)]
    set_pg_pool(_make_pg(rows=rows))
    client = TestClient(dashboard)
    body = client.get("/calls/recent").json()
    assert isinstance(body, list), f"Expected list, got {type(body)}"
    assert len(body) <= 10, f"Expected at most 10 entries, got {len(body)}"


def test_bb2_recent_calls_entry_shape():
    """BB2: Each entry in /calls/recent has the required fields."""
    rows = [_row()]
    set_pg_pool(_make_pg(rows=rows))
    client = TestClient(dashboard)
    body = client.get("/calls/recent").json()
    assert len(body) == 1
    entry = body[0]
    for field in ("conversation_id", "started_at", "ended_at", "duration_s", "caller_number", "outcome"):
        assert field in entry, f"Field '{field}' missing from /calls/recent entry: {entry}"


# ---------------------------------------------------------------------------
# BB3 — GET /workers/active returns list from Redis swarm:worker:* keys
# ---------------------------------------------------------------------------


def test_bb3_active_workers_returns_200():
    """BB3: GET /workers/active returns HTTP 200."""
    set_redis_client(_make_redis())
    client = TestClient(dashboard)
    resp = client.get("/workers/active")
    assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"


def test_bb3_active_workers_lists_from_redis():
    """BB3: /workers/active returns one entry per swarm:worker:* key in Redis."""
    worker_keys = [b"swarm:worker:alpha", b"swarm:worker:beta"]
    worker_values = {
        "swarm:worker:alpha": "running",
        "swarm:worker:beta": "idle",
    }
    set_redis_client(
        _make_redis(worker_keys=worker_keys, worker_values=worker_values)
    )
    client = TestClient(dashboard)
    body = client.get("/workers/active").json()
    assert isinstance(body, list), f"Expected list, got {type(body)}"
    assert len(body) == 2, f"Expected 2 workers, got {len(body)}: {body}"


def test_bb3_active_workers_entry_shape():
    """BB3: Each worker entry has worker_id and status fields."""
    worker_keys = [b"swarm:worker:gamma"]
    worker_values = {"swarm:worker:gamma": "processing"}
    set_redis_client(_make_redis(worker_keys=worker_keys, worker_values=worker_values))
    client = TestClient(dashboard)
    body = client.get("/workers/active").json()
    assert len(body) == 1
    entry = body[0]
    assert "worker_id" in entry, f"worker_id missing: {entry}"
    assert "status" in entry, f"status missing: {entry}"
    assert entry["worker_id"] == "gamma", f"Expected worker_id='gamma', got {entry['worker_id']}"
    assert entry["status"] == "processing", f"Expected status='processing', got {entry['status']}"


# ---------------------------------------------------------------------------
# WB1 — queue_depth is the sum of LLEN on both bridge queues
# ---------------------------------------------------------------------------


def test_wb1_queue_depth_is_sum_of_both_bridge_queues():
    """WB1: queue_depth = LLEN(aiva_to_genesis) + LLEN(genesis_to_aiva)."""
    set_redis_client(
        _make_redis(q_aiva_to_genesis=7, q_genesis_to_aiva=3)
    )
    client = TestClient(dashboard)
    body = client.get("/status").json()
    assert body["queue_depth"] == 10, (
        f"Expected queue_depth=10 (7+3), got {body['queue_depth']}"
    )


def test_wb1_queue_depth_zero_when_queues_empty():
    """WB1: queue_depth is 0 when both queues are empty."""
    set_redis_client(_make_redis(q_aiva_to_genesis=0, q_genesis_to_aiva=0))
    client = TestClient(dashboard)
    body = client.get("/status").json()
    assert body["queue_depth"] == 0, (
        f"Expected queue_depth=0, got {body['queue_depth']}"
    )


# ---------------------------------------------------------------------------
# WB2 — active_calls is count of aiva:state:* keys
# ---------------------------------------------------------------------------


def test_wb2_active_calls_reflects_state_key_count():
    """WB2: active_calls equals the number of aiva:state:* keys in Redis."""
    state_keys = [
        "aiva:state:call-001",
        "aiva:state:call-002",
        "aiva:state:call-003",
    ]
    set_redis_client(_make_redis(state_keys=state_keys))
    client = TestClient(dashboard)
    body = client.get("/status").json()
    assert body["active_calls"] == 3, (
        f"Expected active_calls=3, got {body['active_calls']}"
    )


def test_wb2_active_calls_zero_when_no_state_keys():
    """WB2: active_calls is 0 when no aiva:state:* keys exist."""
    set_redis_client(_make_redis(state_keys=[]))
    client = TestClient(dashboard)
    body = client.get("/status").json()
    assert body["active_calls"] == 0, (
        f"Expected active_calls=0, got {body['active_calls']}"
    )


# ---------------------------------------------------------------------------
# WB3 — /calls/recent reads from Postgres, not Redis
# ---------------------------------------------------------------------------


def test_wb3_recent_calls_uses_postgres_fetch():
    """WB3: /calls/recent calls pg.fetch() (Postgres) — not Redis."""
    pg = _make_pg(rows=[_row()])
    redis = _make_redis()
    set_pg_pool(pg)
    set_redis_client(redis)

    client = TestClient(dashboard)
    client.get("/calls/recent")

    pg.fetch.assert_called_once(), "pg.fetch should be called exactly once"
    # Redis should NOT be involved in /calls/recent
    redis.keys.assert_not_called()
    redis.get.assert_not_called()


def test_wb3_recent_calls_pg_query_contains_royal_conversations():
    """WB3: The SQL query passed to pg.fetch references royal_conversations table."""
    pg = _make_pg(rows=[])
    set_pg_pool(pg)
    client = TestClient(dashboard)
    client.get("/calls/recent")

    call_args = pg.fetch.call_args
    sql: str = call_args[0][0] if call_args[0] else ""
    assert "royal_conversations" in sql, (
        f"Expected 'royal_conversations' in SQL query, got: {sql!r}"
    )


# ---------------------------------------------------------------------------
# COV1 — /status with no Redis → graceful zero response
# ---------------------------------------------------------------------------


def test_cov1_status_no_redis_returns_zeros():
    """COV1: When no Redis client is injected, /status returns all zeros gracefully."""
    # set_redis_client(None) already done by autouse fixture
    client = TestClient(dashboard)
    body = client.get("/status").json()
    assert body == {"active_calls": 0, "swarm_workers": 0, "queue_depth": 0}, (
        f"Expected all-zero status without Redis, got {body}"
    )


# ---------------------------------------------------------------------------
# COV2 — /calls/recent with no Postgres → empty list
# ---------------------------------------------------------------------------


def test_cov2_recent_calls_no_pg_returns_empty():
    """COV2: When no Postgres pool is injected, /calls/recent returns []."""
    client = TestClient(dashboard)
    body = client.get("/calls/recent").json()
    assert body == [], f"Expected [] without Postgres, got {body}"


# ---------------------------------------------------------------------------
# COV3 — /workers/active with no Redis → empty list
# ---------------------------------------------------------------------------


def test_cov3_active_workers_no_redis_returns_empty():
    """COV3: When no Redis client is injected, /workers/active returns []."""
    client = TestClient(dashboard)
    body = client.get("/workers/active").json()
    assert body == [], f"Expected [] without Redis, got {body}"
