"""Integration tests for the skill/KB admin routes (U6).

Uses FastAPI TestClient with a test app that mounts the
``admin_router`` from ``routes.admin`` plus the public ``skills``
router from ``routes.skills`` (to verify disabled-skill filtering).
The ``_require_admin`` dependency is overridden via
``app.dependency_overrides`` so the tests don't need real JWTs.

The :class:`SkillRegistry` is a real instance with a test skill
loaded from a temp YAML file, so import/reload/update endpoints
exercise the real SkillLoader pipeline.
"""

from __future__ import annotations

import os
import uuid
from pathlib import Path
from typing import Any

import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

from agentkit.server.admin.kb_service import set_kb_service
from agentkit.server.admin.skill_service import set_skill_service
from agentkit.server.auth.models import init_auth_db
from agentkit.server.routes import admin as admin_routes_module
from agentkit.server.routes import skills as skills_routes_module
from agentkit.skills.registry import SkillRegistry


# ---------------------------------------------------------------------------
# Test data
# ---------------------------------------------------------------------------


_VALID_SKILL_YAML = """\
name: admin_test_skill
agent_type: simple_generation
version: "1.0.0"
description: "A test skill for admin route testing"
task_mode: llm_generate
execution_mode: direct
max_steps: 1
prompt:
  identity: "Test"
  instructions: "Handle test"
tools: []
"""

_VALID_SKILL_YAML_2 = """\
name: another_test_skill
agent_type: simple_generation
version: "1.0.0"
description: "Another test skill"
task_mode: llm_generate
execution_mode: direct
max_steps: 1
prompt:
  identity: "Test2"
  instructions: "Handle test 2"
tools: []
"""


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


@pytest.fixture
async def tmp_auth_db(tmp_path: Path) -> Path:
    db_path = tmp_path / "admin_skill_kb.db"
    await init_auth_db(db_path)
    return db_path


@pytest.fixture
def skills_dir(tmp_path: Path) -> str:
    """A temp skills directory for YAML files."""
    d = tmp_path / "skills"
    d.mkdir()
    return str(d)


@pytest.fixture
def skill_registry() -> SkillRegistry:
    return SkillRegistry()


@pytest.fixture(autouse=True)
def _reset_singletons():
    """Reset SkillService and KbService singletons before/after each test."""
    set_skill_service(None)
    set_kb_service(None)
    yield
    set_skill_service(None)
    set_kb_service(None)


def _make_admin_user() -> dict[str, Any]:
    return {"user_id": "admin-1", "username": "admin", "role": "admin"}


def _raise_forbidden() -> dict[str, Any]:
    """Dependency override that simulates a non-admin (403) response."""
    raise HTTPException(status_code=403, detail="Admin permission required")


@pytest.fixture
def admin_app(
    tmp_auth_db: Path,
    skills_dir: str,
    skill_registry: SkillRegistry,
) -> FastAPI:
    """A minimal FastAPI app with admin + skills routers mounted.

    The ``_require_admin`` dependency is overridden to return a fake
    admin user. The :class:`SkillRegistry` is set on ``app.state`` so
    the admin skill endpoints can access it.
    """
    app = FastAPI()
    app.state.auth_db_path = str(tmp_auth_db)
    app.state.skill_registry = skill_registry

    # Build a minimal server_config with skill_paths pointing at the
    # temp skills_dir, so the admin endpoints write YAML there.
    class _FakeServerConfig:
        skill_paths = [skills_dir]

    app.state.server_config = _FakeServerConfig()

    app.include_router(admin_routes_module.admin_router, prefix="/api/v1")
    app.include_router(skills_routes_module.router, prefix="/api/v1")

    # Default: allow admin access.
    app.dependency_overrides[admin_routes_module._require_admin] = lambda: _make_admin_user()
    # Override the department context to admin (bypass filtering) so
    # GET /skills returns all skills (the disabled filter still applies).
    from agentkit.server.admin.context import DepartmentContext

    app.dependency_overrides[skills_routes_module.get_department_context] = lambda: (
        DepartmentContext(user_id="admin-1", department_ids=[], is_admin=True)
    )
    return app


@pytest.fixture
def admin_client(admin_app: FastAPI) -> TestClient:
    return TestClient(admin_app)


def _write_skill_yaml(skills_dir: str, name: str, content: str) -> str:
    """Write a skill YAML file to the skills dir and return the path."""
    path = os.path.join(skills_dir, f"{name}.yaml")
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    return path


# ---------------------------------------------------------------------------
# Skill enable/disable
# ---------------------------------------------------------------------------


class TestSkillEnableDisable:
    def test_disable_skill_returns_200(self, admin_client: TestClient):
        resp = admin_client.post("/api/v1/admin/skills/admin_test_skill/disable")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["skill_name"] == "admin_test_skill"
        assert body["is_disabled"] is True

    def test_disable_then_get_skills_excludes_it(
        self,
        admin_client: TestClient,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # Write a skill YAML and register it.
        _write_skill_yaml(skills_dir, "admin_test_skill", _VALID_SKILL_YAML)
        from agentkit.skills.loader import SkillLoader

        SkillLoader(skill_registry=skill_registry).load_from_file(
            os.path.join(skills_dir, "admin_test_skill.yaml")
        )

        # Verify the skill is initially listed.
        resp = admin_client.get("/api/v1/skills")
        assert resp.status_code == 200
        names = [s["name"] for s in resp.json()]
        assert "admin_test_skill" in names

        # Disable the skill.
        resp = admin_client.post("/api/v1/admin/skills/admin_test_skill/disable")
        assert resp.status_code == 200

        # GET /skills should now exclude it.
        resp = admin_client.get("/api/v1/skills")
        assert resp.status_code == 200
        names = [s["name"] for s in resp.json()]
        assert "admin_test_skill" not in names

    def test_enable_skill_returns_200(
        self,
        admin_client: TestClient,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # Write and register the skill, then disable it.
        _write_skill_yaml(skills_dir, "admin_test_skill", _VALID_SKILL_YAML)
        from agentkit.skills.loader import SkillLoader

        SkillLoader(skill_registry=skill_registry).load_from_file(
            os.path.join(skills_dir, "admin_test_skill.yaml")
        )
        admin_client.post("/api/v1/admin/skills/admin_test_skill/disable")

        # Enable it.
        resp = admin_client.post("/api/v1/admin/skills/admin_test_skill/enable")
        assert resp.status_code == 200
        assert resp.json()["enabled"] is True

        # GET /skills should now include it.
        resp = admin_client.get("/api/v1/skills")
        names = [s["name"] for s in resp.json()]
        assert "admin_test_skill" in names

    def test_enable_skill_not_disabled_returns_200(self, admin_client: TestClient):
        """Enabling a skill that wasn't disabled returns enabled=False."""
        resp = admin_client.post("/api/v1/admin/skills/never_disabled/enable")
        assert resp.status_code == 200
        assert resp.json()["enabled"] is False

    def test_disable_skill_invalid_name_returns_400(self, admin_client: TestClient):
        resp = admin_client.post("/api/v1/admin/skills/Has Spaces/disable")
        assert resp.status_code == 400

    def test_non_admin_cannot_disable_skill(self, admin_app: FastAPI):
        admin_app.dependency_overrides[admin_routes_module._require_admin] = _raise_forbidden
        client = TestClient(admin_app)
        resp = client.post("/api/v1/admin/skills/some_skill/disable")
        assert resp.status_code == 403


# ---------------------------------------------------------------------------
# Skill import
# ---------------------------------------------------------------------------


class TestSkillImport:
    def test_import_valid_yaml_returns_201(
        self,
        admin_client: TestClient,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        resp = admin_client.post(
            "/api/v1/admin/skills/import",
            json={"yaml_content": _VALID_SKILL_YAML},
        )
        assert resp.status_code == 201, resp.text
        body = resp.json()
        assert body["name"] == "admin_test_skill"
        assert os.path.isfile(body["path"])
        # Skill should be registered.
        assert skill_registry.has_skill("admin_test_skill")

    def test_import_invalid_yaml_returns_400(self, admin_client: TestClient):
        resp = admin_client.post(
            "/api/v1/admin/skills/import",
            json={"yaml_content": "not: valid: yaml: ["},
        )
        assert resp.status_code == 400

    def test_import_missing_name_returns_400(self, admin_client: TestClient):
        resp = admin_client.post(
            "/api/v1/admin/skills/import",
            json={"yaml_content": "agent_type: test\n"},
        )
        assert resp.status_code == 400

    def test_import_invalid_skill_name_returns_400(self, admin_client: TestClient):
        bad_yaml = 'name: "Has Spaces"\nagent_type: test\n'
        resp = admin_client.post(
            "/api/v1/admin/skills/import",
            json={"yaml_content": bad_yaml},
        )
        assert resp.status_code == 400

    def test_non_admin_cannot_import(self, admin_app: FastAPI):
        admin_app.dependency_overrides[admin_routes_module._require_admin] = _raise_forbidden
        client = TestClient(admin_app)
        resp = client.post(
            "/api/v1/admin/skills/import",
            json={"yaml_content": _VALID_SKILL_YAML},
        )
        assert resp.status_code == 403


# ---------------------------------------------------------------------------
# Skill reload
# ---------------------------------------------------------------------------


class TestSkillReload:
    def test_reload_existing_skill_returns_200(
        self,
        admin_client: TestClient,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # Write and register the skill first.
        _write_skill_yaml(skills_dir, "admin_test_skill", _VALID_SKILL_YAML)
        from agentkit.skills.loader import SkillLoader

        SkillLoader(skill_registry=skill_registry).load_from_file(
            os.path.join(skills_dir, "admin_test_skill.yaml")
        )

        resp = admin_client.post("/api/v1/admin/skills/admin_test_skill/reload")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["name"] == "admin_test_skill"
        assert body["status"] == "reloaded"

    def test_reload_nonexistent_skill_returns_404(self, admin_client: TestClient):
        resp = admin_client.post("/api/v1/admin/skills/nonexistent/reload")
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Skill update (PATCH)
# ---------------------------------------------------------------------------


class TestSkillUpdate:
    def test_update_skill_returns_200(
        self,
        admin_client: TestClient,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # Write and register the skill first.
        _write_skill_yaml(skills_dir, "admin_test_skill", _VALID_SKILL_YAML)
        from agentkit.skills.loader import SkillLoader

        SkillLoader(skill_registry=skill_registry).load_from_file(
            os.path.join(skills_dir, "admin_test_skill.yaml")
        )

        resp = admin_client.patch(
            "/api/v1/admin/skills/admin_test_skill",
            json={"config": {"description": "Updated via admin API"}},
        )
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["status"] == "updated"

        # Verify the YAML file was updated.
        import yaml

        with open(body["path"], encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["description"] == "Updated via admin API"

    def test_update_nonexistent_skill_returns_404(self, admin_client: TestClient):
        resp = admin_client.patch(
            "/api/v1/admin/skills/nonexistent",
            json={"config": {"description": "x"}},
        )
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# KB document endpoints
# ---------------------------------------------------------------------------


class TestKbDocumentRoutes:
    def test_list_documents_returns_200(self, admin_client: TestClient):
        resp = admin_client.get("/api/v1/admin/kb/documents")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert "documents" in body
        assert isinstance(body["documents"], list)

    def test_upload_document_returns_201_with_department_id(self, admin_client: TestClient):
        dept_id = str(uuid.uuid4())
        resp = admin_client.post(
            "/api/v1/admin/kb/documents",
            json={
                "filename": "test.txt",
                "content": "hello world",
                "department_id": dept_id,
            },
        )
        assert resp.status_code == 201, resp.text
        body = resp.json()
        assert body["filename"] == "test.txt"
        assert body["department_id"] == dept_id
        assert "document_id" in body

    def test_upload_document_without_department_id(self, admin_client: TestClient):
        resp = admin_client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "global.txt", "content": "global content"},
        )
        assert resp.status_code == 201
        body = resp.json()
        assert body["department_id"] is None

    def test_upload_then_delete_document(self, admin_client: TestClient):
        # Upload
        resp = admin_client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "delete_me.txt", "content": "x"},
        )
        assert resp.status_code == 201
        doc_id = resp.json()["document_id"]

        # Delete
        resp = admin_client.delete(f"/api/v1/admin/kb/documents/{doc_id}")
        assert resp.status_code == 200
        assert resp.json() == {"deleted": True}

        # Second delete should 404.
        resp = admin_client.delete(f"/api/v1/admin/kb/documents/{doc_id}")
        assert resp.status_code == 404

    def test_delete_nonexistent_document_returns_404(self, admin_client: TestClient):
        resp = admin_client.delete("/api/v1/admin/kb/documents/nonexistent-id")
        assert resp.status_code == 404

    def test_list_documents_filters_by_department_id(self, admin_client: TestClient):
        dept_a = str(uuid.uuid4())
        dept_b = str(uuid.uuid4())

        # Upload 3 docs: one for dept_a, one for dept_b, one global.
        admin_client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "a.txt", "content": "a", "department_id": dept_a},
        )
        admin_client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "b.txt", "content": "b", "department_id": dept_b},
        )
        admin_client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "global.txt", "content": "g"},
        )

        # Filter by dept_a: should see dept_a's doc + global doc.
        resp = admin_client.get("/api/v1/admin/kb/documents", params={"department_id": dept_a})
        assert resp.status_code == 200
        docs = resp.json()["documents"]
        dept_ids = {d["department_id"] for d in docs}
        assert dept_a in dept_ids
        assert None in dept_ids  # global doc
        assert dept_b not in dept_ids

    def test_non_admin_cannot_list_documents(self, admin_app: FastAPI):
        admin_app.dependency_overrides[admin_routes_module._require_admin] = _raise_forbidden
        client = TestClient(admin_app)
        resp = client.get("/api/v1/admin/kb/documents")
        assert resp.status_code == 403

    def test_non_admin_cannot_upload_document(self, admin_app: FastAPI):
        admin_app.dependency_overrides[admin_routes_module._require_admin] = _raise_forbidden
        client = TestClient(admin_app)
        resp = client.post(
            "/api/v1/admin/kb/documents",
            json={"filename": "x.txt", "content": "x"},
        )
        assert resp.status_code == 403


# ---------------------------------------------------------------------------
# KB source sync/rebuild
# ---------------------------------------------------------------------------


class TestKbSourceRoutes:
    def test_sync_nonexistent_source_returns_404(self, admin_client: TestClient):
        resp = admin_client.post("/api/v1/admin/kb/sources/nonexistent/sync")
        assert resp.status_code == 404

    def test_rebuild_nonexistent_source_returns_404(self, admin_client: TestClient):
        resp = admin_client.post("/api/v1/admin/kb/sources/nonexistent/rebuild")
        assert resp.status_code == 404

    def test_sync_existing_source_returns_200(self, admin_client: TestClient):
        # Add a source via the KbService (bypassing the route layer).
        from agentkit.server.admin.kb_service import get_kb_service

        svc = get_kb_service()
        store = svc._resolve_store()
        source = store.add_source("Test Source", "local", {})

        resp = admin_client.post(f"/api/v1/admin/kb/sources/{source.id}/sync")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["status"] == "syncing"

    def test_rebuild_existing_source_returns_200(self, admin_client: TestClient):
        from agentkit.server.admin.kb_service import get_kb_service

        svc = get_kb_service()
        store = svc._resolve_store()
        source = store.add_source("Test Source", "local", {})

        resp = admin_client.post(f"/api/v1/admin/kb/sources/{source.id}/rebuild")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["status"] == "rebuilding"

    def test_non_admin_cannot_sync_source(self, admin_app: FastAPI):
        admin_app.dependency_overrides[admin_routes_module._require_admin] = _raise_forbidden
        client = TestClient(admin_app)
        resp = client.post("/api/v1/admin/kb/sources/any/sync")
        assert resp.status_code == 403
