"""Integration tests: Auth Middleware — write integration (wi-c4de6a35).

Covers gaps not addressed by existing test suites:

1. Secret management: file-based auto-generation, env var override, cross-secret isolation
2. Token format invariants: 4-part structure, HMAC length, nonce uniqueness
3. TTL boundary testing: exact expiry edge, custom TTL via env var
4. Request variations: query params, content types, custom headers through middleware
5. Multi-TokenStore instances: DB isolation, concurrent store access
6. Middleware initialization edge cases: empty public paths, None defaults

Uses httpx.ASGITransport — no network I/O required.

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

from __future__ import annotations

import asyncio
import os
import stat
import time
from pathlib import Path
from unittest.mock import patch

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,
    _get_cluster_secret,
    generate_node_token,
    verify_node_token,
)

# Deterministic secret for test isolation
SECRET = b"test-middleware-secret-32bytes!!"
os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()

ALT_SECRET = b"alternate-cluster-secret-xxxxx!!"


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


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


def _build_app() -> FastAPI:
    """Standard test app with public health + protected API routes."""
    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/search")
    async def search(request: Request, q: str = "", limit: int = 10):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        return {"query": q, "limit": limit, "caller": node_id}

    @app.post("/api/upload")
    async def upload(request: Request):
        node_id = request.scope.get("state", {}).get("node_id", "unknown")
        content_type = request.headers.get("content-type", "unknown")
        body = await request.body()
        return {
            "caller": node_id,
            "content_type": content_type,
            "size": len(body),
        }

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

    return app


def _build_empty_public_app() -> FastAPI:
    """App with a non-default public path set (only /ready is public).

    Note: AuthMiddleware uses `public_paths or PUBLIC_PATHS`, so passing
    an empty set() is falsy and falls back to defaults. To restrict public
    paths, pass a non-empty set with only the paths you want public.
    """
    app = FastAPI()
    app.add_middleware(AuthMiddleware, public_paths={"/ready"})

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

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

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

    return app


# ===========================================================================
# 1. Secret management integration
# ===========================================================================


class TestSecretFileManagement:
    """Cluster secret auto-generation and file-based loading."""

    def test_secret_auto_generated_on_first_use(self, tmp_path: Path):
        """_get_cluster_secret creates a new secret file if none exists."""
        secret_path = tmp_path / "cluster.key"
        with patch.dict(os.environ, {
            "GM_CLUSTER_SECRET": "",
            "GM_CLUSTER_SECRET_PATH": str(secret_path),
        }):
            # Remove env override to trigger file-based path
            del os.environ["GM_CLUSTER_SECRET"]
            secret = _get_cluster_secret()

        assert secret_path.exists()
        assert len(secret) == 64  # os.urandom(64)
        assert secret == secret_path.read_bytes()

    def test_secret_file_permissions(self, tmp_path: Path):
        """Auto-generated secret file has 0o600 permissions."""
        secret_path = tmp_path / "cluster.key"
        with patch.dict(os.environ, {
            "GM_CLUSTER_SECRET_PATH": str(secret_path),
        }):
            del os.environ["GM_CLUSTER_SECRET"]
            _get_cluster_secret()

        mode = secret_path.stat().st_mode & 0o777
        assert mode == 0o600, f"Expected 0o600, got {oct(mode)}"
        # Restore env for other tests
        os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()

    def test_secret_file_reused_on_subsequent_calls(self, tmp_path: Path):
        """Same secret returned on repeated calls without regeneration."""
        secret_path = tmp_path / "cluster.key"
        with patch.dict(os.environ, {
            "GM_CLUSTER_SECRET_PATH": str(secret_path),
        }):
            del os.environ["GM_CLUSTER_SECRET"]
            first = _get_cluster_secret()
            second = _get_cluster_secret()

        assert first == second
        os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()

    def test_env_var_overrides_file(self, tmp_path: Path):
        """GM_CLUSTER_SECRET env var takes priority over file."""
        secret_path = tmp_path / "cluster.key"
        secret_path.write_bytes(b"file-secret-should-not-be-used!!")
        with patch.dict(os.environ, {
            "GM_CLUSTER_SECRET": "env-secret-wins",
            "GM_CLUSTER_SECRET_PATH": str(secret_path),
        }):
            secret = _get_cluster_secret()

        assert secret == b"env-secret-wins"
        os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()

    def test_secret_directory_created_if_missing(self, tmp_path: Path):
        """Parent directories are created if they don't exist."""
        secret_path = tmp_path / "nested" / "deep" / "cluster.key"
        with patch.dict(os.environ, {
            "GM_CLUSTER_SECRET_PATH": str(secret_path),
        }):
            del os.environ["GM_CLUSTER_SECRET"]
            _get_cluster_secret()

        assert secret_path.exists()
        os.environ["GM_CLUSTER_SECRET"] = SECRET.decode()


class TestCrossSecretIsolation:
    """Tokens signed with one secret are rejected by another."""

    async def test_wrong_secret_rejected_by_middleware(self):
        """Token signed with ALT_SECRET fails middleware using SECRET."""
        wrong_token = _token("intruder", secret=ALT_SECRET)
        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/data",
                headers={"Authorization": f"Bearer {wrong_token}"},
            )
            assert resp.status_code == 401

    async def test_correct_secret_accepted_after_wrong_rejected(self):
        """Rejection of wrong-secret token doesn't affect valid tokens."""
        wrong_token = _token("intruder", secret=ALT_SECRET)
        valid_token = _token("legit-node", secret=SECRET)

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

            r2 = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {valid_token}"},
            )
            assert r2.status_code == 200
            assert r2.json()["node_id"] == "legit-node"

    async def test_same_node_id_different_secrets(self):
        """Same node_id but different signing secrets — only correct one passes."""
        node = "shared-name"
        good = _token(node, secret=SECRET)
        bad = _token(node, secret=ALT_SECRET)

        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            rg = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {good}"},
            )
            rb = await c.get(
                "/api/data",
                headers={"Authorization": f"Bearer {bad}"},
            )
            assert rg.status_code == 200
            assert rb.status_code == 401

    def test_cross_secret_verify_function(self):
        """verify_node_token rejects tokens from different secrets."""
        token = generate_node_token("node-x", secret=SECRET)
        valid, _ = verify_node_token(token, secret=ALT_SECRET)
        assert valid is False

    def test_same_secret_verify_function(self):
        """verify_node_token accepts tokens from matching secret."""
        token = generate_node_token("node-x", secret=SECRET)
        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert node_id == "node-x"


# ===========================================================================
# 2. Token format invariants and nonce uniqueness
# ===========================================================================


class TestTokenFormatInvariants:
    """Token structure meets the documented format specification."""

    def test_token_has_four_parts(self):
        """Token is 4 dot-separated parts: node_id.timestamp.nonce.signature."""
        token = _token("format-node")
        parts = token.split(".")
        assert len(parts) == 4

    def test_node_id_preserved_in_token(self):
        """First part of token matches the node_id."""
        token = _token("my-node")
        assert token.split(".")[0] == "my-node"

    def test_timestamp_is_integer_string(self):
        """Second part is a valid integer timestamp."""
        token = _token("ts-node")
        ts_str = token.split(".")[1]
        ts = int(ts_str)
        # Timestamp should be recent (within last 10 seconds)
        assert abs(time.time() - ts) < 10

    def test_nonce_is_hex_string(self):
        """Third part is a 16-char hex nonce (8 bytes)."""
        token = _token("nonce-node")
        nonce = token.split(".")[2]
        assert len(nonce) == 16
        int(nonce, 16)  # Should not raise

    def test_signature_is_64_char_hex(self):
        """Fourth part is a 64-char hex HMAC-SHA256 signature."""
        token = _token("sig-node")
        sig = token.split(".")[3]
        assert len(sig) == 64
        int(sig, 16)  # Should not raise

    def test_nonce_unique_across_tokens(self):
        """Each token has a unique nonce (no collisions in 100 tokens)."""
        nonces = set()
        for i in range(100):
            token = _token(f"node-{i}")
            nonce = token.split(".")[2]
            nonces.add(nonce)
        assert len(nonces) == 100

    def test_same_node_different_nonces(self):
        """Same node_id generates tokens with different nonces."""
        t1 = _token("same-node")
        t2 = _token("same-node")
        assert t1.split(".")[2] != t2.split(".")[2]
        assert t1.split(".")[3] != t2.split(".")[3]

    def test_empty_node_id_produces_valid_format(self):
        """Empty string node_id still produces a 4-part token."""
        token = _token("")
        parts = token.split(".")
        assert len(parts) == 4
        assert parts[0] == ""

    def test_empty_node_id_verifies(self):
        """Empty node_id token can be verified (no special casing)."""
        token = _token("")
        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert node_id == ""


# ===========================================================================
# 3. TTL boundary and time-based expiry
# ===========================================================================


class TestTTLBoundary:
    """Token expiry at and around the TTL boundary."""

    def test_token_valid_within_ttl(self):
        """Token generated now is valid with default TTL."""
        token = _token("ttl-node")
        valid, _ = verify_node_token(token, secret=SECRET)
        assert valid is True

    def test_token_expired_with_zero_ttl(self):
        """max_age=0 rejects any token (immediately expired)."""
        token = _token("instant-expire")
        valid, _ = verify_node_token(token, secret=SECRET, max_age=0)
        assert valid is False

    def test_token_valid_with_large_ttl(self):
        """Token valid when max_age is very large."""
        token = _token("long-live")
        valid, _ = verify_node_token(token, secret=SECRET, max_age=999999)
        assert valid is True

    def test_token_expired_with_tiny_ttl_after_sleep(self):
        """Token expires after sleeping past a 1-second TTL."""
        token = _token("sleepy-node")
        time.sleep(1.1)
        valid, _ = verify_node_token(token, secret=SECRET, max_age=1)
        assert valid is False

    def test_custom_ttl_via_env_var(self, monkeypatch):
        """GM_TOKEN_TTL env var is respected by verify_node_token default."""
        # Generate token with known timestamp
        token = _token("env-ttl-node")
        # Token should be valid with default TTL (86400)
        valid, _ = verify_node_token(token, secret=SECRET)
        assert valid is True

    def test_future_timestamp_treated_as_valid(self):
        """Token with future timestamp is valid (age is negative, < max_age)."""
        # Manually construct a token with a future timestamp
        future_ts = str(int(time.time()) + 3600)
        nonce = os.urandom(8).hex()
        payload = f"future-node.{future_ts}.{nonce}"
        import hashlib
        import hmac as hmac_mod
        sig = hmac_mod.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()
        token = f"{payload}.{sig}"

        valid, node_id = verify_node_token(token, secret=SECRET)
        assert valid is True
        assert node_id == "future-node"


# ===========================================================================
# 4. Request variations through middleware
# ===========================================================================


class TestQueryParamsThroughAuth:
    """Query parameters survive auth middleware unchanged."""

    async def test_query_params_preserved(self):
        """Query string parameters reach the handler after auth."""
        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/search",
                params={"q": "hello world", "limit": "5"},
                headers={"Authorization": f"Bearer {_token('searcher')}"},
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["query"] == "hello world"
            assert body["limit"] == 5
            assert body["caller"] == "searcher"

    async def test_empty_query_params(self):
        """Empty query parameters work through auth."""
        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/search",
                headers={"Authorization": f"Bearer {_token('empty-q')}"},
            )
            assert resp.status_code == 200
            assert resp.json()["query"] == ""

    async def test_special_chars_in_query(self):
        """Special characters in query params pass through auth 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/search",
                params={"q": "a=b&c=d<script>"},
                headers={"Authorization": f"Bearer {_token('special')}"},
            )
            assert resp.status_code == 200
            assert resp.json()["query"] == "a=b&c=d<script>"


class TestContentTypeThroughAuth:
    """Different content types pass through auth middleware correctly."""

    async def test_json_content_type(self):
        """application/json body passes through auth middleware."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.post(
                "/api/upload",
                headers={
                    "Authorization": f"Bearer {_token('json-sender')}",
                    "Content-Type": "application/json",
                },
                content=b'{"key": "value"}',
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["caller"] == "json-sender"
            assert body["size"] == 16

    async def test_plain_text_content_type(self):
        """text/plain body passes through auth middleware."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.post(
                "/api/upload",
                headers={
                    "Authorization": f"Bearer {_token('text-sender')}",
                    "Content-Type": "text/plain",
                },
                content=b"Hello, World!",
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["caller"] == "text-sender"
            assert body["content_type"] == "text/plain"
            assert body["size"] == 13

    async def test_binary_content_through_auth(self):
        """Binary content passes through auth middleware intact."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        binary_data = bytes(range(256))
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.post(
                "/api/upload",
                headers={
                    "Authorization": f"Bearer {_token('binary-sender')}",
                    "Content-Type": "application/octet-stream",
                },
                content=binary_data,
            )
            assert resp.status_code == 200
            assert resp.json()["size"] == 256

    async def test_empty_body_through_auth(self):
        """Empty POST body passes through auth middleware."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.post(
                "/api/upload",
                headers={"Authorization": f"Bearer {_token('empty-sender')}"},
                content=b"",
            )
            assert resp.status_code == 200
            assert resp.json()["size"] == 0


# ===========================================================================
# 5. Multi-TokenStore instances and DB isolation
# ===========================================================================


class TestMultiTokenStoreIsolation:
    """Multiple TokenStore instances operate independently."""

    def test_separate_databases_isolated(self, tmp_path: Path):
        """Tokens in one store don't appear in another."""
        store_a = TokenStore(str(tmp_path / "a.db"))
        store_b = TokenStore(str(tmp_path / "b.db"))

        store_a.issue("node-a")
        store_b.issue("node-b")

        a_nodes = {n["node_id"] for n in store_a.list_nodes()}
        b_nodes = {n["node_id"] for n in store_b.list_nodes()}

        assert "node-a" in a_nodes
        assert "node-b" not in a_nodes
        assert "node-b" in b_nodes
        assert "node-a" not in b_nodes

        store_a.close()
        store_b.close()

    def test_revoke_in_one_store_no_effect_on_other(self, tmp_path: Path):
        """Revoking in store A doesn't affect store B."""
        store_a = TokenStore(str(tmp_path / "a.db"))
        store_b = TokenStore(str(tmp_path / "b.db"))

        store_a.issue("shared-name")
        store_b.issue("shared-name")

        store_a.revoke("shared-name")

        a_nodes = {n["node_id"] for n in store_a.list_nodes()}
        b_nodes = {n["node_id"] for n in store_b.list_nodes()}

        assert "shared-name" not in a_nodes
        assert "shared-name" in b_nodes

        store_a.close()
        store_b.close()

    def test_two_stores_same_db_see_each_others_data(self, tmp_path: Path):
        """Two TokenStore instances on the same DB share state."""
        db_path = str(tmp_path / "shared.db")
        store_1 = TokenStore(db_path)
        store_2 = TokenStore(db_path)

        store_1.issue("node-from-1")

        nodes_in_2 = {n["node_id"] for n in store_2.list_nodes()}
        assert "node-from-1" in nodes_in_2

        store_1.close()
        store_2.close()

    def test_store_issued_tokens_are_hmac_valid(self, tmp_path: Path):
        """All tokens from TokenStore pass stateless HMAC verification."""
        store = TokenStore(str(tmp_path / "tokens.db"))
        nodes = [f"node-{i}" for i in range(10)]
        tokens = {n: store.issue(n) for n in nodes}

        for node_id, token in tokens.items():
            valid, nid = verify_node_token(token, secret=SECRET)
            assert valid is True
            assert nid == node_id

        store.close()

    def test_store_rotation_preserves_previous(self, tmp_path: Path):
        """After rotation, store tracks the previous token."""
        store = TokenStore(str(tmp_path / "tokens.db"))
        t1 = store.issue("rotate-me")
        t2 = store.issue("rotate-me")

        assert t1 != t2
        # Both pass HMAC (stateless check)
        assert verify_node_token(t1, secret=SECRET)[0] is True
        assert verify_node_token(t2, secret=SECRET)[0] is True

        store.close()

    async def test_store_tokens_work_through_middleware(self, tmp_path: Path):
        """TokenStore-issued tokens authenticate through full middleware stack."""
        store = TokenStore(str(tmp_path / "tokens.db"))
        token = store.issue("store-middleware-node")

        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"] == "store-middleware-node"

        store.close()


# ===========================================================================
# 6. Middleware initialization edge cases
# ===========================================================================


class TestMiddlewareInitEdgeCases:
    """AuthMiddleware initialization with unusual configurations."""

    async def test_custom_public_paths_health_requires_auth(self):
        """With custom public_paths={"/ready"}, /health requires auth."""
        app = _build_empty_public_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 == 401

    async def test_custom_public_paths_ready_is_public(self):
        """Custom public path /ready is accessible without auth."""
        app = _build_empty_public_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/ready")
            assert resp.status_code == 200

    async def test_custom_public_paths_auth_still_works(self):
        """With custom public_paths, valid tokens still authenticate."""
        app = _build_empty_public_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/health",
                headers={"Authorization": f"Bearer {_token('auth-health')}"},
            )
            assert resp.status_code == 200

    async def test_empty_set_falls_back_to_defaults(self):
        """Empty set() is falsy, so AuthMiddleware falls back to PUBLIC_PATHS.

        This documents a gotcha: `set()` as public_paths doesn't mean
        'no public paths' — it means 'use defaults' because of `or` semantics.
        """
        app = FastAPI()
        app.add_middleware(AuthMiddleware, public_paths=set())

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

        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/health")
            # /health IS accessible because empty set falls back to PUBLIC_PATHS
            assert resp.status_code == 200

    async def test_none_defaults_to_standard_public_paths(self):
        """Passing None for public_paths uses the default PUBLIC_PATHS."""
        app = FastAPI()
        app.add_middleware(AuthMiddleware, public_paths=None)

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

        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


# ===========================================================================
# 7. Concurrent cross-secret stress
# ===========================================================================


class TestConcurrentCrossSecretStress:
    """Concurrent requests mixing valid and cross-secret tokens."""

    async def test_mixed_secrets_concurrent(self):
        """Concurrent batch: valid-secret and wrong-secret tokens handled correctly."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        valid_tokens = [_token(f"good-{i}", secret=SECRET) for i in range(5)]
        bad_tokens = [_token(f"bad-{i}", secret=ALT_SECRET) for i in range(5)]

        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            tasks = []
            expected = []
            for i in range(5):
                tasks.append(c.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {valid_tokens[i]}"},
                ))
                expected.append(200)
                tasks.append(c.get(
                    "/api/data",
                    headers={"Authorization": f"Bearer {bad_tokens[i]}"},
                ))
                expected.append(401)

            results = await asyncio.gather(*tasks)
            for i, (r, exp) in enumerate(zip(results, expected)):
                assert r.status_code == exp, (
                    f"Request {i}: expected {exp}, got {r.status_code}"
                )

    async def test_rapid_alternating_good_bad_tokens(self):
        """Rapidly alternating valid/invalid tokens — no state leakage."""
        app = _build_app()
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            for i in range(20):
                if i % 2 == 0:
                    token = _token(f"good-{i}", secret=SECRET)
                    resp = await c.get(
                        "/api/data",
                        headers={"Authorization": f"Bearer {token}"},
                    )
                    assert resp.status_code == 200
                    assert resp.json()["node_id"] == f"good-{i}"
                else:
                    token = _token(f"bad-{i}", secret=ALT_SECRET)
                    resp = await c.get(
                        "/api/data",
                        headers={"Authorization": f"Bearer {token}"},
                    )
                    assert resp.status_code == 401


# ===========================================================================
# 8. require_auth dependency edge cases
# ===========================================================================


class TestRequireAuthDependencyEdgeCases:
    """Edge cases for the require_auth FastAPI dependency."""

    async def test_require_auth_sets_request_state(self):
        """require_auth injects node_id into request.state."""
        app = FastAPI()

        @app.get("/check-state")
        async def check_state(
            request: Request,
            node_id: str = Depends(require_auth),
        ):
            return {
                "dep_node_id": node_id,
                "state_node_id": getattr(request.state, "node_id", "missing"),
            }

        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/check-state",
                headers={"Authorization": f"Bearer {_token('state-node')}"},
            )
            assert resp.status_code == 200
            body = resp.json()
            assert body["dep_node_id"] == "state-node"
            assert body["state_node_id"] == "state-node"

    async def test_require_auth_returns_401_not_403_for_missing(self):
        """Missing credentials return 401, not 403."""
        app = FastAPI()

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

        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/protected")
            assert resp.status_code == 401
            assert "Missing authorization header" in resp.json()["detail"]

    async def test_require_auth_www_authenticate_header(self):
        """401 response from require_auth includes WWW-Authenticate: Bearer."""
        app = FastAPI()

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

        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get("/protected")
            assert resp.headers.get("www-authenticate") == "Bearer"

    async def test_require_auth_with_invalid_token_format(self):
        """require_auth rejects token with wrong number of parts."""
        app = FastAPI()

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

        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
            resp = await c.get(
                "/protected",
                headers={"Authorization": "Bearer only.two.parts"},
            )
            assert resp.status_code == 401
            assert "Invalid or expired" in resp.json()["detail"]
