from __future__ import annotations

import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

from fastapi.testclient import TestClient

import api
from api.state import STATE_LOCK, TOKENS, USERS
from src import env_settings


class ApiTests(unittest.TestCase):
    def setUp(self) -> None:
        self.bootstrap_token = "bootstrap-admin-token"
        from cryptography.fernet import Fernet
        self.fernet_key = Fernet.generate_key().decode()
        self._env = patch.dict(
            os.environ,
            {
                "AGENT_BOOTSTRAP_ADMIN_TOKEN": self.bootstrap_token,
                "AGENT_BOOTSTRAP_ADMIN_SUBJECT": "test-bootstrap-admin",
                "AGENT_LANGFUSE_CONTROL_ALLOW_MUTATION": "true",
                "AGENT_DATA_ENCRYPTION_KEY": self.fernet_key,
            },
            clear=False,
        )
        self._env.start()
        self.client = TestClient(api.app)
        with STATE_LOCK:
            USERS.clear()
            TOKENS.clear()

    def tearDown(self) -> None:
        self._env.stop()

    def _admin_headers(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self.bootstrap_token}"}

    def test_health(self) -> None:
        res = self.client.get("/health")
        self.assertEqual(res.status_code, 200)
        body = res.json()
        self.assertTrue(body.get("ok"))

    def test_home_page(self) -> None:
        res = self.client.get("/")
        self.assertEqual(res.status_code, 200)
        self.assertIn("Langfuse Lens", res.text)

    def test_chat_endpoint(self) -> None:
        with patch("api.routers.auth.run", return_value="## Metrics summary\n- error_rate: 1.00%\n- avg_latency_ms: 1234"):
            res = self.client.post("/api/chat", json={"message": "mode=overview", "thread_id": "th_demo"}, headers=self._admin_headers())
        self.assertEqual(res.status_code, 200)
        body = res.json()
        self.assertIn("output", body)
        self.assertIn("metrics", body)
        self.assertEqual(body.get("thread_id"), "th_demo")
        self.assertEqual(body["metrics"].get("error_rate"), "1.00%")

    def test_chat_endpoint_rejects_unauthenticated(self) -> None:
        res = self.client.post("/api/chat", json={"message": "hi"})
        self.assertEqual(res.status_code, 401)

    def test_chat_db_persistence(self) -> None:
        """Chat messages should be persisted to DB and retrievable via threads API."""
        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(os.environ, {"AGENT_APP_DB_PATH": db_path, "AGENT_AUTH_ALLOW_SIGNUP": "true"}, clear=False):
                from src.user_store import init_user_store
                init_user_store()

                # Create user and login to get a session token
                self.client.post("/api/auth/register", json={"username": "chatuser", "password": "password1234"})
                login_res = self.client.post("/api/auth/login", json={"username": "chatuser", "password": "password1234"})
                token = login_res.json()["data"]["access_token"]
                auth_headers = {"Authorization": f"Bearer {token}"}

                with patch("api.routers.auth.run", return_value="Hello from agent"):
                    res = self.client.post("/api/chat", json={"message": "hi", "thread_id": "th_persist"}, headers=auth_headers)
                self.assertEqual(res.status_code, 200)

                threads_res = self.client.get("/api/chat/threads?limit=10", headers=auth_headers)
                self.assertEqual(threads_res.status_code, 200)
                threads = threads_res.json().get("data", [])
                thread_ids = [t["thread_id"] for t in threads]
                self.assertIn("th_persist", thread_ids)

                msgs_res = self.client.get("/api/chat/threads/th_persist/messages", headers=auth_headers)
                self.assertEqual(msgs_res.status_code, 200)
                msgs = msgs_res.json().get("data", [])
                self.assertGreaterEqual(len(msgs), 2)
                roles = [m["role"] for m in msgs]
                self.assertIn("user", roles)
                self.assertIn("assistant", roles)

    def test_audit_log_db(self) -> None:
        """Audit logs should be persisted to DB via _record_audit."""
        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(os.environ, {"AGENT_APP_DB_PATH": db_path}, clear=False):
                from src.user_store import init_user_store
                init_user_store()

                # Create admin user triggers _record_audit
                self.client.post(
                    "/api/admin/users",
                    headers=self._admin_headers(),
                    json={"email": "test@local", "name": "Test", "roles": ["viewer"]},
                )

                audit_res = self.client.get("/api/admin/audit-logs", headers=self._admin_headers())
                self.assertEqual(audit_res.status_code, 200)
                self.assertGreaterEqual(audit_res.json()["data"]["total"], 1)

    def test_extract_metrics(self) -> None:
        from api.deps import _extract_metrics

        output = """## Metrics summary\n- error_rate: 0.00%\n- avg_latency_ms: 55\n\n## Heuristic opinions\n- ok"""
        metrics = _extract_metrics(output)
        self.assertEqual(metrics["error_rate"], "0.00%")
        self.assertEqual(metrics["avg_latency_ms"], "55")

    def test_get_llm_settings(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            project_env = Path(tmp) / "project.env"
            home_env = Path(tmp) / "home.env"
            project_env.write_text("OPENAI_API_KEY=sk-test\n", encoding="utf-8")
            with patch.object(env_settings, "PROJECT_ENV_PATH", project_env), patch.object(
                env_settings, "HOME_ENV_PATH", home_env
            ):
                env_settings._ENV_BOOTSTRAPPED = False
                with patch.dict(os.environ, {"AGENT_BOOTSTRAP_ADMIN_TOKEN": self.bootstrap_token}, clear=True):
                    # Requires owner+ role — use bootstrap admin token
                    res = self.client.get(
                        "/api/settings/llm-keys",
                        headers={"Authorization": f"Bearer {self.bootstrap_token}"},
                    )
                    self.assertEqual(res.status_code, 200)
                    body = res.json()
                    self.assertTrue(body["status"]["openai"])
                    self.assertTrue(body["present"]["OPENAI_API_KEY"])

    def test_bootstrap_config_status_endpoint(self) -> None:
        with patch(
            "api.routers.settings.get_bootstrap_config_status",
            return_value={"loaded": True, "path": "/tmp/Config.yaml", "applied_keys": ["AGENT_DEFAULT_MODE"]},
        ):
            res = self.client.get("/api/settings/bootstrap-config", headers=self._admin_headers())
        self.assertEqual(res.status_code, 200)
        self.assertTrue(res.json()["data"]["loaded"])

    def test_save_llm_settings(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            project_env = Path(tmp) / "project.env"
            home_env = Path(tmp) / "home.env"
            with patch.object(env_settings, "PROJECT_ENV_PATH", project_env), patch.object(
                env_settings, "HOME_ENV_PATH", home_env
            ):
                env_settings._ENV_BOOTSTRAPPED = False
                with patch.dict(os.environ, {"AGENT_BOOTSTRAP_ADMIN_TOKEN": self.bootstrap_token}, clear=True):
                    res = self.client.post(
                        "/api/settings/llm-keys",
                        json={"target": "project", "openai_api_key": "sk-123"},
                        headers={"Authorization": f"Bearer {self.bootstrap_token}"},
                    )
                    self.assertEqual(res.status_code, 200)
                    self.assertIn("OPENAI_API_KEY=sk-123", project_env.read_text(encoding="utf-8"))

    def test_extended_settings_endpoints(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            project_env = Path(tmp) / "project.env"
            home_env = Path(tmp) / "home.env"
            with patch.object(env_settings, "PROJECT_ENV_PATH", project_env), patch.object(
                env_settings, "HOME_ENV_PATH", home_env
            ):
                env_settings._ENV_BOOTSTRAPPED = False
                with patch.dict(os.environ, {}, clear=False):
                    put_res = self.client.put(
                        "/api/settings/defaults",
                        headers=self._admin_headers(),
                        json={
                            "target": "project",
                            "default_mode": "overview",
                            "default_limit": 25,
                            "timezone": "UTC",
                            "retention_days": 14,
                            "default_compare_models": True,
                        },
                    )
                    self.assertEqual(put_res.status_code, 200)

                    get_res = self.client.get("/api/settings/defaults", headers=self._admin_headers())
                    self.assertEqual(get_res.status_code, 200)
                    self.assertEqual(get_res.json()["data"]["default_limit"], 25)

    def test_admin_endpoints(self) -> None:
        create_res = self.client.post(
            "/api/admin/users",
            headers=self._admin_headers(),
            json={"email": "new@local", "name": "New User", "roles": ["viewer"]},
        )
        self.assertEqual(create_res.status_code, 200)
        user_id = create_res.json()["data"]["user_id"]

        role_res = self.client.put(
            f"/api/admin/users/{user_id}/roles",
            headers=self._admin_headers(),
            json={"roles": ["analyst"]},
        )
        self.assertEqual(role_res.status_code, 200)
        self.assertIn("analyst", role_res.json()["data"]["roles"])

        audit_res = self.client.get("/api/admin/audit-logs", headers=self._admin_headers())
        self.assertEqual(audit_res.status_code, 200)
        self.assertGreaterEqual(audit_res.json()["data"]["total"], 1)

    def test_default_role_is_not_super_admin(self) -> None:
        res = self.client.get("/api/admin/users")
        self.assertEqual(res.status_code, 401)

    def test_langfuse_control_endpoints(self) -> None:
        def _fake_run(operation, params=None, settings=None):
            return {"operation": operation, "result": {"ok": True, "params": params or {}}}

        with patch("api.deps.run_langfuse_control_operation", side_effect=_fake_run), patch(
            "api.deps.run_langfuse_control_message",
            side_effect=lambda message, settings=None: {"operation": "message", "result": {"message": message}},
        ), patch("api.routers.langfuse.parse_langfuse_control_message", return_value={"operation": "traces.list", "params": {"limit": 1}}), patch(
            "api.routers.langfuse.is_langfuse_openapi_mutating",
            return_value=False,
        ):
            status_res = self.client.get("/api/langfuse/control/status", headers=self._admin_headers())
            self.assertEqual(status_res.status_code, 200)
            self.assertEqual(status_res.json()["data"]["operation"], "status")

            exec_res = self.client.post(
                "/api/langfuse/control/execute",
                headers=self._admin_headers(),
                json={"operation": "traces.list", "params": {"limit": 1}},
            )
            self.assertEqual(exec_res.status_code, 200)
            self.assertEqual(exec_res.json()["data"]["operation"], "traces.list")

            msg_res = self.client.post(
                "/api/langfuse/control/message",
                headers=self._admin_headers(),
                json={"message": "op=traces.list limit=1"},
            )
            self.assertEqual(msg_res.status_code, 200)
            self.assertEqual(msg_res.json()["data"]["operation"], "message")

    def test_langfuse_management_endpoints(self) -> None:
        def _fake_run(operation, params=None, settings=None):
            return {"operation": operation, "result": {"ok": True, "params": params or {}}}

        with patch("api.deps.run_langfuse_control_operation", side_effect=_fake_run), patch(
            "api.routers.langfuse.is_langfuse_openapi_mutating",
            return_value=False,
        ):
            get_prompt = self.client.get(
                "/api/langfuse/prompts",
                headers=self._admin_headers(),
                params={"name": "demo.prompt", "label": "latest"},
            )
            self.assertEqual(get_prompt.status_code, 200)
            self.assertEqual(get_prompt.json()["data"]["operation"], "prompt.get")

            create_prompt = self.client.post(
                "/api/langfuse/prompts",
                headers=self._admin_headers(),
                json={"name": "demo.prompt", "prompt": "hello", "labels": ["latest"]},
            )
            self.assertEqual(create_prompt.status_code, 200)
            self.assertEqual(create_prompt.json()["data"]["operation"], "prompt.upsert")

            promote_prompt = self.client.post(
                "/api/langfuse/prompts/demo.prompt/promote-label",
                headers=self._admin_headers(),
                json={"label": "production", "version": 3},
            )
            self.assertEqual(promote_prompt.status_code, 200)
            self.assertEqual(promote_prompt.json()["data"]["operation"], "prompt.promote_label")

            create_dataset = self.client.post(
                "/api/langfuse/datasets",
                headers=self._admin_headers(),
                json={"name": "eval-ds", "description": "demo"},
            )
            self.assertEqual(create_dataset.status_code, 200)
            self.assertEqual(create_dataset.json()["data"]["operation"], "datasets.create")

            upsert_items = self.client.post(
                "/api/langfuse/datasets/ds_1/items:upsert",
                headers=self._admin_headers(),
                json={"items": [{"input": "hi"}]},
            )
            self.assertEqual(upsert_items.status_code, 200)
            self.assertEqual(upsert_items.json()["data"]["operation"], "dataset.items.upsert")

            create_score = self.client.post(
                "/api/langfuse/scores",
                headers=self._admin_headers(),
                json={"name": "quality", "value": 0.9, "trace_id": "tr_1"},
            )
            self.assertEqual(create_score.status_code, 200)
            self.assertEqual(create_score.json()["data"]["operation"], "score.create")

            create_annotation = self.client.post(
                "/api/langfuse/annotations",
                headers=self._admin_headers(),
                json={"label": "good", "trace_id": "tr_1"},
            )
            self.assertEqual(create_annotation.status_code, 200)
            self.assertEqual(create_annotation.json()["data"]["operation"], "annotation.create")

            openapi_spec = self.client.get("/api/langfuse/openapi/spec", headers=self._admin_headers())
            self.assertEqual(openapi_spec.status_code, 200)
            self.assertEqual(openapi_spec.json()["data"]["operation"], "openapi.spec")

            openapi_ops = self.client.get("/api/langfuse/openapi/operations", headers=self._admin_headers())
            self.assertEqual(openapi_ops.status_code, 200)
            self.assertEqual(openapi_ops.json()["data"]["operation"], "openapi.list_operations")

            openapi_exec = self.client.post(
                "/api/langfuse/openapi/execute",
                headers=self._admin_headers(),
                json={"operation_id": "projects_get"},
            )
            self.assertEqual(openapi_exec.status_code, 200)
            self.assertEqual(openapi_exec.json()["data"]["operation"], "openapi.execute")

    def test_langfuse_openapi_tool_registry_endpoint(self) -> None:
        with patch(
            "api.routers.langfuse.get_langfuse_openapi_tool_registry",
            return_value={"tool_count": 2, "operation_count": 2, "tools": [], "operations": []},
        ):
            response = self.client.get(
                "/api/langfuse/openapi/tool-registry",
                headers=self._admin_headers(),
                params={"refresh": "true", "include_mutating": "false", "max_tools": 100},
            )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["data"]["tool_count"], 2)

    def test_langfuse_tools_catalog_endpoint(self) -> None:
        with patch(
            "api.routers.langfuse.get_agent_tool_runtime_summary",
            return_value={"tool_count": 3, "profile": "analyst", "tools": []},
        ), patch(
            "api.routers.langfuse.get_agent_tool_presets",
            return_value={"default_preset": "analyst-balanced", "selected_preset": "analyst-balanced", "presets": []},
        ), patch(
            "api.routers.langfuse.get_langfuse_builtin_tool_registry",
            side_effect=[
                {"count": 10, "tools": []},
                {"count": 4, "tools": []},
            ],
        ):
            response = self.client.get("/api/langfuse/tools/catalog", headers=self._admin_headers())
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["data"]["runtime"]["profile"], "analyst")
        self.assertEqual(response.json()["data"]["presets"]["selected_preset"], "analyst-balanced")
        self.assertEqual(response.json()["data"]["builtin"]["count"], 10)

    def test_langfuse_tools_presets_endpoint(self) -> None:
        with patch(
            "api.routers.langfuse.get_agent_tool_presets",
            return_value={"default_preset": "analyst-balanced", "selected_preset": "readonly-safe", "presets": []},
        ):
            response = self.client.get("/api/langfuse/tools/presets", headers=self._admin_headers())
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["data"]["selected_preset"], "readonly-safe")

    def test_agent_state_endpoints(self) -> None:
        with patch(
            "api.routers.langfuse.get_agent_state_runtime_summary",
            return_value={"enabled": True, "ready": True, "sqlite_path": "data/agent_state/checkpoints.sqlite"},
        ), patch(
            "api.routers.langfuse.list_agent_state_checkpoints",
            return_value={"thread_id": "th1", "count": 1, "checkpoints": [{"checkpoint_id": "cp1"}]},
        ):
            status_res = self.client.get("/api/agent/state/status", headers=self._admin_headers())
            self.assertEqual(status_res.status_code, 200)
            self.assertTrue(status_res.json()["data"]["enabled"])

            checkpoints_res = self.client.get(
                "/api/agent/state/threads/th1/checkpoints",
                headers=self._admin_headers(),
                params={"limit": 10},
            )
            self.assertEqual(checkpoints_res.status_code, 200)
            self.assertEqual(checkpoints_res.json()["data"]["count"], 1)

    def test_user_auth_and_personal_langfuse_settings_flow(self) -> None:
        from src.user_store import init_user_store

        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(
                os.environ,
                {
                    "AGENT_APP_DB_PATH": db_path,
                    "AGENT_AUTH_ALLOW_SIGNUP": "true",
                },
                clear=False,
            ):
                init_user_store()
                register_res = self.client.post(
                    "/api/auth/register",
                    json={"username": "alice", "password": "Password123!", "role": "analyst"},
                )
                self.assertEqual(register_res.status_code, 200)

                login_res = self.client.post(
                    "/api/auth/login",
                    json={"username": "alice", "password": "Password123!"},
                )
                self.assertEqual(login_res.status_code, 200)
                user_token = login_res.json()["data"]["access_token"]
                headers = {"Authorization": f"Bearer {user_token}"}

                put_res = self.client.put(
                    "/api/users/me/langfuse-settings",
                    headers=headers,
                    json={
                        "base_url": "https://lf.example.com",
                        "public_key": "pk-user",
                        "secret_key": "sk-user",
                        "environment": "production",
                    },
                )
                self.assertEqual(put_res.status_code, 200)

                get_res = self.client.get("/api/users/me/langfuse-settings", headers=headers)
                self.assertEqual(get_res.status_code, 200)
                self.assertEqual(get_res.json()["data"]["base_url"], "https://lf.example.com")
                self.assertTrue(get_res.json()["data"]["public_key_masked"])

                # blank secrets should keep previously saved values
                put_keep_res = self.client.put(
                    "/api/users/me/langfuse-settings",
                    headers=headers,
                    json={
                        "base_url": "https://lf-2.example.com",
                        "public_key": "",
                        "secret_key": "",
                        "environment": "staging",
                    },
                )
                self.assertEqual(put_keep_res.status_code, 200)

                observed: dict[str, str] = {}

                def _fake_run(operation, params=None, settings=None):
                    observed["operation"] = str(operation)
                    observed["host"] = str((settings or {}).get("host") or "")
                    observed["public_key"] = str((settings or {}).get("public_key") or "")
                    observed["secret_key"] = str((settings or {}).get("secret_key") or "")
                    return {"operation": operation, "result": {"ok": True}}

                with patch("api.deps.run_langfuse_control_operation", side_effect=_fake_run):
                    status_res = self.client.get("/api/langfuse/control/status", headers=headers)
                self.assertEqual(status_res.status_code, 200)
                self.assertEqual(observed.get("operation"), "status")
                self.assertEqual(observed.get("host"), "https://lf-2.example.com")
                self.assertEqual(observed.get("public_key"), "pk-user")
                self.assertEqual(observed.get("secret_key"), "sk-user")

    def test_user_langfuse_settings_are_applied_to_control_calls(self) -> None:
        from src.user_store import init_user_store

        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(
                os.environ,
                {
                    "AGENT_APP_DB_PATH": db_path,
                    "AGENT_AUTH_ALLOW_SIGNUP": "true",
                },
                clear=False,
            ):
                init_user_store()
                _ = self.client.post(
                    "/api/auth/register",
                    json={"username": "bob", "password": "Password123!", "role": "analyst"},
                )
                login_res = self.client.post(
                    "/api/auth/login",
                    json={"username": "bob", "password": "Password123!"},
                )
                user_token = login_res.json()["data"]["access_token"]
                headers = {"Authorization": f"Bearer {user_token}"}
                _ = self.client.put(
                    "/api/users/me/langfuse-settings",
                    headers=headers,
                    json={
                        "base_url": "https://lf.user.com",
                        "public_key": "pk-bob",
                        "secret_key": "sk-bob",
                        "environment": "prod",
                    },
                )

                observed: dict = {}

                def _fake_run(operation, params=None, settings=None):
                    observed["operation"] = operation
                    observed["settings"] = settings or {}
                    return {"operation": operation, "result": {"ok": True}}

                with patch("api.deps.run_langfuse_control_operation", side_effect=_fake_run):
                    status_res = self.client.get("/api/langfuse/control/status", headers=headers)
                self.assertEqual(status_res.status_code, 200)
                self.assertEqual(observed["operation"], "status")
                self.assertEqual(observed["settings"]["host"], "https://lf.user.com")

    def test_bearer_token_auth_flow(self) -> None:
        token_res = self.client.post(
            "/api/admin/tokens",
            headers=self._admin_headers(),
            json={"name": "ci-admin", "role": "super_admin", "expires_days": 7},
        )
        self.assertEqual(token_res.status_code, 200)
        token = token_res.json()["data"]["token"]
        token_id = token_res.json()["data"]["token_id"]

        me_res = self.client.get("/api/auth/me", headers={"Authorization": f"Bearer {token}"})
        self.assertEqual(me_res.status_code, 200)
        self.assertEqual(me_res.json()["data"]["auth_mode"], "bearer-token")
        self.assertEqual(me_res.json()["data"]["role"], "super_admin")

        users_res = self.client.get("/api/admin/users", headers={"Authorization": f"Bearer {token}"})
        self.assertEqual(users_res.status_code, 200)

        revoke_res = self.client.delete(
            f"/api/admin/tokens/{token_id}",
            headers={"Authorization": f"Bearer {token}"},
        )
        self.assertEqual(revoke_res.status_code, 200)

        denied_res = self.client.get("/api/admin/users", headers={"Authorization": f"Bearer {token}"})
        self.assertEqual(denied_res.status_code, 401)

    def test_bearer_required(self) -> None:
        res = self.client.get("/api/auth/me")
        self.assertEqual(res.status_code, 401)
        legacy_res = self.client.get("/api/admin/users", headers={"x-role": "super_admin"})
        self.assertEqual(legacy_res.status_code, 401)

    def test_token_rotation_endpoints(self) -> None:
        create_res = self.client.post(
            "/api/admin/tokens",
            headers=self._admin_headers(),
            json={
                "name": "rot-me",
                "role": "owner",
                "expires_days": 1,
                "auto_rotate": True,
                "rotate_before_days": 3,
            },
        )
        self.assertEqual(create_res.status_code, 200)
        token_id = create_res.json()["data"]["token_id"]

        rotate_res = self.client.post(
            f"/api/admin/tokens/{token_id}/rotate",
            headers=self._admin_headers(),
        )
        self.assertEqual(rotate_res.status_code, 200)
        self.assertIn("new_token", rotate_res.json()["data"])

        bulk_res = self.client.post(
            "/api/admin/tokens/rotate-expiring",
            headers=self._admin_headers(),
            params={"within_days": 30, "dry_run": True},
        )
        self.assertEqual(bulk_res.status_code, 200)
        self.assertTrue(bulk_res.json()["data"]["dry_run"])

        list_res = self.client.get("/api/admin/tokens", headers=self._admin_headers())
        self.assertEqual(list_res.status_code, 200)
        self.assertGreaterEqual(list_res.json()["data"]["total"], 1)

    def test_admin_langfuse_settings_encryption_endpoints(self) -> None:
        with patch(
            "api.routers.admin.get_langfuse_settings_encryption_status",
            return_value={"total_rows": 2, "plaintext_rows": 1, "encrypted_rows": 1, "fully_encrypted": False},
        ), patch(
            "api.routers.admin.migrate_user_langfuse_settings_encryption",
            return_value={"dry_run": False, "affected_rows": 1, "status": {"fully_encrypted": True}},
        ):
            status_res = self.client.get(
                "/api/admin/system/langfuse-settings-encryption",
                headers=self._admin_headers(),
            )
            self.assertEqual(status_res.status_code, 200)
            self.assertEqual(status_res.json()["data"]["plaintext_rows"], 1)

            migrate_res = self.client.post(
                "/api/admin/system/langfuse-settings-encryption/migrate",
                headers=self._admin_headers(),
                params={"dry_run": False},
            )
            self.assertEqual(migrate_res.status_code, 200)
            self.assertEqual(migrate_res.json()["data"]["affected_rows"], 1)


    def test_report_generate_and_list(self) -> None:
        """Generate a daily report and verify it appears in the list."""
        from src.user_store import init_user_store

        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(os.environ, {"AGENT_APP_DB_PATH": db_path}, clear=False):
                init_user_store()

                fake_report = {
                    "summary": {"count": 5, "total_cost": 0.12},
                    "sessions": [{"session_id": "s1", "trace_count": 3}],
                    "model_usage": {"gpt-4": {"model": "gpt-4", "call_count": 5, "total_cost": 0.12}},
                    "timeline": [{"hour": 10, "trace_count": 3}],
                    "trace_count": 5,
                    "total_cost": 0.12,
                }

                with patch(
                    "api.routers.reports._build_langfuse_client",
                    return_value=(object(), {}, None),
                ), patch(
                    "api.routers.reports.generate_daily_report",
                    return_value=fake_report,
                ):
                    gen_res = self.client.post(
                        "/api/reports/generate",
                        headers=self._admin_headers(),
                        json={
                            "target_user_id": "user-a",
                            "period_type": "daily",
                            "period_start": "2026-01-15",
                        },
                    )
                self.assertEqual(gen_res.status_code, 200)
                self.assertTrue(gen_res.json()["ok"])
                report_id = gen_res.json()["data"]["id"]

                # List reports
                list_res = self.client.get("/api/reports", headers=self._admin_headers())
                self.assertEqual(list_res.status_code, 200)
                items = list_res.json()["data"]["items"]
                self.assertGreaterEqual(len(items), 1)
                ids = [r["id"] for r in items]
                self.assertIn(report_id, ids)

                # Get single report
                get_res = self.client.get(f"/api/reports/{report_id}", headers=self._admin_headers())
                self.assertEqual(get_res.status_code, 200)
                self.assertEqual(get_res.json()["data"]["target_user_id"], "user-a")
                self.assertEqual(get_res.json()["data"]["trace_count"], 5)

    def test_report_access_control(self) -> None:
        """Non-admin users can only see their own reports."""
        from src.user_store import init_user_store

        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(
                os.environ,
                {"AGENT_APP_DB_PATH": db_path, "AGENT_AUTH_ALLOW_SIGNUP": "true"},
                clear=False,
            ):
                init_user_store()

                # Register a normal user (analyst role)
                self.client.post(
                    "/api/auth/register",
                    json={"username": "reporter", "password": "Password123!", "role": "analyst"},
                )
                login_res = self.client.post(
                    "/api/auth/login",
                    json={"username": "reporter", "password": "Password123!"},
                )
                user_token = login_res.json()["data"]["access_token"]
                user_headers = {"Authorization": f"Bearer {user_token}"}

                fake_report = {
                    "summary": {"count": 2},
                    "sessions": [],
                    "model_usage": {},
                    "timeline": [],
                    "trace_count": 2,
                    "total_cost": 0.05,
                }

                # Admin generates a report for 'other-user'
                with patch(
                    "api.routers.reports._build_langfuse_client",
                    return_value=(object(), {}, None),
                ), patch(
                    "api.routers.reports.generate_daily_report",
                    return_value=fake_report,
                ):
                    admin_gen = self.client.post(
                        "/api/reports/generate",
                        headers=self._admin_headers(),
                        json={
                            "target_user_id": "other-user",
                            "period_type": "daily",
                            "period_start": "2026-01-20",
                        },
                    )
                self.assertEqual(admin_gen.status_code, 200)
                other_report_id = admin_gen.json()["data"]["id"]

                # Non-admin trying to generate for someone else → 403
                with patch(
                    "api.routers.reports._build_langfuse_client",
                    return_value=(object(), {}, None),
                ), patch(
                    "api.routers.reports.generate_daily_report",
                    return_value=fake_report,
                ):
                    denied_gen = self.client.post(
                        "/api/reports/generate",
                        headers=user_headers,
                        json={
                            "target_user_id": "other-user",
                            "period_type": "daily",
                            "period_start": "2026-01-21",
                        },
                    )
                self.assertEqual(denied_gen.status_code, 403)

                # Non-admin trying to access other's report → 403
                get_denied = self.client.get(f"/api/reports/{other_report_id}", headers=user_headers)
                self.assertEqual(get_denied.status_code, 403)

                # Non-admin trying to delete → 403 (reports:admin required)
                del_denied = self.client.delete(f"/api/reports/{other_report_id}", headers=user_headers)
                self.assertEqual(del_denied.status_code, 403)

    def test_report_crud_full_flow(self) -> None:
        """Full CRUD: generate → get → list → delete."""
        from src.user_store import init_user_store

        with tempfile.TemporaryDirectory() as tmp:
            db_path = str(Path(tmp) / "app.sqlite")
            with patch.dict(os.environ, {"AGENT_APP_DB_PATH": db_path}, clear=False):
                init_user_store()

                fake_report = {
                    "summary": {"count": 10, "total_cost": 0.5},
                    "sessions": [],
                    "model_usage": {},
                    "timeline": [],
                    "trace_count": 10,
                    "total_cost": 0.5,
                }

                # Generate
                with patch(
                    "api.routers.reports._build_langfuse_client",
                    return_value=(object(), {}, None),
                ), patch(
                    "api.routers.reports.generate_daily_report",
                    return_value=fake_report,
                ):
                    gen_res = self.client.post(
                        "/api/reports/generate",
                        headers=self._admin_headers(),
                        json={
                            "target_user_id": "user-b",
                            "period_type": "daily",
                            "period_start": "2026-02-01",
                        },
                    )
                self.assertEqual(gen_res.status_code, 200)
                report_id = gen_res.json()["data"]["id"]

                # Get
                get_res = self.client.get(f"/api/reports/{report_id}", headers=self._admin_headers())
                self.assertEqual(get_res.status_code, 200)
                self.assertEqual(get_res.json()["data"]["period_type"], "daily")

                # List with filter
                list_res = self.client.get(
                    "/api/reports",
                    headers=self._admin_headers(),
                    params={"target_user_id": "user-b", "period_type": "daily"},
                )
                self.assertEqual(list_res.status_code, 200)
                self.assertEqual(list_res.json()["data"]["total"], 1)

                # Delete
                del_res = self.client.delete(f"/api/reports/{report_id}", headers=self._admin_headers())
                self.assertEqual(del_res.status_code, 200)
                self.assertTrue(del_res.json()["data"]["deleted"])

                # Verify deleted
                get_deleted = self.client.get(f"/api/reports/{report_id}", headers=self._admin_headers())
                self.assertEqual(get_deleted.status_code, 404)


if __name__ == "__main__":
    unittest.main()
