"""tests/test_admin_tooling_824_752.py — Tests for admin tooling endpoints.

Covers:
  - GET  /api/v1/admin/owners          (#824)
  - POST /api/v1/admin/owners/{id}/reindex  (#824)
  - GET  /api/v1/admin/global-stats    (#824)
  - DELETE /api/v1/admin/memories      (#752)
"""
from __future__ import annotations

import asyncio
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock

import pytest

pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi.testclient import TestClient  # noqa: E402


def _make_fake_conn(rows_by_query: Optional[Dict[str, Any]] = None):
    rows_by_query = rows_by_query or {}

    class _FakeCursor:
        def __init__(self, rows):
            self._rows = rows
        async def fetchone(self):
            return self._rows[0] if self._rows else None
        async def fetchall(self):
            return list(self._rows)
        async def __aenter__(self):
            return self
        async def __aexit__(self, *_):
            pass
        def __aiter__(self):
            return iter(self._rows)

    class _FakeConn:
        def execute(self, sql, params=None):
            for keyword, rows in rows_by_query.items():
                if keyword.lower() in sql.lower():
                    return _FakeCursor(rows)
            return _FakeCursor([])
        async def commit(self):
            pass

    return _FakeConn()


def _make_mock_plugin(conn=None) -> MagicMock:
    plugin = MagicMock()
    plugin._init_errors = []
    cfg = MagicMock()
    cfg.owner_id = "default"
    cfg.quality_gates_enabled = False
    cfg.metrics_enabled = True
    plugin.config = cfg
    storage = MagicMock()
    storage.ping = AsyncMock(return_value=None)
    storage.initialize = AsyncMock(return_value=None)
    storage.close = AsyncMock(return_value=None)
    storage._write_lock = asyncio.Lock()
    if conn is not None:
        storage._get_conn = AsyncMock(return_value=conn)
    else:
        storage._get_conn = AsyncMock(return_value=_make_fake_conn())
    plugin.storage = storage
    metrics = MagicMock()
    async def _get_summary():
        return {"error_total": 0.0, "request_total": 0.0, "latency_p99": 0.0}
    metrics.get_summary = _get_summary
    plugin.metrics = metrics
    return plugin


def _build_client(plugin: MagicMock) -> TestClient:
    from fastapi import FastAPI
    from cortex.api.routes.admin import router as _admin_router, _make_routes
    import fastapi
    app = FastAPI()
    def _noop_verify(x: Any = None):
        return None
    # Create a fresh router for this test to avoid state bleed
    import fastapi.routing
    fresh_router = fastapi.routing.APIRouter()
    import cortex.api.routes.admin as _mod
    orig_router = _mod.router
    _mod.router = fresh_router
    _make_routes(verify_api_key=_noop_verify)
    app.include_router(fresh_router)
    _mod.router = orig_router
    from cortex.api import deps as _deps
    _deps.plugin_holder["plugin"] = plugin
    return TestClient(app)


class TestAdminListOwners:
    @pytest.fixture
    def client(self):
        conn = _make_fake_conn({"group by owner_id": [("alice", 42), ("bob", 7)]})
        return _build_client(_make_mock_plugin(conn=conn))

    def test_returns_200(self, client):
        assert client.get("/api/v1/admin/owners").status_code == 200

    def test_owners_list_populated(self, client):
        data = client.get("/api/v1/admin/owners").json()
        assert len(data["owners"]) == 2

    def test_owner_entries_have_required_fields(self, client):
        data = client.get("/api/v1/admin/owners").json()
        for e in data["owners"]:
            assert "owner_id" in e and "claim_count" in e

    def test_total_owners_field(self, client):
        assert client.get("/api/v1/admin/owners").json()["total_owners"] == 2

    def test_no_errors_on_success(self, client):
        assert client.get("/api/v1/admin/owners").json().get("errors", []) == []

    def test_storage_unavailable_returns_error(self):
        p = _make_mock_plugin(); p.storage = None
        resp = _build_client(p).get("/api/v1/admin/owners")
        assert resp.status_code == 200 and len(resp.json()["errors"]) > 0


class TestAdminReindexOwner:
    @pytest.fixture
    def client_with_claims(self):
        conn = _make_fake_conn({"select id, subject": [("c1","s1","p1","o1"),("c2","s2","p2","o2"),("c3","s3","p3","o3")]})
        return _build_client(_make_mock_plugin(conn=conn))

    def test_returns_200(self, client_with_claims):
        assert client_with_claims.post("/api/v1/admin/owners/alice/reindex").status_code == 200

    def test_response_has_job_id(self, client_with_claims):
        data = client_with_claims.post("/api/v1/admin/owners/alice/reindex").json()
        assert "job_id" in data and len(data["job_id"]) > 0

    def test_response_has_claims_queued(self, client_with_claims):
        data = client_with_claims.post("/api/v1/admin/owners/alice/reindex").json()
        assert data["claims_queued"] == 3

    def test_response_status_queued(self, client_with_claims):
        data = client_with_claims.post("/api/v1/admin/owners/alice/reindex").json()
        assert data["status"] == "queued"

    def test_owner_id_echoed(self, client_with_claims):
        data = client_with_claims.post("/api/v1/admin/owners/alice/reindex").json()
        assert data["owner_id"] == "alice"

    def test_noop_when_no_claims(self):
        conn = _make_fake_conn({"select id, subject": []})
        client = _build_client(_make_mock_plugin(conn=conn))
        data = client.post("/api/v1/admin/owners/ghost/reindex").json()
        assert data["claims_queued"] == 0 and data["status"] == "noop"

    def test_storage_unavailable_returns_error(self):
        p = _make_mock_plugin(); p.storage = None
        data = _build_client(p).post("/api/v1/admin/owners/alice/reindex").json()
        assert data["status"] == "error" and len(data["errors"]) > 0


class TestAdminGlobalStats:
    @pytest.fixture
    def client(self):
        conn = _make_fake_conn({
            "count(*) from semantic_claim where": [(150,)],
            "group by owner_id": [("alice", 100), ("bob", 50)],
            "group by category": [("episodic", 80), ("semantic", 60), ("misc", 10)],
            "inner join embedding_index": [(120,)],
        })
        return _build_client(_make_mock_plugin(conn=conn))

    def test_returns_200(self, client):
        assert client.get("/api/v1/admin/global-stats").status_code == 200

    def test_has_total_claims(self, client):
        data = client.get("/api/v1/admin/global-stats").json()
        assert "total_claims" in data and isinstance(data["total_claims"], int)

    def test_has_claims_by_owner(self, client):
        data = client.get("/api/v1/admin/global-stats").json()
        assert "claims_by_owner" in data and isinstance(data["claims_by_owner"], dict)

    def test_has_claims_by_category(self, client):
        data = client.get("/api/v1/admin/global-stats").json()
        assert "claims_by_category" in data and isinstance(data["claims_by_category"], dict)

    def test_has_embedding_coverage_pct(self, client):
        data = client.get("/api/v1/admin/global-stats").json()
        val = data["embedding_coverage_pct"]
        assert isinstance(val, (int, float)) and 0.0 <= val <= 100.0

    def test_has_total_owners(self, client):
        assert "total_owners" in client.get("/api/v1/admin/global-stats").json()

    def test_storage_unavailable_returns_error(self):
        p = _make_mock_plugin(); p.storage = None
        resp = _build_client(p).get("/api/v1/admin/global-stats")
        assert resp.status_code == 200 and len(resp.json()["errors"]) > 0


class TestAdminBulkDeleteMemories:
    def _make_delete_client(self, id_rows=None):
        if id_rows is None:
            id_rows = [("claim-1",), ("claim-2",), ("claim-3",)]
        conn = _make_fake_conn({
            "select id from semantic_claim": id_rows,
            "select count(*)": [(len(id_rows),)],
        })
        return _build_client(_make_mock_plugin(conn=conn))

    def test_returns_200_for_live_delete(self):
        resp = self._make_delete_client().request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice"})
        assert resp.status_code == 200

    def test_deleted_count_returned(self):
        data = self._make_delete_client().request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice"}).json()
        assert "deleted" in data and isinstance(data["deleted"], int)

    def test_owner_id_echoed_in_response(self):
        data = self._make_delete_client().request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice"}).json()
        assert data["owner_id"] == "alice"

    def test_dry_run_returns_would_delete_not_deleted(self):
        conn = _make_fake_conn({"select count(*)": [(5,)]})
        client = _build_client(_make_mock_plugin(conn=conn))
        data = client.request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice", "dry_run": True}).json()
        assert "would_delete" in data and data.get("deleted") is None and data["dry_run"] is True

    def test_dry_run_does_not_actually_delete(self):
        executed_sqls: List[str] = []
        class _TrackingCursor:
            def __init__(self, rows): self._rows = rows
            async def fetchone(self): return self._rows[0] if self._rows else None
            async def fetchall(self): return list(self._rows)
            async def __aenter__(self): return self
            async def __aexit__(self, *_): pass
        class _TrackingConn:
            def execute(self, sql, params=None):
                executed_sqls.append(sql.strip().upper())
                return _TrackingCursor([(3,)])
            async def commit(self): pass
        p = _make_mock_plugin(conn=_TrackingConn())
        _build_client(p).request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice", "dry_run": True})
        mutating = [s for s in executed_sqls if s.startswith(("UPDATE", "DELETE"))]
        assert len(mutating) == 0, f"Dry run issued mutating SQL: {mutating}"

    def test_category_filter_accepted(self):
        resp = self._make_delete_client(id_rows=[("c1",)]).request(
            "DELETE", "/api/v1/admin/memories", json={"owner_id": "alice", "category": "episodic"})
        assert resp.status_code == 200 and resp.json()["owner_id"] == "alice"

    def test_owner_id_required(self):
        conn = _make_fake_conn({})
        resp = _build_client(_make_mock_plugin(conn=conn)).request(
            "DELETE", "/api/v1/admin/memories", json={"category": "episodic"})
        assert resp.status_code == 422

    def test_storage_unavailable_returns_error(self):
        p = _make_mock_plugin(); p.storage = None
        data = _build_client(p).request("DELETE", "/api/v1/admin/memories", json={"owner_id": "alice"}).json()
        assert len(data["errors"]) > 0

    def test_no_errors_on_empty_owner(self):
        data = self._make_delete_client(id_rows=[]).request(
            "DELETE", "/api/v1/admin/memories", json={"owner_id": "ghost"}).json()
        assert data.get("errors", []) == [] and data["deleted"] == 0
