"""Tests for the auth middleware (core/src/greymatter_core/security/middleware.py).

Covers both the ASGI AuthMiddleware and the require_auth FastAPI dependency.
Uses httpx.ASGITransport to test against a real FastAPI app without network I/O.
"""

from __future__ import annotations

import os

import httpx
import pytest
from fastapi import Depends, FastAPI
from greymatter_core.security.middleware import AuthMiddleware, PUBLIC_PATHS, require_auth  # noqa: E402
from greymatter_core.security.tokens import generate_node_token  # noqa: E402

# Set cluster secret so middleware verification uses the same key as test tokens
SECRET = b"test-middleware-secret-32bytes!!"
os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()


def _make_token(node_id: str = "test-node") -> str:
    return generate_node_token(node_id, secret=SECRET)


# -- Helpers: build test apps --


def _app_with_middleware(public_paths: set[str] | None = None) -> FastAPI:
    """FastAPI app protected by AuthMiddleware."""
    app = FastAPI()
    app.add_middleware(AuthMiddleware, public_paths=public_paths)

    @app.get("/health")
    async def health():
        return {"status": "ok"}

    @app.get("/api/data")
    async def data():
        return {"data": "secret"}

    return app


def _app_with_dependency() -> FastAPI:
    """FastAPI app using require_auth dependency on specific routes."""
    app = FastAPI()

    @app.get("/public")
    async def public():
        return {"status": "open"}

    @app.get("/protected")
    async def protected(node_id: str = Depends(require_auth)):
        return {"node_id": node_id}

    return app


# =========================================================================
# AuthMiddleware Tests
# =========================================================================


class TestAuthMiddlewarePublicPaths:
    """Public paths should bypass authentication."""

    @pytest.mark.asyncio
    async def test_health_no_auth_required(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get("/health")
            assert resp.status_code == 200
            assert resp.json()["status"] == "ok"

    @pytest.mark.asyncio
    async def test_default_public_paths(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            for path in PUBLIC_PATHS:
                resp = await client.get(path)
                # Some paths may 404 (no handler), but should NOT be 401
                assert resp.status_code != 401, f"{path} should not require auth"

    @pytest.mark.asyncio
    async def test_custom_public_paths(self):
        app = _app_with_middleware(public_paths={"/health", "/api/data"})
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get("/api/data")
            assert resp.status_code == 200


class TestAuthMiddlewareRejectsUnauthenticated:
    """Protected paths should reject requests without valid auth."""

    @pytest.mark.asyncio
    async def test_missing_auth_header(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get("/api/data")
            assert resp.status_code == 401
            assert "Missing authorization header" in resp.json()["detail"]
            assert resp.headers.get("www-authenticate") == "Bearer"

    @pytest.mark.asyncio
    async def test_malformed_auth_header(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/api/data",
                headers={"Authorization": "Basic dXNlcjpwYXNz"},
            )
            assert resp.status_code == 401

    @pytest.mark.asyncio
    async def test_invalid_token(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/api/data",
                headers={"Authorization": "Bearer invalid.token.here.bad"},
            )
            assert resp.status_code == 401
            assert "Invalid or expired" in resp.json()["detail"]

    @pytest.mark.asyncio
    async def test_expired_token(self):
        token = _make_token("worker-1")
        parts = token.split(".")
        parts[1] = "1000000000"  # Jan 2001 — ancient timestamp, breaks HMAC
        tampered = ".".join(parts)

        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/api/data",
                headers={"Authorization": f"Bearer {tampered}"},
            )
            assert resp.status_code == 401


class TestAuthMiddlewareAcceptsValidToken:
    """Protected paths should accept valid bearer tokens."""

    @pytest.mark.asyncio
    async def test_valid_token_passes(self):
        token = _make_token("worker-1")
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            assert resp.json()["data"] == "secret"

    @pytest.mark.asyncio
    async def test_different_nodes_both_valid(self):
        app = _app_with_middleware()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            for node in ["worker-1", "worker-2", "coordinator"]:
                token = _make_token(node)
                resp = await client.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {token}"},
                )
                assert resp.status_code == 200


class TestAuthMiddlewareNonHTTP:
    """Non-HTTP scopes (e.g., WebSocket lifespan) should pass through."""

    @pytest.mark.asyncio
    async def test_non_http_scope_passes(self):
        called = False

        async def inner_app(scope, receive, send):
            nonlocal called
            called = True

        middleware = AuthMiddleware(inner_app)
        await middleware({"type": "lifespan"}, None, None)
        assert called is True


# =========================================================================
# require_auth Dependency Tests
# =========================================================================


class TestRequireAuthDependency:
    """Tests for the require_auth FastAPI dependency."""

    @pytest.mark.asyncio
    async def test_public_route_no_auth(self):
        app = _app_with_dependency()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get("/public")
            assert resp.status_code == 200
            assert resp.json()["status"] == "open"

    @pytest.mark.asyncio
    async def test_protected_route_without_token(self):
        app = _app_with_dependency()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get("/protected")
            assert resp.status_code in (401, 403)

    @pytest.mark.asyncio
    async def test_protected_route_with_valid_token(self):
        token = _make_token("dep-node")
        app = _app_with_dependency()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/protected",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            assert resp.json()["node_id"] == "dep-node"

    @pytest.mark.asyncio
    async def test_protected_route_with_invalid_token(self):
        app = _app_with_dependency()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/protected",
                headers={"Authorization": "Bearer bad.token.data.sig"},
            )
            assert resp.status_code == 401

    @pytest.mark.asyncio
    async def test_node_id_returned(self):
        token = _make_token("my-special-node")
        app = _app_with_dependency()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            resp = await client.get(
                "/protected",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.json()["node_id"] == "my-special-node"


# =========================================================================
# PUBLIC_PATHS constant
# =========================================================================


class TestPublicPaths:
    """Verify the PUBLIC_PATHS constant."""

    def test_is_frozenset(self):
        assert isinstance(PUBLIC_PATHS, frozenset)

    def test_health_in_public(self):
        assert "/health" in PUBLIC_PATHS

    def test_docs_in_public(self):
        assert "/docs" in PUBLIC_PATHS

    def test_openapi_in_public(self):
        assert "/openapi.json" in PUBLIC_PATHS
