"""Integration tests: Auth Middleware — end-to-end authentication flows.

Validates the auth middleware working with the full security stack:
- AuthMiddleware (ASGI) + require_auth (dependency) on the same app
- TokenStore issue/rotate/revoke with live middleware verification
- Multi-node authentication with distinct identities
- Request state propagation (node_id flows to route handlers)
- Concurrent authenticated requests
- Edge cases: token expiry, rotation grace periods, revocation
- Error response format compliance (WWW-Authenticate header, JSON body)

Uses httpx.ASGITransport to test against a real FastAPI app without network I/O.

Run with: pytest core/tests/test_auth_middleware_integration.py -v
"""

from __future__ import annotations

import asyncio
import os
import time

import httpx
import pytest
from fastapi import Depends, FastAPI, Request

from greymatter_core.security.middleware import AuthMiddleware, PUBLIC_PATHS, require_auth
from greymatter_core.security.tokens import (
    TokenStore,
    generate_node_token,
    verify_node_token,
)

# Use the SAME cluster secret as test_auth_middleware.py to avoid env var collisions
# when both files run in the same pytest session (module-level os.environ sets are global)
SECRET = b"test-middleware-secret-32bytes!!"
os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()


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


@pytest.fixture
def token_store(tmp_path):
    """TokenStore backed by a temp SQLite database."""
    store = TokenStore(str(tmp_path / "tokens.db"))
    yield store
    store.close()


def _make_token(node_id: str = "test-node") -> str:
    """Generate a valid token for testing."""
    return generate_node_token(node_id, secret=SECRET)


def _build_full_app() -> FastAPI:
    """Build a FastAPI app with both middleware and dependency-based auth.

    - AuthMiddleware protects all non-public paths (blanket)
    - /api/identity uses require_auth dependency for node_id extraction
    - /health is public (in default PUBLIC_PATHS)
    - /api/data is protected by middleware only
    """
    app = FastAPI()
    app.add_middleware(AuthMiddleware)

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

    @app.get("/api/data")
    async def data(request: Request):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        return {"data": "secret", "node_id": node_id}

    @app.get("/api/identity")
    async def identity(node_id: str = Depends(require_auth)):
        return {"node_id": node_id, "authenticated": True}

    @app.get("/api/nodes")
    async def list_nodes(request: Request):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        return {"caller": node_id, "nodes": ["worker-1", "worker-2"]}

    @app.post("/api/work")
    async def create_work(request: Request):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        body = await request.json()
        return {"created": True, "by": node_id, "title": body.get("title")}

    return app


def _build_mixed_app() -> FastAPI:
    """App with mix of public routes, middleware-protected, and dependency-protected."""
    app = FastAPI()
    app.add_middleware(AuthMiddleware, public_paths={"/health", "/metrics", "/ready"})

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

    @app.get("/metrics")
    async def metrics():
        return {"requests": 42}

    @app.get("/ready")
    async def ready():
        return {"ready": True}

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

    @app.get("/api/status")
    async def status(request: Request):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        return {"status": "running", "node_id": node_id}

    return app


# ---------------------------------------------------------------------------
# 1. End-to-end: public + protected paths on same app
# ---------------------------------------------------------------------------


class TestEndToEndAuthFlow:
    """Full request lifecycle with middleware + dependency on the same app."""

    async def test_public_path_no_auth_required(self):
        """Health endpoint accessible without any credentials."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/health")
            assert resp.status_code == 200
            assert resp.json()["status"] == "ok"

    async def test_protected_path_rejected_without_token(self):
        """Protected endpoint returns 401 without auth header."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/api/data")
            assert resp.status_code == 401
            assert resp.json()["detail"] == "Missing authorization header"
            assert resp.headers.get("www-authenticate") == "Bearer"

    async def test_protected_path_accepts_valid_token(self):
        """Protected endpoint returns data with valid bearer token."""
        token = _make_token("worker-1")
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            assert resp.json()["data"] == "secret"

    async def test_node_id_propagated_to_handler(self):
        """Middleware injects node_id into scope state, accessible by handler."""
        token = _make_token("coordinator-node")
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            assert resp.json()["node_id"] == "coordinator-node"

    async def test_dependency_returns_node_id(self):
        """require_auth dependency extracts and returns node_id."""
        token = _make_token("dep-node")
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/identity",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["node_id"] == "dep-node"
            assert body["authenticated"] is True

    async def test_post_with_auth(self):
        """POST requests work with auth middleware."""
        token = _make_token("submitter-1")
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.post(
                "/api/work",
                headers={"Authorization": f"Bearer {token}"},
                json={"title": "Integration task"},
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["created"] is True
            assert body["by"] == "submitter-1"
            assert body["title"] == "Integration task"


# ---------------------------------------------------------------------------
# 2. TokenStore integration with live middleware
# ---------------------------------------------------------------------------


class TestTokenStoreWithMiddleware:
    """TokenStore-issued tokens work through the full middleware stack."""

    async def test_store_issued_token_accepted(self, token_store):
        """Token from TokenStore.issue() is accepted by middleware."""
        token = token_store.issue("store-node-1")
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token}"},
            )
            assert resp.status_code == 200
            assert resp.json()["node_id"] == "store-node-1"

    async def test_token_rotation_both_valid(self, token_store):
        """After rotation, both old and new tokens pass HMAC verification."""
        old_token = token_store.issue("rotate-node")
        new_token = token_store.issue("rotate-node")  # Rotates
        assert old_token != new_token

        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            # New token works
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {new_token}"},
            )
            assert resp.status_code == 200

            # Old token also works (same HMAC secret, not yet expired)
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {old_token}"},
            )
            assert resp.status_code == 200

    async def test_store_list_tracks_nodes(self, token_store):
        """TokenStore tracks all issued nodes."""
        token_store.issue("node-a")
        token_store.issue("node-b")
        token_store.issue("node-c")

        nodes = token_store.list_nodes()
        node_ids = {n["node_id"] for n in nodes}
        assert {"node-a", "node-b", "node-c"} <= node_ids

    async def test_store_revoke_removes_record(self, token_store):
        """Revoked node no longer appears in list."""
        token_store.issue("ephemeral-node")
        token_store.revoke("ephemeral-node")

        nodes = token_store.list_nodes()
        node_ids = {n["node_id"] for n in nodes}
        assert "ephemeral-node" not in node_ids

    async def test_store_verify_delegates_to_hmac(self, token_store):
        """TokenStore.verify() uses stateless HMAC verification."""
        token = token_store.issue("verify-node")
        valid, node_id = token_store.verify(token)
        assert valid is True
        assert node_id == "verify-node"


# ---------------------------------------------------------------------------
# 3. Multi-node authentication
# ---------------------------------------------------------------------------


class TestMultiNodeAuthentication:
    """Multiple nodes authenticate simultaneously with distinct identities."""

    async def test_multiple_nodes_distinct_identities(self):
        """Each node's token resolves to its own identity."""
        nodes = ["worker-1", "worker-2", "coordinator", "reviewer"]
        tokens = {n: _make_token(n) for n in nodes}

        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            for node_id, token in tokens.items():
                resp = await c.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {token}"},
                )
                assert resp.status_code == 200
                assert resp.json()["node_id"] == node_id

    async def test_concurrent_multi_node_requests(self):
        """Multiple nodes make concurrent requests without identity mixing."""
        nodes = ["alpha", "beta", "gamma", "delta"]
        tokens = {n: _make_token(n) for n in nodes}

        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = [
                c.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {tokens[n]}"},
                )
                for n in nodes
            ]
            responses = await asyncio.gather(*tasks)

            node_ids = {r.json()["node_id"] for r in responses}
            assert node_ids == set(nodes)
            assert all(r.status_code == 200 for r in responses)


# ---------------------------------------------------------------------------
# 4. Custom public paths
# ---------------------------------------------------------------------------


class TestCustomPublicPaths:
    """AuthMiddleware with custom public_paths set."""

    async def test_custom_public_paths_accessible(self):
        """All custom public paths bypass auth."""
        app = _build_mixed_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            for path in ("/health", "/metrics", "/ready"):
                resp = await c.get(path)
                assert resp.status_code == 200, f"{path} should be public"

    async def test_non_public_path_requires_auth(self):
        """Paths not in public_paths require auth even with custom set."""
        app = _build_mixed_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/api/status")
            assert resp.status_code == 401


# ---------------------------------------------------------------------------
# 5. Error response format compliance
# ---------------------------------------------------------------------------


class TestErrorResponseFormat:
    """Auth error responses follow RFC 6750 conventions."""

    async def test_missing_token_response_format(self):
        """401 response includes WWW-Authenticate header and JSON detail."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/api/data")
            assert resp.status_code == 401
            assert "www-authenticate" in resp.headers
            assert resp.headers["www-authenticate"] == "Bearer"
            body = resp.json()
            assert "detail" in body
            assert isinstance(body["detail"], str)

    async def test_invalid_token_response_format(self):
        """Invalid token returns 401 with specific error detail."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": "Bearer invalid.token.data.sig"},
            )
            assert resp.status_code == 401
            assert "Invalid or expired" in resp.json()["detail"]
            assert resp.headers["www-authenticate"] == "Bearer"

    async def test_wrong_scheme_rejected(self):
        """Non-Bearer auth scheme is rejected."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": "Basic dXNlcjpwYXNz"},
            )
            assert resp.status_code == 401

    async def test_empty_bearer_rejected(self):
        """Empty bearer value is rejected."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": "Bearer "},
            )
            assert resp.status_code == 401


# ---------------------------------------------------------------------------
# 6. Token lifecycle integration (generate -> verify -> expire)
# ---------------------------------------------------------------------------


class TestTokenLifecycleIntegration:
    """Token generation, verification, and expiry through the middleware."""

    async def test_freshly_generated_token_works(self):
        """A just-generated token passes middleware verification."""
        token = generate_node_token("lifecycle-node", secret=SECRET)
        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert node_id == "lifecycle-node"

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

    async def test_tampered_signature_rejected(self):
        """Token with modified signature fails HMAC check."""
        token = _make_token("tamper-node")
        parts = token.split(".")
        # Flip a character in the signature
        sig = parts[3]
        tampered_sig = sig[:-1] + ("a" if sig[-1] != "a" else "b")
        parts[3] = tampered_sig
        tampered = ".".join(parts)

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

    async def test_tampered_node_id_rejected(self):
        """Token with modified node_id fails HMAC check."""
        token = _make_token("original-node")
        parts = token.split(".")
        parts[0] = "spoofed-node"
        tampered = ".".join(parts)

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

    async def test_tampered_timestamp_rejected(self):
        """Token with modified timestamp fails HMAC check."""
        token = _make_token("ts-node")
        parts = token.split(".")
        parts[1] = str(int(parts[1]) - 100)
        tampered = ".".join(parts)

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

    async def test_expired_token_rejected(self):
        """Token with max_age=0 is immediately expired."""
        token = _make_token("expire-node")
        # Verify directly with max_age=0 — should be expired
        valid, _ = verify_node_token(token, secret=SECRET, max_age=0)
        assert valid is False

    async def test_truncated_token_rejected(self):
        """Token missing parts is rejected."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": "Bearer node-id.timestamp"},
            )
            assert resp.status_code == 401


# ---------------------------------------------------------------------------
# 7. Concurrent requests with mixed auth states
# ---------------------------------------------------------------------------


class TestConcurrentMixedAuth:
    """Concurrent requests with valid, invalid, and missing auth."""

    async def test_mixed_auth_concurrent(self):
        """Mix of valid and invalid tokens in concurrent requests."""
        valid_token = _make_token("valid-node")

        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = [
                c.get("/health"),  # Public
                c.get("/api/data", headers={"Authorization": f"Bearer {valid_token}"}),
                c.get("/api/data"),  # No auth
                c.get("/api/data", headers={"Authorization": "Bearer bad.token.data.sig"}),
                c.get("/api/data", headers={"Authorization": f"Bearer {valid_token}"}),
            ]
            results = await asyncio.gather(*tasks)

            assert results[0].status_code == 200  # Public
            assert results[1].status_code == 200  # Valid
            assert results[2].status_code == 401  # No auth
            assert results[3].status_code == 401  # Bad token
            assert results[4].status_code == 200  # Valid again

    async def test_rapid_sequential_auth(self):
        """Rapid sequential requests don't leak state between calls."""
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            for i in range(10):
                node_id = f"rapid-{i}"
                token = _make_token(node_id)
                resp = await c.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {token}"},
                )
                assert resp.status_code == 200
                assert resp.json()["node_id"] == node_id


# ---------------------------------------------------------------------------
# 8. Non-HTTP scope pass-through
# ---------------------------------------------------------------------------


class TestNonHTTPScope:
    """Non-HTTP scopes (WebSocket, lifespan) pass through middleware."""

    async def test_lifespan_scope_passes(self):
        """Lifespan events pass through without auth check."""
        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

    async def test_websocket_scope_passes(self):
        """WebSocket scope passes through without auth check."""
        called = False
        received_scope = {}

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

        middleware = AuthMiddleware(inner_app)
        await middleware({"type": "websocket", "path": "/ws"}, None, None)
        assert called is True
        assert received_scope["type"] == "websocket"


# ---------------------------------------------------------------------------
# 9. TokenStore full lifecycle integration
# ---------------------------------------------------------------------------


class TestTokenStoreLifecycle:
    """Complete TokenStore lifecycle: issue -> verify -> rotate -> revoke."""

    async def test_full_lifecycle(self, token_store):
        """Issue, use, rotate, use both, revoke, confirm record removed."""
        # Issue
        token1 = token_store.issue("lifecycle-node")
        valid, nid = token_store.verify(token1)
        assert valid is True
        assert nid == "lifecycle-node"

        # Use through middleware
        app = _build_full_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token1}"},
            )
            assert resp.status_code == 200

        # Rotate
        token2 = token_store.issue("lifecycle-node")
        assert token1 != token2

        # Both tokens valid through middleware
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            r1 = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token1}"},
            )
            r2 = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {token2}"},
            )
            assert r1.status_code == 200
            assert r2.status_code == 200

        # Revoke
        token_store.revoke("lifecycle-node")
        nodes = token_store.list_nodes()
        assert all(n["node_id"] != "lifecycle-node" for n in nodes)

    async def test_multiple_nodes_independent(self, token_store):
        """Operations on one node don't affect another."""
        t_a = token_store.issue("node-a")
        t_b = token_store.issue("node-b")

        token_store.revoke("node-a")

        # node-b still in store
        nodes = token_store.list_nodes()
        node_ids = {n["node_id"] for n in nodes}
        assert "node-b" in node_ids
        assert "node-a" not in node_ids

        # node-b token still valid
        valid, nid = token_store.verify(t_b)
        assert valid is True
        assert nid == "node-b"

    async def test_rotation_updates_metadata(self, token_store):
        """Rotation updates rotated_at timestamp in store."""
        token_store.issue("meta-node")
        time.sleep(0.01)  # Ensure timestamp differs
        token_store.issue("meta-node")  # Rotate

        nodes = token_store.list_nodes()
        meta = next(n for n in nodes if n["node_id"] == "meta-node")
        assert meta["rotated_at"] is not None
        assert meta["rotated_at"] > meta["created_at"]
