"""ACL (Access Control List) boundary tests.

Verifies that the permission model correctly restricts document access
based on user identity (office, department, area, role).

Permission model:
  - ACL IDs use prefixed identifiers: U_ (user), O_ (office),
    D_ (department), A_ (area), R_ (role)
  - A document is visible if ANY of the user's tokens matches
    the document's acl_ids, OR the document has no acl_ids (public).

Predefined test users (from mock.py):
  admin     : O_01, D_01, A_01, R_01  (area leader)
  zhang_san : O_17, D_05, A_01        (finance dept user)
  li_si     : O_22, D_08, A_02        (tech dept user, different area)
  wang_wu   : O_01, D_05, A_01, R_03  (dept manager)
"""

import pytest
from httpx import AsyncClient


# ── Helper to build tokens for specific users ────────────────────────────────

def _make_headers(make_token, **kwargs) -> dict[str, str]:
    """Build Authorization headers from custom claims."""
    token = make_token(**kwargs)
    return {"Authorization": f"Bearer {token}"}


# ── Search endpoint ACL tests ────────────────────────────────────────────────


@pytest.mark.asyncio
class TestSearchACL:
    """POST /api/v1/search — verify ACL filtering in search results."""

    async def test_search_different_users_different_results(
        self,
        client: AsyncClient,
        api_prefix: str,
        auth_headers: dict,
        user_headers: dict,
    ):
        """Admin (A_01, D_01) and regular user (D_05, O_17) may see different
        result sets for the same query due to ACL filtering."""
        query_payload = {"query": "政府", "page": 1, "page_size": 50}

        resp_admin = await client.post(
            f"{api_prefix}/search", json=query_payload, headers=auth_headers,
        )
        assert resp_admin.status_code == 200

        resp_user = await client.post(
            f"{api_prefix}/search", json=query_payload, headers=user_headers,
        )
        assert resp_user.status_code == 200

        # Both requests should succeed; total counts may differ
        admin_total = resp_admin.json()["total"]
        user_total = resp_user.json()["total"]
        # Admin (area leader with R_01) typically has broader access
        assert admin_total >= 0
        assert user_total >= 0

    async def test_search_with_custom_acl_user(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """A user with ACL tokens matching NO documents should get 0 results."""
        headers = _make_headers(
            make_token,
            user_id="isolated_user_xyz",
            office_id="O_99",
            dept_id="D_99",
            area_id="A_99",
            role_ids=[],
        )
        resp = await client.post(
            f"{api_prefix}/search",
            json={"query": "政府", "page_size": 10},
            headers=headers,
        )
        assert resp.status_code == 200
        body = resp.json()
        # An isolated user should see only public docs (if any)
        assert body["total"] >= 0


# ── Document detail ACL tests ────────────────────────────────────────────────


@pytest.mark.asyncio
class TestDocumentDetailACL:
    """GET /api/v1/document/{doc_id} — ACL enforcement on document detail."""

    async def test_unauthenticated_rejected(self, client: AsyncClient, api_prefix: str):
        resp = await client.get(f"{api_prefix}/document/any-doc-id")
        assert resp.status_code in (401, 403)

    async def test_nonexistent_doc_returns_404(
        self, client: AsyncClient, api_prefix: str, auth_headers: dict,
    ):
        resp = await client.get(
            f"{api_prefix}/document/nonexistent_doc_acl_xyz",
            headers=auth_headers,
        )
        assert resp.status_code == 404


@pytest.mark.asyncio
class TestDocumentDeleteACL:
    """DELETE /api/v1/document/{doc_id} — ACL enforcement on document delete."""

    async def test_delete_unauthenticated(self, client: AsyncClient, api_prefix: str):
        resp = await client.delete(f"{api_prefix}/document/any-doc-id")
        assert resp.status_code in (401, 403)

    async def test_delete_nonexistent(self, client: AsyncClient, api_prefix: str, auth_headers: dict):
        resp = await client.delete(
            f"{api_prefix}/document/nonexistent_doc_del_xyz",
            headers=auth_headers,
        )
        assert resp.status_code == 404


# ── Permission token construction tests ──────────────────────────────────────


@pytest.mark.asyncio
class TestPermissionTokens:
    """Verify that JWT claims produce the correct ACL token sets.

    These tests validate the token generation and permission resolution
    without needing documents in ES. They test the auth pipeline.
    """

    async def test_admin_token_has_area_role(
        self,
        client: AsyncClient,
        api_prefix: str,
        auth_headers: dict,
    ):
        """Admin user should be able to access admin endpoints (has R_01 role)."""
        resp = await client.get(f"{api_prefix}/admin/stats", headers=auth_headers)
        assert resp.status_code == 200

    async def test_regular_user_can_search(
        self,
        client: AsyncClient,
        api_prefix: str,
        user_headers: dict,
    ):
        """Regular user (zhang_san) should be able to search."""
        resp = await client.post(
            f"{api_prefix}/search",
            json={"query": "测试"},
            headers=user_headers,
        )
        assert resp.status_code == 200

    async def test_custom_user_different_area(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """User from area A_02 should still be able to search (just see different results)."""
        headers = _make_headers(
            make_token,
            user_id="li_si_test",
            office_id="O_22",
            dept_id="D_08",
            area_id="A_02",
            role_ids=[],
        )
        resp = await client.post(
            f"{api_prefix}/search",
            json={"query": "政务"},
            headers=headers,
        )
        assert resp.status_code == 200

    async def test_user_with_multiple_roles(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """User with multiple roles should have broader ACL token set."""
        headers = _make_headers(
            make_token,
            user_id="multi_role_user",
            office_id="O_01",
            dept_id="D_01",
            area_id="A_01",
            role_ids=["R_01", "R_03", "R_05"],
        )
        resp = await client.post(
            f"{api_prefix}/search",
            json={"query": "改革"},
            headers=headers,
        )
        assert resp.status_code == 200


# ── Cross-department / cross-office boundary tests ───────────────────────────


@pytest.mark.asyncio
class TestCrossBoundaryAccess:
    """Verify that users from different organizational boundaries
    have properly isolated access when documents have strict ACL."""

    async def test_department_level_isolation(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """Users in D_05 vs D_08 should have different ACL token sets.

        This tests that the permission system constructs distinct token
        sets for users in different departments.
        """
        # zhang_san style (D_05)
        h_d05 = _make_headers(
            make_token, user_id="u_d05", office_id="O_17", dept_id="D_05", area_id="A_01",
        )
        # li_si style (D_08)
        h_d08 = _make_headers(
            make_token, user_id="u_d08", office_id="O_22", dept_id="D_08", area_id="A_02",
        )
        # Both should be able to search
        resp_d05 = await client.post(
            f"{api_prefix}/search",
            json={"query": "数字政府", "page_size": 10},
            headers=h_d05,
        )
        resp_d08 = await client.post(
            f"{api_prefix}/search",
            json={"query": "数字政府", "page_size": 10},
            headers=h_d08,
        )
        assert resp_d05.status_code == 200
        assert resp_d08.status_code == 200

    async def test_office_level_isolation(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """Users in O_01 vs O_17 should have different token sets."""
        h_o01 = _make_headers(
            make_token, user_id="u_o01", office_id="O_01", dept_id="D_01", area_id="A_01",
        )
        h_o17 = _make_headers(
            make_token, user_id="u_o17", office_id="O_17", dept_id="D_05", area_id="A_01",
        )
        resp_o01 = await client.post(
            f"{api_prefix}/search",
            json={"query": "通知", "page_size": 10},
            headers=h_o01,
        )
        resp_o17 = await client.post(
            f"{api_prefix}/search",
            json={"query": "通知", "page_size": 10},
            headers=h_o17,
        )
        assert resp_o01.status_code == 200
        assert resp_o17.status_code == 200

    async def test_area_level_isolation(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """Users in A_01 vs A_02 should have different token sets."""
        h_a01 = _make_headers(
            make_token, user_id="u_a01", office_id="O_01", dept_id="D_01", area_id="A_01",
        )
        h_a02 = _make_headers(
            make_token, user_id="u_a02", office_id="O_22", dept_id="D_08", area_id="A_02",
        )
        resp_a01 = await client.post(
            f"{api_prefix}/search",
            json={"query": "规划", "page_size": 10},
            headers=h_a01,
        )
        resp_a02 = await client.post(
            f"{api_prefix}/search",
            json={"query": "规划", "page_size": 10},
            headers=h_a02,
        )
        assert resp_a01.status_code == 200
        assert resp_a02.status_code == 200

    async def test_role_based_access(
        self,
        client: AsyncClient,
        api_prefix: str,
        make_token,
    ):
        """User with role R_01 vs no roles should potentially see different results."""
        h_with_role = _make_headers(
            make_token, user_id="u_role", office_id="O_01", dept_id="D_01",
            area_id="A_01", role_ids=["R_01"],
        )
        h_no_role = _make_headers(
            make_token, user_id="u_norole", office_id="O_01", dept_id="D_01",
            area_id="A_01", role_ids=[],
        )
        resp_role = await client.post(
            f"{api_prefix}/search",
            json={"query": "改革", "page_size": 50},
            headers=h_with_role,
        )
        resp_no_role = await client.post(
            f"{api_prefix}/search",
            json={"query": "改革", "page_size": 50},
            headers=h_no_role,
        )
        assert resp_role.status_code == 200
        assert resp_no_role.status_code == 200

        # User with role can see role-restricted docs; without role cannot
        total_with_role = resp_role.json()["total"]
        total_no_role = resp_no_role.json()["total"]
        assert total_with_role >= total_no_role


# ── Graph endpoint ACL (auth required) ───────────────────────────────────────


@pytest.mark.asyncio
class TestGraphACL:
    """Verify graph endpoints enforce authentication."""

    async def test_graph_overview_requires_auth(self, client: AsyncClient, api_prefix: str):
        resp = await client.get(f"{api_prefix}/graph/overview")
        assert resp.status_code in (401, 403)

    async def test_graph_search_requires_auth(self, client: AsyncClient, api_prefix: str):
        resp = await client.post(
            f"{api_prefix}/graph/search",
            json={"name": "测试"},
        )
        assert resp.status_code in (401, 403)

    async def test_graph_related_docs_requires_auth(self, client: AsyncClient, api_prefix: str):
        resp = await client.post(
            f"{api_prefix}/graph/related-docs",
            json={"entity_name": "test", "entity_label": "Organization"},
        )
        assert resp.status_code in (401, 403)

    async def test_document_graph_requires_auth(self, client: AsyncClient, api_prefix: str):
        resp = await client.get(f"{api_prefix}/document/some-doc-id/graph")
        assert resp.status_code in (401, 403)


# ── Research endpoint ACL ────────────────────────────────────────────────────


@pytest.mark.asyncio
class TestResearchACL:
    """POST /api/v1/research — auth enforcement."""

    async def test_research_requires_auth(self, client: AsyncClient, api_prefix: str):
        resp = await client.post(
            f"{api_prefix}/research",
            json={"question": "测试问题"},
        )
        assert resp.status_code in (401, 403)

    async def test_research_with_different_users(
        self,
        client: AsyncClient,
        api_prefix: str,
        auth_headers: dict,
        user_headers: dict,
    ):
        """Both admin and regular user should be able to use research endpoint."""
        for headers in (auth_headers, user_headers):
            resp = await client.post(
                f"{api_prefix}/research",
                json={"question": "数字政府建设的目标是什么？"},
                headers=headers,
            )
            assert resp.status_code == 200


# ── Ingest endpoint permission tests ─────────────────────────────────────────


@pytest.mark.asyncio
class TestIngestACL:
    """Ingest endpoints have varying auth requirements."""

    async def test_ingest_status_requires_auth(self, client: AsyncClient, api_prefix: str):
        """GET /ingest/status/{task_id} should require auth."""
        resp = await client.get(f"{api_prefix}/ingest/status/some-task-id")
        assert resp.status_code in (401, 403)

    async def test_admin_delete_requires_auth(self, client: AsyncClient, api_prefix: str):
        """DELETE /admin/document/{doc_id} should require auth."""
        resp = await client.delete(f"{api_prefix}/admin/document/some-doc-id")
        assert resp.status_code in (401, 403)

    async def test_admin_stats_requires_auth(self, client: AsyncClient, api_prefix: str):
        """GET /admin/stats should require auth."""
        resp = await client.get(f"{api_prefix}/admin/stats")
        assert resp.status_code in (401, 403)

    async def test_admin_ingest_logs_requires_auth(self, client: AsyncClient, api_prefix: str):
        """GET /admin/ingest-logs should require auth."""
        resp = await client.get(f"{api_prefix}/admin/ingest-logs")
        assert resp.status_code in (401, 403)
