"""Tests for the admin CLI command group (U8).

These tests exercise the Typer command callbacks in
:mod:`agentkit.cli.admin` by mocking :class:`AdminHttpClient` so no
real HTTP traffic is generated. They verify that each command:
- Calls the correct endpoint with the correct method/body/params.
- Exits 0 on success and prints the expected output.
- Handles connection / HTTP errors gracefully with a non-zero exit.
"""

from __future__ import annotations

import os
import tempfile
from unittest.mock import MagicMock, patch

import httpx
import pytest
from typer.testing import CliRunner

from agentkit.cli.admin import admin_app

runner = CliRunner()


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_client():
    """Patch :class:`AdminHttpClient` and return the mock instance.

    The mock's ``base_url`` is set so error handlers can format messages.
    """
    with patch("agentkit.cli.admin.AdminHttpClient") as mock_client_class:
        mock = MagicMock()
        mock.base_url = "http://localhost:8001"
        mock_client_class.from_config.return_value = mock
        yield mock


# ---------------------------------------------------------------------------
# Department commands
# ---------------------------------------------------------------------------


class TestDepartmentCommands:
    def test_dept_list_json(self, mock_client):
        """department list --json outputs raw JSON."""
        mock_client.get.return_value = [
            {
                "id": "dept-1",
                "name": "HR",
                "description": "Human Resources",
                "is_active": True,
                "created_at": "2026-01-01T00:00:00Z",
            }
        ]
        result = runner.invoke(admin_app, ["department", "list", "--json"])
        assert result.exit_code == 0
        assert "HR" in result.stdout
        assert "dept-1" in result.stdout
        mock_client.get.assert_called_once_with("/api/v1/admin/departments", params=None)

    def test_dept_list_table(self, mock_client):
        """department list renders a Rich table by default."""
        mock_client.get.return_value = [
            {
                "id": "dept-1",
                "name": "Engineering",
                "description": "Eng team",
                "is_active": True,
                "created_at": "2026-01-01",
            }
        ]
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code == 0
        assert "Engineering" in result.stdout
        assert "Departments" in result.stdout

    def test_dept_list_empty(self, mock_client):
        """department list with no results shows empty message."""
        mock_client.get.return_value = []
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code == 0
        assert "No departments" in result.stdout

    def test_dept_create(self, mock_client):
        """department create posts the correct body."""
        mock_client.post.return_value = {
            "id": "dept-2",
            "name": "Finance",
            "description": "Finance team",
        }
        result = runner.invoke(
            admin_app,
            [
                "department",
                "create",
                "--name",
                "Finance",
                "--description",
                "Finance team",
            ],
        )
        assert result.exit_code == 0
        assert "Finance" in result.stdout
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/departments",
            json={"name": "Finance", "description": "Finance team"},
        )

    def test_dept_update(self, mock_client):
        """department update patches only provided fields."""
        mock_client.patch.return_value = {"id": "dept-1", "name": "HR Updated"}
        result = runner.invoke(
            admin_app,
            ["department", "update", "dept-1", "--name", "HR Updated"],
        )
        assert result.exit_code == 0
        mock_client.patch.assert_called_once_with(
            "/api/v1/admin/departments/dept-1",
            json={"name": "HR Updated"},
        )

    def test_dept_update_no_fields_errors(self, mock_client):
        """department update without --name or --description exits non-zero."""
        result = runner.invoke(admin_app, ["department", "update", "dept-1"])
        assert result.exit_code != 0
        assert "Provide" in result.stdout or "Error" in result.stdout

    def test_dept_delete(self, mock_client):
        """department delete calls DELETE and prints success."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(admin_app, ["department", "delete", "dept-1"])
        assert result.exit_code == 0
        assert "deleted" in result.stdout.lower()
        mock_client.delete.assert_called_once_with("/api/v1/admin/departments/dept-1", params=None)

    def test_dept_enable(self, mock_client):
        """department enable calls the enable endpoint."""
        mock_client.post.return_value = {"id": "dept-1", "is_active": True}
        result = runner.invoke(admin_app, ["department", "enable", "dept-1"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/enable", json=None
        )

    def test_dept_disable(self, mock_client):
        """department disable calls the disable endpoint."""
        mock_client.post.return_value = {"id": "dept-1", "is_active": False}
        result = runner.invoke(admin_app, ["department", "disable", "dept-1"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/disable", json=None
        )

    def test_dept_bind_skill(self, mock_client):
        """department bind-skill posts to the binding endpoint."""
        mock_client.post.return_value = {"bound": True}
        result = runner.invoke(
            admin_app,
            ["department", "bind-skill", "dept-1", "content_generator"],
        )
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/skills/content_generator",
            json=None,
        )

    def test_dept_unbind_skill(self, mock_client):
        """department unbind-skill deletes the binding."""
        mock_client.delete.return_value = {"unbound": True}
        result = runner.invoke(
            admin_app,
            ["department", "unbind-skill", "dept-1", "content_generator"],
        )
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/skills/content_generator",
            params=None,
        )

    def test_dept_bind_kb(self, mock_client):
        """department bind-kb posts to the KB binding endpoint."""
        mock_client.post.return_value = {"bound": True}
        result = runner.invoke(
            admin_app,
            ["department", "bind-kb", "dept-1", "kb-src-1"],
        )
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/kb/kb-src-1",
            json=None,
        )

    def test_dept_unbind_kb(self, mock_client):
        """department unbind-kb deletes the KB binding."""
        mock_client.delete.return_value = {"unbound": True}
        result = runner.invoke(
            admin_app,
            ["department", "unbind-kb", "dept-1", "kb-src-1"],
        )
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/kb/kb-src-1",
            params=None,
        )

    def test_dept_list_skills(self, mock_client):
        """department list-skills returns bound skill names."""
        mock_client.get.return_value = ["content_generator", "code_reviewer"]
        result = runner.invoke(admin_app, ["department", "list-skills", "dept-1"])
        assert result.exit_code == 0
        assert "content_generator" in result.stdout

    def test_dept_list_quotas(self, mock_client):
        """department list-quotas returns quota rows."""
        mock_client.get.return_value = [
            {"quota_type": "token", "limit_value": 100000, "period": "daily"}
        ]
        result = runner.invoke(admin_app, ["department", "list-quotas", "dept-1"])
        assert result.exit_code == 0
        assert "token" in result.stdout

    def test_dept_set_quota_token(self, mock_client):
        """department set-quota with token type sends int limit."""
        mock_client.put.return_value = {"quota_type": "token", "limit_value": 100000}
        result = runner.invoke(
            admin_app,
            [
                "department",
                "set-quota",
                "dept-1",
                "--type",
                "token",
                "--limit",
                "100000",
            ],
        )
        assert result.exit_code == 0
        mock_client.put.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/quotas",
            json={
                "quota_type": "token",
                "limit_value": 100000,
                "period": "daily",
            },
        )

    def test_dept_set_quota_whitelist(self, mock_client):
        """department set-quota with model_whitelist sends list."""
        mock_client.put.return_value = {"quota_type": "model_whitelist"}
        result = runner.invoke(
            admin_app,
            [
                "department",
                "set-quota",
                "dept-1",
                "--type",
                "model_whitelist",
                "--limit",
                "gpt-4,claude-3",
            ],
        )
        assert result.exit_code == 0
        mock_client.put.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/quotas",
            json={
                "quota_type": "model_whitelist",
                "limit_value": ["gpt-4", "claude-3"],
                "period": "daily",
            },
        )

    def test_dept_delete_quota(self, mock_client):
        """department delete-quota passes query params."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(
            admin_app,
            [
                "department",
                "delete-quota",
                "dept-1",
                "--type",
                "token",
                "--period",
                "monthly",
            ],
        )
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with(
            "/api/v1/admin/departments/dept-1/quotas",
            params={"quota_type": "token", "period": "monthly"},
        )


# ---------------------------------------------------------------------------
# User commands
# ---------------------------------------------------------------------------


class TestUserCommands:
    def test_user_list(self, mock_client):
        """user list returns users table."""
        mock_client.get.return_value = [
            {
                "id": "user-1",
                "username": "alice",
                "email": "alice@example.com",
                "role": "member",
                "is_active": True,
            }
        ]
        result = runner.invoke(admin_app, ["user", "list"])
        assert result.exit_code == 0
        assert "alice" in result.stdout
        mock_client.get.assert_called_once_with("/api/v1/admin/users", params=None)

    def test_user_list_with_department_filter(self, mock_client):
        """user list --department-id passes the filter."""
        mock_client.get.return_value = []
        result = runner.invoke(admin_app, ["user", "list", "--department-id", "dept-1"])
        assert result.exit_code == 0
        mock_client.get.assert_called_once_with(
            "/api/v1/admin/users", params={"department_id": "dept-1"}
        )

    def test_user_create(self, mock_client):
        """user create posts the user body."""
        mock_client.post.return_value = {
            "id": "user-2",
            "username": "bob",
            "email": "bob@example.com",
        }
        result = runner.invoke(
            admin_app,
            [
                "user",
                "create",
                "--username",
                "bob",
                "--email",
                "bob@example.com",
                "--password",
                "secret123",
            ],
        )
        assert result.exit_code == 0
        assert "bob" in result.stdout
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/users",
            json={
                "username": "bob",
                "email": "bob@example.com",
                "password": "secret123",
                "role": "member",
            },
        )

    def test_user_create_with_departments(self, mock_client):
        """user create --department-ids splits the comma list."""
        mock_client.post.return_value = {"id": "user-3", "username": "carol"}
        result = runner.invoke(
            admin_app,
            [
                "user",
                "create",
                "--username",
                "carol",
                "--email",
                "carol@example.com",
                "--password",
                "secret123",
                "--department-ids",
                "dept-1,dept-2",
            ],
        )
        assert result.exit_code == 0
        body = mock_client.post.call_args.kwargs["json"]
        assert body["department_ids"] == ["dept-1", "dept-2"]

    def test_user_update_role(self, mock_client):
        """user update --role patches only the role field."""
        mock_client.patch.return_value = {"id": "user-1", "role": "admin"}
        result = runner.invoke(admin_app, ["user", "update", "user-1", "--role", "admin"])
        assert result.exit_code == 0
        mock_client.patch.assert_called_once_with(
            "/api/v1/admin/users/user-1",
            json={"role": "admin"},
        )

    def test_user_delete(self, mock_client):
        """user delete soft-deletes the user."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(admin_app, ["user", "delete", "user-1"])
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with("/api/v1/admin/users/user-1", params=None)

    def test_user_reset_password(self, mock_client):
        """user reset-password posts the new password."""
        mock_client.post.return_value = {"reset": True}
        result = runner.invoke(
            admin_app,
            ["user", "reset-password", "user-1", "--password", "newpass"],
        )
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/users/user-1/reset-password",
            json={"new_password": "newpass"},
        )

    def test_user_assign_department(self, mock_client):
        """user assign-department posts to the assignment endpoint."""
        mock_client.post.return_value = {"assigned": True}
        result = runner.invoke(admin_app, ["user", "assign-department", "user-1", "dept-1"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/users/user-1/departments/dept-1",
            json=None,
        )

    def test_user_remove_department(self, mock_client):
        """user remove-department deletes the assignment."""
        mock_client.delete.return_value = {"removed": True}
        result = runner.invoke(admin_app, ["user", "remove-department", "user-1", "dept-1"])
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with(
            "/api/v1/admin/users/user-1/departments/dept-1",
            params=None,
        )


# ---------------------------------------------------------------------------
# LLM commands
# ---------------------------------------------------------------------------


class TestLlmCommands:
    def test_llm_list_providers(self, mock_client):
        """llm list-providers returns the providers table."""
        mock_client.get.return_value = [
            {
                "name": "openai",
                "type": "openai",
                "base_url": "",
                "api_key": "sk-***",
                "max_tokens": 4096,
                "timeout": 60.0,
            }
        ]
        result = runner.invoke(admin_app, ["llm", "list-providers"])
        assert result.exit_code == 0
        assert "openai" in result.stdout

    def test_llm_add_provider(self, mock_client):
        """llm add-provider posts the provider config."""
        mock_client.post.return_value = {"name": "anthropic"}
        result = runner.invoke(
            admin_app,
            [
                "llm",
                "add-provider",
                "--name",
                "anthropic",
                "--api-key",
                "sk-ant-xxx",
            ],
        )
        assert result.exit_code == 0
        body = mock_client.post.call_args.kwargs["json"]
        assert body["name"] == "anthropic"
        assert body["api_key"] == "sk-ant-xxx"

    def test_llm_update_provider(self, mock_client):
        """llm update-provider patches the provider."""
        mock_client.patch.return_value = {"name": "openai"}
        result = runner.invoke(
            admin_app,
            ["llm", "update-provider", "openai", "--max-tokens", "8192"],
        )
        assert result.exit_code == 0
        mock_client.patch.assert_called_once_with(
            "/api/v1/admin/llm/providers/openai",
            json={"max_tokens": 8192},
        )

    def test_llm_delete_provider(self, mock_client):
        """llm delete-provider deletes the provider."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(admin_app, ["llm", "delete-provider", "openai"])
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with(
            "/api/v1/admin/llm/providers/openai", params=None
        )

    def test_llm_set_api_key(self, mock_client):
        """llm set-api-key posts the new key."""
        mock_client.post.return_value = {"name": "openai", "api_key": "sk-***"}
        result = runner.invoke(
            admin_app,
            ["llm", "set-api-key", "openai", "--api-key", "sk-new"],
        )
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/llm/providers/openai/api-key",
            json={"api_key": "sk-new"},
        )

    def test_llm_list_fallbacks(self, mock_client):
        """llm list-fallbacks returns the fallback map."""
        mock_client.get.return_value = {"gpt-4": ["openai/gpt-4", "anthropic/claude-3"]}
        result = runner.invoke(admin_app, ["llm", "list-fallbacks"])
        assert result.exit_code == 0
        assert "gpt-4" in result.stdout

    def test_llm_set_fallback(self, mock_client):
        """llm set-fallback puts the chain list."""
        mock_client.put.return_value = {"model": "gpt-4", "chain": ["a", "b"]}
        result = runner.invoke(
            admin_app,
            ["llm", "set-fallback", "gpt-4", "--chain", "openai/gpt-4,anthropic/claude-3"],
        )
        assert result.exit_code == 0
        mock_client.put.assert_called_once_with(
            "/api/v1/admin/llm/fallbacks/gpt-4",
            json={"chain": ["openai/gpt-4", "anthropic/claude-3"]},
        )

    def test_llm_delete_fallback(self, mock_client):
        """llm delete-fallback deletes the chain."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(admin_app, ["llm", "delete-fallback", "gpt-4"])
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with("/api/v1/admin/llm/fallbacks/gpt-4", params=None)


# ---------------------------------------------------------------------------
# Skill commands
# ---------------------------------------------------------------------------


class TestSkillCommands:
    def test_skill_list(self, mock_client):
        """skill list calls GET /skills (not /admin/skills)."""
        mock_client.get.return_value = [
            {"name": "content_generator", "agent_type": "llm", "description": "Gen"}
        ]
        result = runner.invoke(admin_app, ["skill", "list"])
        assert result.exit_code == 0
        mock_client.get.assert_called_once_with("/api/v1/skills", params=None)

    def test_skill_enable(self, mock_client):
        """skill enable posts to the enable endpoint."""
        mock_client.post.return_value = {"enabled": True, "skill_name": "x"}
        result = runner.invoke(admin_app, ["skill", "enable", "x"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/enable", json=None)

    def test_skill_disable(self, mock_client):
        """skill disable posts to the disable endpoint."""
        mock_client.post.return_value = {"disabled": True}
        result = runner.invoke(admin_app, ["skill", "disable", "x"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/disable", json=None)

    def test_skill_reload(self, mock_client):
        """skill reload posts to the reload endpoint."""
        mock_client.post.return_value = {"name": "x", "reloaded": True}
        result = runner.invoke(admin_app, ["skill", "reload", "x"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/reload", json=None)

    def test_skill_import(self, mock_client):
        """skill import reads the YAML file and posts its content."""
        mock_client.post.return_value = {"name": "imported_skill"}
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("name: imported_skill\ndescription: test\n")
            f.flush()
            path = f.name
        try:
            result = runner.invoke(admin_app, ["skill", "import", path])
            assert result.exit_code == 0
            body = mock_client.post.call_args.kwargs["json"]
            assert "imported_skill" in body["yaml_content"]
        finally:
            os.unlink(path)

    def test_skill_import_missing_file(self, mock_client):
        """skill import with a missing file exits non-zero."""
        result = runner.invoke(admin_app, ["skill", "import", "/nonexistent.yaml"])
        assert result.exit_code != 0
        assert "not found" in result.stdout.lower()


# ---------------------------------------------------------------------------
# KB commands
# ---------------------------------------------------------------------------


class TestKbCommands:
    def test_kb_list_documents(self, mock_client):
        """kb list-documents returns the documents table."""
        mock_client.get.return_value = {
            "documents": [
                {
                    "id": "doc-1",
                    "filename": "spec.md",
                    "source_id": "src-1",
                    "department_id": "dept-1",
                    "size": 1024,
                    "created_at": "2026-01-01",
                }
            ]
        }
        result = runner.invoke(admin_app, ["kb", "list-documents"])
        assert result.exit_code == 0
        assert "spec.md" in result.stdout

    def test_kb_upload(self, mock_client):
        """kb upload reads the file and posts the content."""
        mock_client.post.return_value = {"id": "doc-2", "filename": "x.md"}
        with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
            f.write("# Title\nbody")
            f.flush()
            path = f.name
        try:
            result = runner.invoke(
                admin_app,
                [
                    "kb",
                    "upload",
                    "--filename",
                    "x.md",
                    "--content-file",
                    path,
                    "--source-id",
                    "src-1",
                ],
            )
            assert result.exit_code == 0
            body = mock_client.post.call_args.kwargs["json"]
            assert body["filename"] == "x.md"
            assert "# Title" in body["content"]
        finally:
            os.unlink(path)

    def test_kb_delete(self, mock_client):
        """kb delete deletes the document."""
        mock_client.delete.return_value = {"deleted": True}
        result = runner.invoke(admin_app, ["kb", "delete", "doc-1"])
        assert result.exit_code == 0
        mock_client.delete.assert_called_once_with("/api/v1/admin/kb/documents/doc-1", params=None)

    def test_kb_sync(self, mock_client):
        """kb sync triggers a source sync."""
        mock_client.post.return_value = {"synced": True}
        result = runner.invoke(admin_app, ["kb", "sync", "src-1"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with("/api/v1/admin/kb/sources/src-1/sync", json=None)

    def test_kb_rebuild(self, mock_client):
        """kb rebuild triggers a source rebuild."""
        mock_client.post.return_value = {"rebuilt": True}
        result = runner.invoke(admin_app, ["kb", "rebuild", "src-1"])
        assert result.exit_code == 0
        mock_client.post.assert_called_once_with(
            "/api/v1/admin/kb/sources/src-1/rebuild", json=None
        )


# ---------------------------------------------------------------------------
# Usage commands
# ---------------------------------------------------------------------------


class TestUsageCommands:
    def test_usage_summary(self, mock_client):
        """usage summary returns aggregated metrics."""
        mock_client.get.return_value = {
            "total_requests": 100,
            "total_tokens": 50000,
            "total_cost": 1.23,
        }
        result = runner.invoke(admin_app, ["usage", "summary"])
        assert result.exit_code == 0
        assert "total_requests" in result.stdout
        assert "100" in result.stdout

    def test_usage_summary_json(self, mock_client):
        """usage summary --json outputs raw JSON."""
        mock_client.get.return_value = {"total_requests": 5}
        result = runner.invoke(admin_app, ["usage", "summary", "--json"])
        assert result.exit_code == 0
        assert "total_requests" in result.stdout
        assert "5" in result.stdout

    def test_usage_timeseries(self, mock_client):
        """usage timeseries returns bucketed rows."""
        mock_client.get.return_value = [
            {"bucket": "2026-01-01", "requests": 10, "tokens": 1000, "cost": 0.1}
        ]
        result = runner.invoke(admin_app, ["usage", "timeseries", "--interval", "day"])
        assert result.exit_code == 0
        assert "2026-01-01" in result.stdout
        params = mock_client.get.call_args.kwargs["params"]
        assert params["interval"] == "day"

    def test_usage_by_model(self, mock_client):
        """usage by-model returns per-model rows."""
        mock_client.get.return_value = [
            {"model": "gpt-4", "requests": 50, "tokens": 20000, "cost": 0.8}
        ]
        result = runner.invoke(admin_app, ["usage", "by-model"])
        assert result.exit_code == 0
        assert "gpt-4" in result.stdout

    def test_usage_top_users(self, mock_client):
        """usage top-users returns ranked rows."""
        mock_client.get.return_value = [
            {
                "user_id": "user-1",
                "requests": 100,
                "tokens": 50000,
                "cost": 1.5,
            }
        ]
        result = runner.invoke(admin_app, ["usage", "top-users", "--limit", "5"])
        assert result.exit_code == 0
        assert "user-1" in result.stdout
        params = mock_client.get.call_args.kwargs["params"]
        assert params["limit"] == 5

    def test_usage_export_stdout(self, mock_client):
        """usage export prints CSV to stdout."""
        mock_client.get_text.return_value = "user_id,tokens\nalice,1000\n"
        result = runner.invoke(admin_app, ["usage", "export", "--format", "csv"])
        assert result.exit_code == 0
        assert "user_id" in result.stdout

    def test_usage_export_to_file(self, mock_client):
        """usage export --output writes to a file."""
        mock_client.get_text.return_value = "user_id,tokens\nalice,1000\n"
        with tempfile.NamedTemporaryFile(mode="r", suffix=".csv", delete=False) as f:
            path = f.name
        try:
            result = runner.invoke(admin_app, ["usage", "export", "--output", path])
            assert result.exit_code == 0
            with open(path) as f:
                content = f.read()
            assert "user_id" in content
        finally:
            os.unlink(path)


# ---------------------------------------------------------------------------
# Login command
# ---------------------------------------------------------------------------


class TestLoginCommand:
    def test_login_success(self):
        """admin login saves the token to the config file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            config_path = os.path.join(tmpdir, "admin_config.yaml")
            with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls:
                mock_instance = MagicMock()
                mock_instance.login.return_value = "jwt-token-123"
                mock_cls.return_value = mock_instance
                result = runner.invoke(
                    admin_app,
                    [
                        "login",
                        "--username",
                        "admin",
                        "--password",
                        "secret",
                        "--config-path",
                        config_path,
                    ],
                )
                assert result.exit_code == 0
                assert "Login successful" in result.stdout
                import yaml

                with open(config_path) as f:
                    cfg = yaml.safe_load(f)
                assert cfg["token"] == "jwt-token-123"
                assert cfg["server_url"] == "http://localhost:18001"

    def test_login_with_server_url(self):
        """admin login --server-url saves the custom URL."""
        with tempfile.TemporaryDirectory() as tmpdir:
            config_path = os.path.join(tmpdir, "admin_config.yaml")
            with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls:
                mock_instance = MagicMock()
                mock_instance.login.return_value = "tok"
                mock_cls.return_value = mock_instance
                result = runner.invoke(
                    admin_app,
                    [
                        "login",
                        "--username",
                        "admin",
                        "--password",
                        "secret",
                        "--server-url",
                        "http://my-server:9000",
                        "--config-path",
                        config_path,
                    ],
                )
                assert result.exit_code == 0
                import yaml

                with open(config_path) as f:
                    cfg = yaml.safe_load(f)
                assert cfg["server_url"] == "http://my-server:9000"

    def test_login_failure_auth_error(self):
        """admin login with bad credentials exits non-zero."""
        with tempfile.TemporaryDirectory() as tmpdir:
            config_path = os.path.join(tmpdir, "admin_config.yaml")
            with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls:
                mock_instance = MagicMock()
                # Simulate a 401 from the server.
                response = MagicMock(spec=httpx.Response)
                response.status_code = 401
                response.text = "Unauthorized"
                response.json.return_value = {"detail": "Invalid credentials"}
                mock_instance.login.side_effect = httpx.HTTPStatusError(
                    "Unauthorized", request=MagicMock(), response=response
                )
                mock_cls.return_value = mock_instance
                result = runner.invoke(
                    admin_app,
                    [
                        "login",
                        "--username",
                        "admin",
                        "--password",
                        "wrong",
                        "--config-path",
                        config_path,
                    ],
                )
                assert result.exit_code != 0
                assert "Authentication failed" in result.stdout


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


class TestErrorHandling:
    def test_connection_error(self, mock_client):
        """Connection errors produce a friendly message and exit 1."""
        mock_client.get.side_effect = httpx.ConnectError("connection refused")
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code != 0
        assert "Cannot connect" in result.stdout

    def test_auth_error_401(self, mock_client):
        """401 errors prompt the user to run 'admin login'."""
        response = MagicMock(spec=httpx.Response)
        response.status_code = 401
        response.text = "Unauthorized"
        response.json.return_value = {"detail": "Unauthorized"}
        mock_client.get.side_effect = httpx.HTTPStatusError(
            "Unauthorized", request=MagicMock(), response=response
        )
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code != 0
        assert "Authentication failed" in result.stdout

    def test_forbidden_error_403(self, mock_client):
        """403 errors explain admin permission is required."""
        response = MagicMock(spec=httpx.Response)
        response.status_code = 403
        response.text = "Forbidden"
        response.json.return_value = {"detail": "Forbidden"}
        mock_client.get.side_effect = httpx.HTTPStatusError(
            "Forbidden", request=MagicMock(), response=response
        )
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code != 0
        assert "Admin permission required" in result.stdout

    def test_not_found_error_404(self, mock_client):
        """404 errors print a not-found message."""
        response = MagicMock(spec=httpx.Response)
        response.status_code = 404
        response.text = "Not Found"
        response.json.return_value = {"detail": "Not found"}
        mock_client.get.side_effect = httpx.HTTPStatusError(
            "Not Found", request=MagicMock(), response=response
        )
        result = runner.invoke(admin_app, ["department", "list"])
        assert result.exit_code != 0
        assert "not found" in result.stdout.lower()

    def test_conflict_error_409(self, mock_client):
        """409 errors print the conflict detail."""
        response = MagicMock(spec=httpx.Response)
        response.status_code = 409
        response.text = "Conflict"
        response.json.return_value = {"detail": "Name already exists"}
        mock_client.post.side_effect = httpx.HTTPStatusError(
            "Conflict", request=MagicMock(), response=response
        )
        result = runner.invoke(
            admin_app,
            ["department", "create", "--name", "dup"],
        )
        assert result.exit_code != 0
        assert "Conflict" in result.stdout
        assert "Name already exists" in result.stdout


# ---------------------------------------------------------------------------
# Help / registration smoke tests
# ---------------------------------------------------------------------------


class TestAdminAppRegistration:
    def test_admin_help_shows_groups(self):
        """agentkit admin --help lists all sub-groups."""
        result = runner.invoke(admin_app, ["--help"])
        assert result.exit_code == 0
        for group in ("department", "user", "llm", "skill", "kb", "usage", "login"):
            assert group in result.stdout

    def test_admin_department_help(self):
        """agentkit admin department --help lists department commands."""
        result = runner.invoke(admin_app, ["department", "--help"])
        assert result.exit_code == 0
        for cmd in ("list", "create", "update", "delete", "bind-skill", "unbind-skill"):
            assert cmd in result.stdout

    def test_admin_registered_on_main_app(self):
        """The admin sub-app is registered on the main agentkit app."""
        from agentkit.cli.main import app as main_app

        result = runner.invoke(main_app, ["--help"])
        assert result.exit_code == 0
        assert "admin" in result.stdout
