"""Integration tests: TaskGroup migration — security layer + async patterns.

Validates the security module's integration with the AsyncDatabase concurrent
patterns introduced by the TaskGroup migration:

1. Security module exports: PUBLIC_PATHS, middleware, tokens all importable
2. Concurrent token generation and verification via asyncio.gather
3. TokenStore operations under concurrent async load
4. Auth middleware with concurrent request handling
5. Security layer renumbering (Layer 0 for API Auth) consistency
6. AsyncDatabase + security cross-module workflows
7. Concurrent token verification with error isolation

These tests exercise the security changes made during the TaskGroup migration:
- PUBLIC_PATHS now exported from security.__init__
- Layer numbering updated (Layer 0 = API Auth)
- Middleware + token verification under structured concurrency patterns

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

from __future__ import annotations

import asyncio
import os
import time
from concurrent.futures import ThreadPoolExecutor

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

from greymatter_core.db import AsyncDatabase, SQLiteDatabase
from greymatter_core.knowledge import KnowledgeStore
from greymatter_core.patterns import PatternManager
from greymatter_core.security.middleware import AuthMiddleware, PUBLIC_PATHS, require_auth
from greymatter_core.security.tokens import (
    TokenStore,
    generate_node_token,
    verify_node_token,
)
from greymatter_core.work_items import WorkItemManager


# Use the SAME cluster secret as other auth test files to avoid env var
# collisions when all test files run in the same pytest session.
SECRET = b"test-middleware-secret-32bytes!!"
os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


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


def _build_app() -> FastAPI:
    """FastAPI app with middleware auth and status endpoint."""
    app = FastAPI()
    app.add_middleware(AuthMiddleware)

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

    @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}

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

    @app.get("/api/concurrent")
    async def concurrent(request: Request):
        """Simulates a handler that does concurrent DB queries."""
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        return {"node_id": node_id, "concurrent": True}

    return app


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


@pytest.fixture
def db():
    """In-memory SQLite database."""
    database = SQLiteDatabase(":memory:")
    yield database
    database.close()


@pytest.fixture
def adb(db):
    """AsyncDatabase wrapping the in-memory DB."""
    return AsyncDatabase(db)


@pytest.fixture
def knowledge(db):
    return KnowledgeStore(db)


@pytest.fixture
def patterns(db):
    return PatternManager(db)


@pytest.fixture
def work_items(db):
    return WorkItemManager(db)


@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()


# ===========================================================================
# 1. Security module exports after TaskGroup migration
# ===========================================================================


class TestSecurityModuleExports:
    """Verify security module exports are complete after migration changes."""

    def test_public_paths_importable_from_init(self):
        """PUBLIC_PATHS is importable from greymatter_core.security."""
        from greymatter_core.security import PUBLIC_PATHS as exported_paths
        assert isinstance(exported_paths, frozenset)
        assert "/health" in exported_paths

    def test_middleware_importable_from_init(self):
        """AuthMiddleware and require_auth importable from security package."""
        from greymatter_core.security import AuthMiddleware, require_auth
        assert AuthMiddleware is not None
        assert callable(require_auth)

    def test_token_functions_importable_from_init(self):
        """Token functions importable from security package."""
        from greymatter_core.security import (
            generate_node_token,
            verify_node_token,
            TokenStore,
        )
        assert callable(generate_node_token)
        assert callable(verify_node_token)
        assert TokenStore is not None

    def test_encryption_importable_from_init(self):
        """Encryption functions importable from security package."""
        from greymatter_core.security import (
            encrypt_sync_payload,
            decrypt_sync_payload,
            MLKEM_AVAILABLE,
        )
        assert callable(encrypt_sync_payload)
        assert callable(decrypt_sync_payload)
        assert isinstance(MLKEM_AVAILABLE, bool)

    def test_all_exports_listed(self):
        """__all__ includes all expected security exports."""
        from greymatter_core.security import __all__ as exports
        expected = {
            "generate_node_token",
            "verify_node_token",
            "TokenStore",
            "encrypt_sync_payload",
            "decrypt_sync_payload",
            "MLKEM_AVAILABLE",
        }
        assert expected.issubset(set(exports))
        # Middleware exports are conditional on FastAPI availability
        assert "require_auth" in exports
        assert "AuthMiddleware" in exports
        assert "PUBLIC_PATHS" in exports

    def test_security_docstring_layer_zero(self):
        """Security module docstring references Layer 0 for API Auth."""
        import greymatter_core.security as sec
        assert "Layer 0" in sec.__doc__
        assert "API Authentication" in sec.__doc__

    def test_tokens_docstring_layer_zero(self):
        """Tokens module docstring references Layer 0."""
        import greymatter_core.security.tokens as tokens
        assert "Layer 0" in tokens.__doc__


# ===========================================================================
# 2. Concurrent token generation and verification
# ===========================================================================


class TestConcurrentTokenOperations:
    """Token generation and verification under concurrent async patterns."""

    async def test_concurrent_token_generation(self):
        """Generate multiple tokens concurrently via run_in_executor."""
        loop = asyncio.get_running_loop()
        nodes = [f"node-{i}" for i in range(10)]

        tokens = await asyncio.gather(
            *[
                loop.run_in_executor(
                    None, generate_node_token, node, SECRET
                )
                for node in nodes
            ]
        )

        assert len(tokens) == 10
        assert len(set(tokens)) == 10  # All unique

    async def test_concurrent_token_verification(self):
        """Verify multiple tokens concurrently via run_in_executor."""
        loop = asyncio.get_running_loop()
        nodes = [f"verify-{i}" for i in range(10)]
        tokens = [generate_node_token(n, secret=SECRET) for n in nodes]

        results = await asyncio.gather(
            *[
                loop.run_in_executor(
                    None, verify_node_token, t, SECRET
                )
                for t in tokens
            ]
        )

        for i, (valid, node_id) in enumerate(results):
            assert valid is True
            assert node_id == nodes[i]

    async def test_concurrent_verify_mixed_valid_invalid(self):
        """Concurrent verification of mixed valid/invalid tokens."""
        loop = asyncio.get_running_loop()
        alt_secret = b"wrong-secret-for-testing-xxxxx!!"
        valid_tokens = [generate_node_token(f"good-{i}", secret=SECRET) for i in range(5)]
        invalid_tokens = [generate_node_token(f"bad-{i}", secret=alt_secret) for i in range(5)]

        all_tokens = valid_tokens + invalid_tokens
        results = await asyncio.gather(
            *[
                loop.run_in_executor(None, verify_node_token, t, SECRET)
                for t in all_tokens
            ]
        )

        for i, (valid, node_id) in enumerate(results):
            if i < 5:
                assert valid is True
                assert node_id == f"good-{i}"
            else:
                assert valid is False
                assert node_id == ""

    async def test_generate_and_verify_round_trip(self):
        """Generate then verify in a concurrent pipeline."""
        loop = asyncio.get_running_loop()
        nodes = [f"roundtrip-{i}" for i in range(5)]

        # Generate concurrently
        tokens = await asyncio.gather(
            *[
                loop.run_in_executor(None, generate_node_token, n, SECRET)
                for n in nodes
            ]
        )

        # Verify concurrently
        results = await asyncio.gather(
            *[
                loop.run_in_executor(None, verify_node_token, t, SECRET)
                for t in tokens
            ]
        )

        assert all(valid for valid, _ in results)
        assert [nid for _, nid in results] == nodes


# ===========================================================================
# 3. TokenStore under concurrent async load
# ===========================================================================


class TestTokenStoreConcurrentOperations:
    """TokenStore issue/verify/revoke with concurrent async patterns."""

    def test_batch_token_issue_then_concurrent_verify(self, token_store):
        """Issue tokens synchronously, then verify concurrently via HMAC.

        TokenStore.issue() is sync-only (standard sqlite3 connection).
        Verification is stateless HMAC and fully thread-safe.
        """
        nodes = [f"batch-{i}" for i in range(10)]
        tokens = [token_store.issue(node) for node in nodes]

        assert len(tokens) == 10
        assert len(set(tokens)) == 10
        stored = token_store.list_nodes()
        stored_ids = {n["node_id"] for n in stored}
        assert all(n in stored_ids for n in nodes)

        # Verify all tokens — stateless HMAC is thread-safe
        for token, expected_node in zip(tokens, nodes):
            valid, node_id = verify_node_token(token, secret=SECRET)
            assert valid is True
            assert node_id == expected_node

    async def test_concurrent_hmac_verify_after_issue(self, token_store):
        """Issue tokens via store, verify concurrently via stateless HMAC."""
        loop = asyncio.get_running_loop()
        nodes = [f"verify-store-{i}" for i in range(5)]
        tokens = {n: token_store.issue(n) for n in nodes}

        # Stateless HMAC verify is thread-safe (no SQLite access)
        results = await asyncio.gather(
            *[
                loop.run_in_executor(None, verify_node_token, t, SECRET)
                for t in tokens.values()
            ]
        )

        for (valid, node_id), expected_node in zip(results, nodes):
            assert valid is True
            assert node_id == expected_node

    async def test_issue_rotate_verify_concurrently(self, token_store):
        """Issue, rotate, then verify both tokens via concurrent HMAC check."""
        loop = asyncio.get_running_loop()

        # Issue and rotate (sync — TokenStore is not thread-safe)
        t1 = token_store.issue("rotate-test")
        t2 = token_store.issue("rotate-test")
        assert t1 != t2

        # Concurrent stateless HMAC verification
        r1, r2 = await asyncio.gather(
            loop.run_in_executor(None, verify_node_token, t1, SECRET),
            loop.run_in_executor(None, verify_node_token, t2, SECRET),
        )

        assert r1[0] is True and r1[1] == "rotate-test"
        assert r2[0] is True and r2[1] == "rotate-test"


# ===========================================================================
# 4. Auth middleware with concurrent request handling
# ===========================================================================


class TestMiddlewareConcurrentRequests:
    """Auth middleware correctness under concurrent request patterns."""

    async def test_concurrent_authenticated_requests(self):
        """Multiple authenticated requests handled concurrently without mixing."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        nodes = [f"concurrent-{i}" for i in range(8)]
        tokens = {n: _token(n) for n in nodes}

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

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

    async def test_concurrent_public_and_protected_requests(self):
        """Mix of public and protected requests handled concurrently."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        token = _token("mixed-node")

        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = [
                c.get("/health"),
                c.get("/api/status", headers={"Authorization": f"Bearer {token}"}),
                c.get("/health"),
                c.get("/api/identity", headers={"Authorization": f"Bearer {token}"}),
                c.get("/api/status"),  # No auth — should fail
            ]
            results = await asyncio.gather(*tasks)

            assert results[0].status_code == 200  # Public
            assert results[1].status_code == 200  # Authenticated
            assert results[2].status_code == 200  # Public
            assert results[3].status_code == 200  # Authenticated via dep
            assert results[4].status_code == 401  # Missing auth

    async def test_middleware_public_paths_match_exported_set(self):
        """AuthMiddleware defaults match the PUBLIC_PATHS exported from security."""
        from greymatter_core.security import PUBLIC_PATHS as exported
        from greymatter_core.security.middleware import PUBLIC_PATHS as middleware_paths
        assert exported is middleware_paths

    async def test_concurrent_requests_identity_isolation(self):
        """Node identity doesn't leak between concurrent requests."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)

        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = []
            for i in range(20):
                node_id = f"isolated-{i}"
                tasks.append(
                    c.get(
                        "/api/identity",
                        headers={"Authorization": f"Bearer {_token(node_id)}"},
                    )
                )
            responses = await asyncio.gather(*tasks)

            for i, resp in enumerate(responses):
                assert resp.status_code == 200
                assert resp.json()["node_id"] == f"isolated-{i}"


# ===========================================================================
# 5. AsyncDatabase + security cross-module workflows
# ===========================================================================


class TestSecurityAsyncDatabaseCrossModule:
    """Cross-module workflows combining security tokens with async DB queries."""

    async def test_auth_then_concurrent_db_queries(self, adb, db, knowledge, patterns):
        """Authenticated request pattern: verify token, then fan-out DB queries."""
        # Simulate auth check
        token = generate_node_token("query-node", secret=SECRET)
        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True

        # Create data as the authenticated node would
        knowledge.create(
            entry_type="expertise",
            title="Structured concurrency",
            content="Use TaskGroup for concurrent operations",
            domain="async",
        )
        patterns.create(
            type="convention",
            domain="async",
            title="Gather fan-out",
            observation="Use asyncio.gather for parallel read queries",
        )

        # Fan-out concurrent queries (the gather pattern)
        know_count, pat_count = await asyncio.gather(
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM knowledge_entries WHERE pruned_at IS NULL"
            ),
            adb.execute_one("SELECT COUNT(*) as cnt FROM patterns"),
        )

        assert know_count["cnt"] >= 1
        assert pat_count["cnt"] >= 1

    async def test_token_store_with_async_knowledge_pipeline(
        self, token_store, adb, db, knowledge
    ):
        """TokenStore issue combined with async knowledge queries."""
        # Issue token for a node
        token = token_store.issue("knowledge-node")
        valid, node_id = token_store.verify(token)
        assert valid is True

        # Create knowledge as the authenticated node
        knowledge.create(
            entry_type="expertise",
            title="Token-based auth",
            content="HMAC-signed bearer tokens for stateless verification",
            domain="security",
        )
        knowledge.create(
            entry_type="gotcha",
            title="Dot in node_id breaks token",
            content="Node IDs must not contain dots as they break the 4-part token format",
            domain="security",
        )

        # Concurrent knowledge queries
        total, security_count = await asyncio.gather(
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM knowledge_entries WHERE pruned_at IS NULL"
            ),
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM knowledge_entries "
                "WHERE domain = 'security' AND pruned_at IS NULL"
            ),
        )

        assert total["cnt"] == 2
        assert security_count["cnt"] == 2

    async def test_concurrent_status_with_token_verification(
        self, adb, db, knowledge, patterns, work_items
    ):
        """Full status fan-out after token verification — mirrors real usage."""
        # Auth check
        token = generate_node_token("status-node", secret=SECRET)
        valid, _ = verify_node_token(token, secret=SECRET)
        assert valid is True

        # Populate data
        knowledge.create(
            entry_type="expertise",
            title="AsyncDatabase",
            content="Thread-pool async wrapper for SQLite",
            domain="async",
        )
        patterns.create(
            type="convention",
            domain="async",
            title="Gather for reads",
            observation="Use gather for concurrent read-only queries",
        )
        work_items.create(title="TaskGroup migration", priority=75)

        # 5-query gather (subset of the 9-query gm_status pattern)
        (
            know_count,
            pat_count,
            work_total,
            work_queued,
            obs_count,
        ) = await asyncio.gather(
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM knowledge_entries WHERE pruned_at IS NULL"
            ),
            adb.execute_one("SELECT COUNT(*) as cnt FROM patterns"),
            adb.execute_one("SELECT COUNT(*) as cnt FROM work_items"),
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM work_items WHERE status = 'queued'"
            ),
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM observations WHERE pruned = 0"
            ),
        )

        assert know_count["cnt"] >= 1
        assert pat_count["cnt"] >= 1
        assert work_total["cnt"] >= 1
        assert work_queued["cnt"] >= 1
        assert obs_count["cnt"] == 0


# ===========================================================================
# 6. Error isolation in concurrent security operations
# ===========================================================================


class TestSecurityErrorIsolation:
    """Errors in one security operation don't corrupt concurrent operations."""

    async def test_invalid_token_doesnt_affect_valid_verification(self):
        """gather with return_exceptions isolates invalid token errors."""
        loop = asyncio.get_running_loop()
        valid_token = generate_node_token("good-node", secret=SECRET)
        invalid_token = "bad.token.only.three"

        results = await asyncio.gather(
            loop.run_in_executor(None, verify_node_token, valid_token, SECRET),
            loop.run_in_executor(None, verify_node_token, invalid_token, SECRET),
            return_exceptions=True,
        )

        # Both return tuples (not exceptions), even the invalid one
        assert isinstance(results[0], tuple)
        assert results[0] == (True, "good-node")
        assert isinstance(results[1], tuple)
        assert results[1] == (False, "")

    async def test_middleware_error_isolation_concurrent(self):
        """Invalid request in concurrent batch doesn't affect valid ones."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        valid_token = _token("ok-node")

        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = [
                c.get("/api/status", headers={"Authorization": f"Bearer {valid_token}"}),
                c.get("/api/status", headers={"Authorization": "Bearer invalid.sig"}),
                c.get("/api/status", headers={"Authorization": f"Bearer {valid_token}"}),
                c.get("/api/status"),  # No auth
                c.get("/api/status", headers={"Authorization": f"Bearer {valid_token}"}),
            ]
            results = await asyncio.gather(*tasks)

            assert results[0].status_code == 200
            assert results[0].json()["node_id"] == "ok-node"
            assert results[1].status_code == 401
            assert results[2].status_code == 200
            assert results[3].status_code == 401
            assert results[4].status_code == 200

    async def test_auth_and_db_error_isolation(self, adb, db):
        """Auth verification and DB query errors are isolated in gather."""
        loop = asyncio.get_running_loop()
        token = generate_node_token("iso-node", secret=SECRET)

        results = await asyncio.gather(
            loop.run_in_executor(None, verify_node_token, token, SECRET),
            adb.execute_one("SELECT COUNT(*) as cnt FROM patterns"),
            adb.execute_one("SELECT COUNT(*) as cnt FROM nonexistent_table_xyz"),
            return_exceptions=True,
        )

        # Auth verification succeeds
        assert isinstance(results[0], tuple)
        assert results[0] == (True, "iso-node")
        # DB query succeeds
        assert isinstance(results[1], dict)
        assert results[1]["cnt"] == 0
        # Invalid table query fails
        assert isinstance(results[2], Exception)


# ===========================================================================
# 7. Token format and TTL under async patterns
# ===========================================================================


class TestTokenFormatAsync:
    """Token format invariants hold under async generation patterns."""

    async def test_all_tokens_four_parts_concurrent(self):
        """All concurrently generated tokens maintain the 4-part format."""
        loop = asyncio.get_running_loop()
        tokens = await asyncio.gather(
            *[
                loop.run_in_executor(None, generate_node_token, f"fmt-{i}", SECRET)
                for i in range(20)
            ]
        )

        for t in tokens:
            parts = t.split(".")
            assert len(parts) == 4
            assert len(parts[2]) == 16  # Nonce is 8 bytes = 16 hex chars
            assert len(parts[3]) == 64  # HMAC-SHA256 = 64 hex chars

    async def test_nonce_uniqueness_concurrent(self):
        """Nonces are unique even when tokens are generated concurrently."""
        loop = asyncio.get_running_loop()
        tokens = await asyncio.gather(
            *[
                loop.run_in_executor(
                    None, generate_node_token, "same-node", SECRET
                )
                for _ in range(50)
            ]
        )

        nonces = {t.split(".")[2] for t in tokens}
        assert len(nonces) == 50

    async def test_ttl_verification_concurrent(self):
        """TTL checking works correctly under concurrent verification."""
        loop = asyncio.get_running_loop()
        token = generate_node_token("ttl-node", secret=SECRET)

        # All should be valid with default TTL
        results = await asyncio.gather(
            *[
                loop.run_in_executor(None, verify_node_token, token, SECRET)
                for _ in range(10)
            ]
        )
        assert all(valid for valid, _ in results)

        # All should be expired with max_age=0
        expired_results = await asyncio.gather(
            *[
                loop.run_in_executor(
                    None, verify_node_token, token, SECRET, 0
                )
                for _ in range(10)
            ]
        )
        assert all(not valid for valid, _ in expired_results)


# ===========================================================================
# 8. Full stack: TokenStore + Middleware + AsyncDB concurrent workflow
# ===========================================================================


class TestFullStackConcurrentWorkflow:
    """End-to-end: issue token, authenticate via middleware, do concurrent DB work."""

    async def test_complete_authenticated_workflow(
        self, token_store, adb, db, knowledge, patterns, work_items
    ):
        """Full lifecycle: issue → authenticate → concurrent queries → verify."""
        # 1. Issue token via TokenStore
        token = token_store.issue("workflow-node")
        valid, node_id = token_store.verify(token)
        assert valid is True
        assert node_id == "workflow-node"

        # 2. Authenticate via middleware
        app = _build_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
            assert resp.json()["node_id"] == "workflow-node"

        # 3. Create data as the authenticated agent
        knowledge.create(
            entry_type="expertise",
            title="TaskGroup migration complete",
            content="AsyncDatabase concurrent patterns verified",
            domain="async",
        )
        pat = patterns.create(
            type="convention",
            domain="security",
            title="Token before query",
            observation="Always verify auth token before running DB queries",
        )
        patterns.add_evidence(pat["id"], "test-session")
        work = work_items.create(
            title="Verify TaskGroup migration", priority=80
        )
        work_items.update_status(work["id"], "in_progress")

        # 4. Run concurrent status queries
        know_count, pat_count, work_total, work_ip = await asyncio.gather(
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM knowledge_entries WHERE pruned_at IS NULL"
            ),
            adb.execute_one("SELECT COUNT(*) as cnt FROM patterns"),
            adb.execute_one("SELECT COUNT(*) as cnt FROM work_items"),
            adb.execute_one(
                "SELECT COUNT(*) as cnt FROM work_items WHERE status = 'in_progress'"
            ),
        )

        assert know_count["cnt"] >= 1
        assert pat_count["cnt"] >= 1
        assert work_total["cnt"] >= 1
        assert work_ip["cnt"] >= 1

        # 5. Verify token still valid after workflow
        still_valid, _ = verify_node_token(token, secret=SECRET)
        assert still_valid is True

    async def test_multi_node_concurrent_full_stack(self, token_store):
        """Multiple nodes authenticate and make concurrent requests."""
        nodes = ["worker-1", "worker-2", "reviewer-1"]
        tokens = {n: token_store.issue(n) for n in nodes}

        app = _build_app()
        transport = httpx.ASGITransport(app=app)

        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            # All nodes make concurrent authenticated requests
            tasks = [
                c.get(
                    "/api/concurrent",
                    headers={"Authorization": f"Bearer {tokens[n]}"},
                )
                for n in nodes
            ]
            responses = await asyncio.gather(*tasks)

            for i, resp in enumerate(responses):
                assert resp.status_code == 200
                assert resp.json()["node_id"] == nodes[i]
                assert resp.json()["concurrent"] is True

        # All nodes in store
        stored = token_store.list_nodes()
        stored_ids = {n["node_id"] for n in stored}
        assert set(nodes) <= stored_ids
