"""Unit tests for workspace configuration API router endpoints.

Tests CRUD operations for Skills, MCPs, Knowledgebases, Context management,
audit log retrieval, and privileged capability confirmation for the
/api/workspaces/{id}/... config endpoints.

Updated for the single-workspace (singleton) model: there is only one
workspace (``swarmws``), so the old multi-workspace intersection model
helpers have been replaced with direct singleton helpers.

Key helpers:

- ``_ensure_default_workspace``  — Idempotent singleton workspace creation
- ``_seed_skill`` / ``_seed_mcp`` — Insert global skill/MCP rows
- ``_set_workspace_skill`` / ``_set_workspace_mcp`` — Upsert junction rows

Requirements: 19.6-19.9
"""
import json
import tempfile
from pathlib import Path
from uuid import uuid4

import pytest
from fastapi.testclient import TestClient

from database import db
from tests.helpers import now_iso


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


@pytest.fixture
def workspace_id(client: TestClient) -> str:
    """Return the singleton workspace ID after seeding workspace_config."""
    import asyncio
    return asyncio.run(_ensure_default_workspace())


@pytest.fixture
def second_workspace_id(client: TestClient) -> str:
    """Return the singleton workspace ID (same as workspace_id in single-workspace model)."""
    import asyncio
    return asyncio.run(_ensure_default_workspace())


async def _ensure_default_workspace() -> str:
    """Ensure a default SwarmWS workspace config exists and return its ID."""
    existing = await db.workspace_config.get_config()
    if existing:
        return existing["id"]
    now = now_iso()
    await db.workspace_config.put({
        "id": "swarmws",
        "name": "SwarmWS",
        "file_path": f"/tmp/test-swarmws-{uuid4().hex[:8]}",
        "icon": "🏠",
        "context": "Default workspace",
        "created_at": now,
        "updated_at": now,
    })
    return "swarmws"


_SEEDED_SKILL_PATHS: list[Path] = []
"""Track skill dirs created by _seed_skill so the fixture can clean up."""


async def _seed_skill(name: str = "TestSkill", is_privileged: bool = False) -> str:
    """Create a filesystem-based skill and return its folder name.

    Tracks created paths for cleanup by ``_cleanup_seeded_skills``.
    Name is lowercased to match SDK command-matching convention.
    """
    from pathlib import Path

    # Create folder name from skill name (kebab-case, lowercase)
    folder_name = name.lower().replace(" ", "-")

    # Determine skill directory based on privilege level
    from config import get_app_data_dir
    if is_privileged:
        skills_dir = get_app_data_dir() / "built-in-skills"
    else:
        skills_dir = get_app_data_dir() / "skills"

    skill_path = skills_dir / folder_name
    skill_path.mkdir(parents=True, exist_ok=True)

    # SKILL.md — name must be lowercase for SDK matching
    skill_md_content = f"""---
name: {folder_name}
description: Test skill: {folder_name}
version: 1.0.0
---

# {folder_name}

A test skill for workspace config tests.
"""

    skill_md = skill_path / "SKILL.md"
    skill_md.write_text(skill_md_content)
    _SEEDED_SKILL_PATHS.append(skill_path)

    return folder_name


@pytest.fixture(autouse=True)
def _cleanup_seeded_skills():
    """Remove filesystem skills created during test — prevent leaking into runtime."""
    _SEEDED_SKILL_PATHS.clear()
    yield
    import shutil
    for p in _SEEDED_SKILL_PATHS:
        if p.exists():
            shutil.rmtree(p, ignore_errors=True)
    _SEEDED_SKILL_PATHS.clear()


async def _seed_mcp(name: str = "TestMCP", is_privileged: bool = False) -> str:
    """Insert an MCP server directly into the DB and return its ID."""
    now = now_iso()
    mcp_id = str(uuid4())
    await db.mcp_servers.put({
        "id": mcp_id,
        "name": name,
        "description": f"Test MCP: {name}",
        "connection_type": "stdio",
        "config": json.dumps({"command": "echo", "args": ["test"]}),
        "allowed_tools": "[]",
        "rejected_tools": "[]",
        "is_active": 1,
        "is_system": 0,
        "is_privileged": 1 if is_privileged else 0,
        "created_at": now,
        "updated_at": now,
    })
    return mcp_id


async def _set_workspace_skill(workspace_id: str, skill_id: str, enabled: bool = True) -> None:
    """No-op — workspace_skills DB table was removed.

    In the filesystem-based skill model, all skills are always enabled.
    This helper is kept for backward-compatible test signatures but does nothing.
    """
    pass


async def _set_workspace_mcp(workspace_id: str, mcp_id: str, enabled: bool = True) -> None:
    """Create or update a workspace_mcps junction row (upsert-safe)."""
    now = now_iso()
    existing = await db.workspace_mcps.get_by_workspace_and_mcp(workspace_id, mcp_id)
    if existing:
        await db.workspace_mcps.update(existing["id"], {
            "enabled": 1 if enabled else 0,
            "updated_at": now,
        })
    else:
        await db.workspace_mcps.put({
            "id": str(uuid4()),
            "workspace_id": workspace_id,
            "mcp_server_id": mcp_id,
            "enabled": 1 if enabled else 0,
            "created_at": now,
            "updated_at": now,
        })


# ============================================================================
# Skills Endpoints
# Validates: Requirement 19.6
# ============================================================================


class TestGetSkills:
    """Tests for GET /api/workspaces/{id}/skills.

    In the filesystem-based model, all skills are always enabled.
    The endpoint returns every skill from the filesystem cache.
    """

    def test_get_skills_returns_list(self, client: TestClient, workspace_id: str):
        """Skills endpoint returns 200 with a list."""
        resp = client.get(f"/api/workspaces/{workspace_id}/skills")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)

    @pytest.mark.anyio
    async def test_get_skills_returns_configured(self, client: TestClient, workspace_id: str):
        """Skills from the filesystem cache are returned."""
        from core.skill_manager import skill_manager
        skill_manager.invalidate_cache()

        resp = client.get(f"/api/workspaces/{workspace_id}/skills")
        assert resp.status_code == 200
        data = resp.json()
        # All returned skills have the expected shape
        for skill in data:
            assert "skill_id" in skill
            assert "skill_name" in skill
            assert "enabled" in skill
            assert skill["enabled"] is True  # All skills always enabled

    @pytest.mark.anyio
    async def test_get_skills_includes_privileged_flag(self, client: TestClient, workspace_id: str):
        """Skills include is_privileged flag in response."""
        resp = client.get(f"/api/workspaces/{workspace_id}/skills")
        assert resp.status_code == 200
        for skill in resp.json():
            assert "is_privileged" in skill
            assert isinstance(skill["is_privileged"], bool)

    @pytest.mark.anyio
    async def test_get_skills_all_enabled(self, client: TestClient, workspace_id: str):
        """All skills are enabled in the filesystem model (no disable capability)."""
        resp = client.get(f"/api/workspaces/{workspace_id}/skills")
        assert resp.status_code == 200
        for skill in resp.json():
            assert skill["enabled"] is True


class TestUpdateSkills:
    """Tests for PUT /api/workspaces/{id}/skills.

    In the filesystem model, PUT is a no-op for runtime state (all skills
    remain enabled).  Only disable requests create audit trail entries
    (enable is already the ground truth, so logging it would be noise).
    """

    @pytest.mark.anyio
    async def test_update_skill_returns_200(self, client: TestClient, workspace_id: str):
        """PUT skills returns 200 with the effective skill list."""
        resp = client.put(f"/api/workspaces/{workspace_id}/skills", json={
            "configs": [{
                "skill_id": "test-skill",
                "skill_name": "TestSkill",
                "enabled": True,
                "is_privileged": False,
            }]
        })
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)

    @pytest.mark.anyio
    async def test_update_skill_disable_still_returns_all(self, client: TestClient, workspace_id: str):
        """Disabling a skill via PUT still returns all skills (filesystem model)."""
        resp = client.put(f"/api/workspaces/{workspace_id}/skills", json={
            "configs": [{
                "skill_id": "test-skill",
                "skill_name": "TestSkill",
                "enabled": False,
                "is_privileged": False,
            }]
        })
        assert resp.status_code == 200
        # All filesystem skills are always in the effective set
        for skill in resp.json():
            assert skill["enabled"] is True

    @pytest.mark.anyio
    async def test_update_skill_creates_audit_entry(self, client: TestClient, workspace_id: str):
        """Updating a skill config creates an audit log entry."""
        client.put(f"/api/workspaces/{workspace_id}/skills", json={
            "configs": [{
                "skill_id": "audit-test-skill",
                "skill_name": "AuditSkill",
                "enabled": False,
                "is_privileged": False,
            }]
        })

        audit_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert audit_resp.status_code == 200
        entries = audit_resp.json()["entries"]
        skill_entries = [e for e in entries if e["entity_id"] == "audit-test-skill"]
        assert len(skill_entries) >= 1
        assert skill_entries[0]["entity_type"] == "skill"
        assert skill_entries[0]["change_type"] == "disabled"


# ============================================================================
# MCPs Endpoints
# Validates: Requirement 19.7
# ============================================================================


class TestGetMcps:
    """Tests for GET /api/workspaces/{id}/mcps."""

    def test_get_mcps_empty(self, client: TestClient, workspace_id: str):
        resp = client.get(f"/api/workspaces/{workspace_id}/mcps")
        assert resp.status_code == 200
        assert resp.json() == []

    @pytest.mark.anyio
    async def test_get_mcps_returns_configured(self, client: TestClient, workspace_id: str):
        """MCPs enabled in the singleton workspace are returned."""
        mcp_id = await _seed_mcp("MyMCP")
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=True)

        resp = client.get(f"/api/workspaces/{workspace_id}/mcps")
        assert resp.status_code == 200
        mcp_ids = [m["mcp_server_id"] for m in resp.json()]
        assert mcp_id in mcp_ids

    @pytest.mark.anyio
    async def test_get_mcps_includes_privileged_flag(self, client: TestClient, workspace_id: str):
        """Privileged MCPs have is_privileged=True in response."""
        mcp_id = await _seed_mcp("PrivMCP", is_privileged=True)
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=True)

        resp = client.get(f"/api/workspaces/{workspace_id}/mcps")
        assert resp.status_code == 200
        priv_mcps = [m for m in resp.json() if m["mcp_server_id"] == mcp_id]
        assert len(priv_mcps) == 1
        assert priv_mcps[0]["is_privileged"] is True

    @pytest.mark.anyio
    async def test_get_mcps_disabled_not_in_effective_set(self, client: TestClient, workspace_id: str):
        """MCP disabled in the workspace is NOT in the effective set."""
        mcp_id = await _seed_mcp("DisabledMCP")
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=False)

        resp = client.get(f"/api/workspaces/{workspace_id}/mcps")
        assert resp.status_code == 200
        mcp_ids = [m["mcp_server_id"] for m in resp.json()]
        assert mcp_id not in mcp_ids


class TestUpdateMcps:
    """Tests for PUT /api/workspaces/{id}/mcps."""

    @pytest.mark.anyio
    async def test_update_mcp_enable(self, client: TestClient, workspace_id: str):
        """Enable an MCP via PUT."""
        mcp_id = await _seed_mcp("EnableMCP")
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=False)

        resp = client.put(f"/api/workspaces/{workspace_id}/mcps", json={
            "configs": [{
                "mcp_server_id": mcp_id,
                "mcp_server_name": "EnableMCP",
                "enabled": True,
                "is_privileged": False,
            }]
        })
        assert resp.status_code == 200
        updated = [m for m in resp.json() if m["mcp_server_id"] == mcp_id]
        assert len(updated) == 1
        assert updated[0]["enabled"] is True

    @pytest.mark.anyio
    async def test_update_mcp_disable(self, client: TestClient, workspace_id: str):
        """Disable an MCP via PUT."""
        mcp_id = await _seed_mcp("DisableMCP")
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=True)

        resp = client.put(f"/api/workspaces/{workspace_id}/mcps", json={
            "configs": [{
                "mcp_server_id": mcp_id,
                "mcp_server_name": "DisableMCP",
                "enabled": False,
                "is_privileged": False,
            }]
        })
        assert resp.status_code == 200
        mcp_ids = [m["mcp_server_id"] for m in resp.json()]
        assert mcp_id not in mcp_ids

    @pytest.mark.anyio
    async def test_update_mcp_creates_audit_entry(self, client: TestClient, workspace_id: str):
        """Updating an MCP config creates an audit log entry."""
        mcp_id = await _seed_mcp("AuditMCP")
        await _set_workspace_mcp(workspace_id, mcp_id, enabled=True)

        client.put(f"/api/workspaces/{workspace_id}/mcps", json={
            "configs": [{
                "mcp_server_id": mcp_id,
                "mcp_server_name": "AuditMCP",
                "enabled": False,
                "is_privileged": False,
            }]
        })

        audit_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert audit_resp.status_code == 200
        entries = audit_resp.json()["entries"]
        mcp_entries = [e for e in entries if e["entity_id"] == mcp_id]
        assert len(mcp_entries) >= 1
        assert mcp_entries[0]["entity_type"] == "mcp"
        assert mcp_entries[0]["change_type"] == "disabled"


# ============================================================================
# Knowledgebases Endpoints (Union Model with Exclusions)
# Validates: Requirement 19.8
# ============================================================================


class TestGetKnowledgebases:
    """Tests for GET /api/workspaces/{id}/knowledgebases."""

    def test_get_knowledgebases_empty(self, client: TestClient, workspace_id: str):
        resp = client.get(f"/api/workspaces/{workspace_id}/knowledgebases")
        assert resp.status_code == 200
        assert resp.json() == []


class TestAddKnowledgebase:
    """Tests for POST /api/workspaces/{id}/knowledgebases."""

    def test_add_knowledgebase_success(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/docs/guide.md",
            "display_name": "Project Guide",
        })
        assert resp.status_code == 201
        data = resp.json()
        assert data["display_name"] == "Project Guide"
        assert data["source_type"] == "local_file"
        assert data["source_path"] == "/data/docs/guide.md"
        assert "id" in data

    def test_add_knowledgebase_with_metadata(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "url",
            "source_path": "https://example.com/docs",
            "display_name": "External Docs",
            "metadata": {"category": "reference", "priority": 1},
        })
        assert resp.status_code == 201
        assert resp.json()["metadata"]["category"] == "reference"

    def test_add_knowledgebase_with_exclusions(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "context_file",
            "source_path": "/ctx/notes.md",
            "display_name": "Notes",
            "excluded_sources": [1, 2, 3],
        })
        assert resp.status_code == 201
        assert resp.json()["excluded_sources"] == [1, 2, 3]

    def test_add_knowledgebase_all_source_types(self, client: TestClient, workspace_id: str):
        """All valid source types should be accepted."""
        for src_type in ["local_file", "url", "indexed_document", "context_file", "vector_index"]:
            resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
                "source_type": src_type,
                "source_path": f"/path/{src_type}",
                "display_name": f"KB {src_type}",
            })
            assert resp.status_code == 201, f"Failed for source_type={src_type}"

    def test_add_knowledgebase_appears_in_list(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/listed.md",
            "display_name": "Listed KB",
        })
        kb_id = resp.json()["id"]

        list_resp = client.get(f"/api/workspaces/{workspace_id}/knowledgebases")
        assert list_resp.status_code == 200
        kb_ids = [k["id"] for k in list_resp.json()]
        assert kb_id in kb_ids

    def test_add_knowledgebase_creates_audit_entry(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/audit-test.md",
            "display_name": "Audit Test KB",
        })
        assert resp.status_code == 201
        kb_id = resp.json()["id"]

        audit_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert audit_resp.status_code == 200
        entries = audit_resp.json()["entries"]
        kb_entries = [e for e in entries if e["entity_id"] == kb_id]
        assert len(kb_entries) >= 1
        assert kb_entries[0]["entity_type"] == "knowledgebase"
        assert kb_entries[0]["change_type"] == "added"


class TestUpdateKnowledgebase:
    """Tests for PUT /api/workspaces/{id}/knowledgebases/{kb_id}."""

    def _create_kb(self, client: TestClient, workspace_id: str) -> dict:
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/original.md",
            "display_name": "Original KB",
        })
        assert resp.status_code == 201
        return resp.json()

    def test_update_display_name(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        resp = client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}",
            json={"display_name": "Updated KB"},
        )
        assert resp.status_code == 200
        assert resp.json()["display_name"] == "Updated KB"

    def test_update_source_path(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        resp = client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}",
            json={"source_path": "/data/new-path.md"},
        )
        assert resp.status_code == 200
        assert resp.json()["source_path"] == "/data/new-path.md"

    def test_update_source_type(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        resp = client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}",
            json={"source_type": "url"},
        )
        assert resp.status_code == 200
        assert resp.json()["source_type"] == "url"

    def test_update_excluded_sources(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        resp = client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}",
            json={"excluded_sources": [10, 20]},
        )
        assert resp.status_code == 200
        assert resp.json()["excluded_sources"] == [10, 20]

    def test_update_not_found(self, client: TestClient, workspace_id: str):
        resp = client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/nonexistent-kb-id",
            json={"display_name": "Nope"},
        )
        assert resp.status_code == 404

    def test_update_creates_audit_entry(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        client.put(
            f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}",
            json={"display_name": "Audited Update"},
        )
        audit_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert audit_resp.status_code == 200
        entries = audit_resp.json()["entries"]
        update_entries = [
            e for e in entries
            if e["entity_id"] == kb["id"] and e["change_type"] == "updated"
        ]
        assert len(update_entries) >= 1


class TestDeleteKnowledgebase:
    """Tests for DELETE /api/workspaces/{id}/knowledgebases/{kb_id}."""

    def _create_kb(self, client: TestClient, workspace_id: str) -> dict:
        resp = client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/delete-me.md",
            "display_name": "Delete Me KB",
        })
        assert resp.status_code == 201
        return resp.json()

    def test_delete_success(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        resp = client.delete(f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}")
        assert resp.status_code == 200
        assert resp.json()["status"] == "deleted"

    def test_delete_removes_from_list(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        client.delete(f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}")

        list_resp = client.get(f"/api/workspaces/{workspace_id}/knowledgebases")
        assert list_resp.status_code == 200
        kb_ids = [k["id"] for k in list_resp.json()]
        assert kb["id"] not in kb_ids

    def test_delete_not_found(self, client: TestClient, workspace_id: str):
        resp = client.delete(f"/api/workspaces/{workspace_id}/knowledgebases/nonexistent-kb-id")
        assert resp.status_code == 404

    def test_delete_creates_audit_entry(self, client: TestClient, workspace_id: str):
        kb = self._create_kb(client, workspace_id)
        client.delete(f"/api/workspaces/{workspace_id}/knowledgebases/{kb['id']}")

        audit_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert audit_resp.status_code == 200
        entries = audit_resp.json()["entries"]
        remove_entries = [
            e for e in entries
            if e["entity_id"] == kb["id"] and e["change_type"] == "removed"
        ]
        assert len(remove_entries) >= 1


# ============================================================================
# Context Endpoints
# Validates: Requirements 29.9, 29.10
# ============================================================================


class TestGetContext:
    """Tests for GET /api/workspaces/{id}/context."""

    def test_get_context_returns_content_key(self, client: TestClient, workspace_id: str):
        resp = client.get(f"/api/workspaces/{workspace_id}/context")
        assert resp.status_code == 200
        assert "content" in resp.json()

    def test_get_context_any_workspace_returns_200(self, client: TestClient):
        """Context endpoint is a stub — returns 200 with empty content for any workspace."""
        resp = client.get("/api/workspaces/nonexistent-ws-id/context")
        assert resp.status_code == 200
        assert resp.json()["content"] == ""


class TestUpdateContext:
    """Tests for PUT /api/workspaces/{id}/context."""

    def test_update_context_success(self, client: TestClient, workspace_id: str):
        resp = client.put(f"/api/workspaces/{workspace_id}/context", json={
            "content": "# My Workspace\n\nThis is the project context.",
        })
        assert resp.status_code == 200
        assert resp.json()["status"] == "updated"

    def test_update_then_get_context(self, client: TestClient, workspace_id: str):
        """Context is stub — GET always returns empty content regardless of PUT."""
        content = "# Updated Context\n\nNew content here."
        client.put(f"/api/workspaces/{workspace_id}/context", json={"content": content})

        resp = client.get(f"/api/workspaces/{workspace_id}/context")
        assert resp.status_code == 200
        # Context is managed via .context/ files now, so this endpoint returns ""
        assert resp.json()["content"] == ""

    def test_update_context_any_workspace_returns_200(self, client: TestClient):
        """Context update endpoint is a stub — returns 200 for any workspace."""
        resp = client.put("/api/workspaces/nonexistent-ws-id/context", json={
            "content": "nope",
        })
        assert resp.status_code == 200


class TestCompressContext:
    """Tests for POST /api/workspaces/{id}/context/compress."""

    def test_compress_context_success(self, client: TestClient, workspace_id: str):
        client.put(f"/api/workspaces/{workspace_id}/context", json={
            "content": "# Project\n\nSome context to compress.",
        })

        resp = client.post(f"/api/workspaces/{workspace_id}/context/compress")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "compressed"
        assert "content" in data

    def test_compress_empty_context(self, client: TestClient, workspace_id: str):
        resp = client.post(f"/api/workspaces/{workspace_id}/context/compress")
        assert resp.status_code == 200

    def test_compress_any_workspace_returns_200(self, client: TestClient):
        """Context compress endpoint is a stub — returns 200 for any workspace."""
        resp = client.post("/api/workspaces/nonexistent-ws-id/context/compress")
        assert resp.status_code == 200


# ============================================================================
# Audit Log Endpoints
# Validates: Requirement 25.5
# ============================================================================


class TestGetAuditLog:
    """Tests for GET /api/workspaces/{id}/audit-log."""

    def test_get_audit_log_empty(self, client: TestClient, workspace_id: str):
        resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert resp.status_code == 200
        data = resp.json()
        assert data["entries"] == []
        assert data["total"] == 0
        assert data["has_more"] is False

    def test_get_audit_log_after_kb_add(self, client: TestClient, workspace_id: str):
        """Adding a knowledgebase should produce an audit entry."""
        client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "url",
            "source_path": "https://example.com",
            "display_name": "Example",
        })

        resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] >= 1
        assert len(data["entries"]) >= 1

    def test_audit_log_entry_fields(self, client: TestClient, workspace_id: str):
        """Audit entries should have all required fields."""
        client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/fields-test.md",
            "display_name": "Fields Test",
        })

        resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert resp.status_code == 200
        entry = resp.json()["entries"][0]
        assert "id" in entry
        assert "workspace_id" in entry
        assert "change_type" in entry
        assert "entity_type" in entry
        assert "entity_id" in entry
        assert "changed_by" in entry
        assert "changed_at" in entry

    def test_audit_log_pagination_limit(self, client: TestClient, workspace_id: str):
        for i in range(5):
            client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
                "source_type": "local_file",
                "source_path": f"/data/page-{i}.md",
                "display_name": f"Page KB {i}",
            })

        resp = client.get(f"/api/workspaces/{workspace_id}/audit-log?limit=2")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["entries"]) == 2
        assert data["total"] >= 5
        assert data["has_more"] is True

    def test_audit_log_pagination_offset(self, client: TestClient, workspace_id: str):
        for i in range(4):
            client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
                "source_type": "local_file",
                "source_path": f"/data/off-{i}.md",
                "display_name": f"Offset KB {i}",
            })

        all_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log?limit=100")
        total = all_resp.json()["total"]

        offset_resp = client.get(f"/api/workspaces/{workspace_id}/audit-log?offset=2&limit=100")
        assert offset_resp.status_code == 200
        assert len(offset_resp.json()["entries"]) == total - 2

    def test_audit_log_all_entries_belong_to_workspace(
        self, client: TestClient, workspace_id: str
    ):
        """Audit log entries all belong to the singleton workspace."""
        client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/ws1.md",
            "display_name": "WS1 KB",
        })
        client.post(f"/api/workspaces/{workspace_id}/knowledgebases", json={
            "source_type": "local_file",
            "source_path": "/data/ws2.md",
            "display_name": "WS2 KB",
        })

        resp = client.get(f"/api/workspaces/{workspace_id}/audit-log")
        assert resp.status_code == 200
        entries = resp.json()["entries"]
        assert all(e["workspace_id"] == workspace_id for e in entries)


# ============================================================================
# Privileged Capability Confirmation
# Validates: Requirements 16.2, 16.11, 17.2, 17.11
# ============================================================================


class TestPrivilegedCapabilities:
    """Tests for privileged skill/MCP handling."""

    @pytest.mark.anyio
    async def test_privileged_skill_flag_in_response(self, client: TestClient, workspace_id: str):
        """Skills include is_privileged flag in response.

        In filesystem model, is_privileged is based on source_tier=='builtin'.
        """
        resp = client.get(f"/api/workspaces/{workspace_id}/skills")
        assert resp.status_code == 200
        for skill in resp.json():
            assert "is_privileged" in skill
            assert isinstance(skill["is_privileged"], bool)

    @pytest.mark.anyio
    async def test_privileged_mcp_flag_in_response(self, client: TestClient, workspace_id: str):
        """Privileged MCPs are flagged in GET response."""
        safe_id = await _seed_mcp("SafeMCP", is_privileged=False)
        priv_id = await _seed_mcp("PrivilegedMCP", is_privileged=True)
        await _set_workspace_mcp(workspace_id, safe_id, enabled=True)
        await _set_workspace_mcp(workspace_id, priv_id, enabled=True)

        resp = client.get(f"/api/workspaces/{workspace_id}/mcps")
        assert resp.status_code == 200
        data = resp.json()

        safe = [m for m in data if m["mcp_server_id"] == safe_id]
        priv = [m for m in data if m["mcp_server_id"] == priv_id]
        assert len(safe) == 1
        assert safe[0]["is_privileged"] is False
        assert len(priv) == 1
        assert priv[0]["is_privileged"] is True

    @pytest.mark.anyio
    async def test_privileged_skill_can_be_toggled(self, client: TestClient, workspace_id: str):
        """PUT skills succeeds and creates audit entries for toggling."""
        resp = client.put(f"/api/workspaces/{workspace_id}/skills", json={
            "configs": [{
                "skill_id": "toggle-priv-test",
                "skill_name": "TogglePriv",
                "enabled": True,
                "is_privileged": True,
            }]
        })
        assert resp.status_code == 200

        resp = client.put(f"/api/workspaces/{workspace_id}/skills", json={
            "configs": [{
                "skill_id": "toggle-priv-test",
                "skill_name": "TogglePriv",
                "enabled": False,
                "is_privileged": True,
            }]
        })
        assert resp.status_code == 200
