"""Tests for the PyAuthz FastAPI integration."""

from fastapi import Depends, FastAPI, Request
from fastapi.testclient import TestClient

from pyauthz import (
    Condition,
    Effect,
    Policy,
    PyAuthz,
    Resource,
    Role,
    Subject,
)
from pyauthz_fastapi import get_subject, requires, setup_pyauthz


def make_app(rp: PyAuthz, subject: Subject | None = None) -> FastAPI:
    """Create a test FastAPI app with PyAuthz middleware."""
    app = FastAPI()

    async def resolve_user(request: Request) -> Subject:
        """Simple test resolver: use a fixed subject or parse from header."""
        if subject is not None:
            return subject
        user_id = request.headers.get("X-User-Id", "anonymous")
        roles = (
            request.headers.get("X-User-Roles", "").split(",")
            if request.headers.get("X-User-Roles")
            else []
        )
        return Subject(id=user_id, roles=roles)

    setup_pyauthz(app, rp, resolve_user)
    return app


# --- Basic middleware + requires ---


class TestMiddlewareAndRequires:
    def test_allowed_request(self):
        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        admin = Subject(id="alice", roles=["admin"])
        app = make_app(rp, admin)

        @app.get("/items")
        @requires("documents:read")
        async def get_docs(request: Request):
            return {"ok": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get("/items")
        assert resp.status_code == 200
        assert resp.json() == {"ok": True}

    def test_denied_request_returns_403(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        viewer = Subject(id="bob", roles=["viewer"])
        app = make_app(rp, viewer)

        @app.delete("/docs/{doc_id}")
        @requires("documents:delete")
        async def delete_doc(request: Request, doc_id: str):
            return {"deleted": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.delete("/docs/123")
        assert resp.status_code == 403
        body = resp.json()
        assert body["error"] == "forbidden"
        assert "reasoning" in body

    def test_action_shorthand_with_resource_type(self):
        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        editor = Subject(id="alice", roles=["editor"])
        app = make_app(rp, editor)

        @app.put("/docs/{doc_id}")
        @requires("documents:write")
        async def update_doc(request: Request, doc_id: str):
            return {"updated": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.put("/docs/1")
        assert resp.status_code == 200


# --- Subject dependency injection ---


class TestDependencies:
    def test_get_subject(self):
        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        app = make_app(rp)

        @app.get("/me")
        async def me(request: Request, subject: Subject = Depends(get_subject)):  # noqa: B008
            return {"id": subject.id, "roles": subject.roles}

        client = TestClient(app)
        resp = client.get("/me", headers={"X-User-Id": "alice", "X-User-Roles": "admin"})
        assert resp.status_code == 200
        assert resp.json()["id"] == "alice"
        assert "admin" in resp.json()["roles"]

    def test_get_decision_after_requires(self):
        """After @requires runs, the decision is on request.state.pyauthz_decision."""
        rp = PyAuthz()
        rp.add_role(Role(name="admin", permissions=["*:*"]))
        admin = Subject(id="alice", roles=["admin"])
        app = make_app(rp, admin)

        @app.get("/items")
        @requires("documents:read")
        async def get_docs(request: Request):
            # The decorator sets the decision on request.state
            decision = getattr(request.state, "pyauthz_decision", None)
            return {"allowed": decision.allowed if decision else None}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get("/items")
        assert resp.status_code == 200
        assert resp.json()["allowed"] is True


# --- Resource resolver ---


class TestResourceResolver:
    def test_custom_resource_resolver(self):
        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )

        alice = Subject(id="alice", roles=["editor"])
        app = make_app(rp, alice)

        def resolve_document(request: Request, doc_id: str = "", **kwargs) -> Resource:
            # In real code, you'd look up the document from DB
            return Resource(id=doc_id, type="documents", owner_id="alice")

        @app.put("/docs/{doc_id}")
        @requires("documents:write", resource_resolver=resolve_document)
        async def update_doc(request: Request, doc_id: str):
            return {"updated": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.put("/docs/doc-1")
        assert resp.status_code == 200

    def test_custom_resolver_non_owner_denied(self):
        rp = PyAuthz()
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )

        bob = Subject(id="bob", roles=["editor"])
        app = make_app(rp, bob)

        def resolve_document(request: Request, doc_id: str = "", **kwargs) -> Resource:
            return Resource(id=doc_id, type="documents", owner_id="alice")

        @app.put("/docs/{doc_id}")
        @requires("documents:write", resource_resolver=resolve_document)
        async def update_doc(request: Request, doc_id: str):
            return {"updated": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.put("/docs/doc-1")
        assert resp.status_code == 403


# --- Header-based subject resolution ---


class TestHeaderBasedResolver:
    def test_resolve_from_headers(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        app = make_app(rp)  # No fixed subject — uses header resolver

        @app.get("/items")
        @requires("documents:read")
        async def get_docs(request: Request):
            return {"ok": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get("/items", headers={"X-User-Id": "carol", "X-User-Roles": "viewer"})
        assert resp.status_code == 200

    def test_anonymous_user_denied(self):
        rp = PyAuthz()
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        app = make_app(rp)

        @app.get("/items")
        @requires("documents:read")
        async def get_docs(request: Request):
            return {"ok": True}

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get("/items")  # No headers → anonymous with no roles
        assert resp.status_code == 403
