"""Tests for airflow/plugins/api/middleware.py.

Uses minimal FastAPI test apps with TestClient to exercise each middleware
in isolation and then the full stack together.
"""

import logging
import uuid
from unittest.mock import MagicMock, patch

import pytest
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from starlette.middleware.base import BaseHTTPMiddleware

# ---------------------------------------------------------------------------
# We cannot import from `api.*` directly because the plugin uses a
# relative-style import path that only resolves inside the Airflow container.
# Instead we add the plugins directory to sys.path so that `api.middleware`
# (and its transitive imports) resolve correctly.
# ---------------------------------------------------------------------------
import sys
from pathlib import Path

_plugins_dir = str(Path(__file__).resolve().parents[3] / "airflow" / "plugins")
if _plugins_dir not in sys.path:
    sys.path.insert(0, _plugins_dir)

from api.middleware import (
    CorrelationIdMiddleware,
    HMACVerificationMiddleware,
    RequestIdMiddleware,
)
from api.responses import error_response


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

def _make_app(*middleware_classes):
    """Return a tiny FastAPI app with the given middleware and a /ping route."""
    app = FastAPI()
    for cls in middleware_classes:
        app.add_middleware(cls)

    @app.get("/ping")
    def ping(request: Request):
        return JSONResponse({
            "request_id": getattr(request.state, "request_id", None),
            "correlation_id": getattr(request.state, "correlation_id", None),
            "hmac_ctx": getattr(request.state, "hmac_ctx", None),
            "auth_method": getattr(request.state, "auth_method", None),
        })

    return app


_VALID_CTX = {"tenant_id": "t1", "node_id": "n1", "user_id": "u1", "role": "admin"}


def _is_uuid4(value: str) -> bool:
    try:
        return uuid.UUID(value).version == 4
    except (ValueError, AttributeError):
        return False


# ===================================================================
# RequestIdMiddleware
# ===================================================================

class TestRequestIdMiddleware:

    def setup_method(self):
        self.app = _make_app(RequestIdMiddleware)
        self.client = TestClient(self.app)

    def test_request_id_generated(self):
        """No X-Request-ID header → middleware generates a UUIDv4."""
        resp = self.client.get("/ping")
        assert resp.status_code == 200
        rid = resp.headers["X-Request-ID"]
        assert _is_uuid4(rid)

    def test_request_id_uses_client_value(self):
        """Client-provided X-Request-ID is echoed back, not replaced."""
        client_id = "client-req-001"
        resp = self.client.get("/ping", headers={"X-Request-ID": client_id})
        assert resp.headers["X-Request-ID"] == client_id

    def test_request_id_on_state(self):
        """request.state.request_id matches the response header."""
        resp = self.client.get("/ping")
        body = resp.json()
        assert body["request_id"] == resp.headers["X-Request-ID"]


# ===================================================================
# CorrelationIdMiddleware
# ===================================================================

class TestCorrelationIdMiddleware:

    def setup_method(self):
        self.app = _make_app(CorrelationIdMiddleware)
        self.client = TestClient(self.app)

    def test_correlation_id_forwarded(self):
        """Upstream X-Correlation-ID is forwarded in the response."""
        cid = "upstream-corr-123"
        resp = self.client.get("/ping", headers={"X-Correlation-ID": cid})
        assert resp.headers["X-Correlation-ID"] == cid

    def test_correlation_id_missing_generates_fallback(self):
        """Missing header → auto-generated UUIDv4."""
        resp = self.client.get("/ping")
        cid = resp.headers["X-Correlation-ID"]
        assert _is_uuid4(cid)

    def test_correlation_id_missing_logs_warning(self, caplog):
        """Missing header → WARNING with request path."""
        with caplog.at_level(logging.WARNING):
            self.client.get("/ping")
        assert any("Missing X-Correlation-ID" in m and "/ping" in m for m in caplog.messages)

    def test_correlation_id_on_state(self):
        """request.state.correlation_id matches response header."""
        cid = "state-test-corr"
        resp = self.client.get("/ping", headers={"X-Correlation-ID": cid})
        body = resp.json()
        assert body["correlation_id"] == cid


# ===================================================================
# HMACVerificationMiddleware
# ===================================================================

class TestHMACVerificationMiddleware:

    def setup_method(self):
        self.app = _make_app(HMACVerificationMiddleware)
        self.client = TestClient(self.app)

    @patch("api.middleware.verify_hmac_headers", return_value=_VALID_CTX)
    def test_hmac_valid_passes(self, mock_verify):
        """Valid HMAC → 200, hmac_ctx populated, auth_method=hmac."""
        resp = self.client.get("/ping")
        assert resp.status_code == 200
        body = resp.json()
        assert body["hmac_ctx"] == _VALID_CTX
        assert body["auth_method"] == "hmac"

    @patch("api.middleware.verify_hmac_headers")
    def test_hmac_invalid_returns_401(self, mock_verify):
        """VerificationError → 401 JSON, handler NOT called."""
        from textiq_hmac import VerificationError
        mock_verify.side_effect = VerificationError("bad sig")
        resp = self.client.get("/ping")
        assert resp.status_code == 401
        err = resp.json()["error"]
        assert err["code"] == "UNAUTHORIZED"
        assert "bad sig" in err["message"]

    @patch("api.middleware.verify_hmac_headers")
    def test_hmac_key_ring_error_returns_500(self, mock_verify):
        """Generic exception (e.g. key ring failure) → 500 INTERNAL_ERROR."""
        mock_verify.side_effect = RuntimeError("key ring corrupted")
        resp = self.client.get("/ping")
        assert resp.status_code == 500
        err = resp.json()["error"]
        assert err["code"] == "INTERNAL_ERROR"


# ===================================================================
# Full middleware stack integration
# ===================================================================

class TestMiddlewareStack:
    """Integration tests with all three middleware registered in correct order."""

    def setup_method(self):
        # Register in reverse order so execution is: RequestId → CorrelationId → HMAC
        self.app = _make_app()  # no middleware yet
        self.app.add_middleware(HMACVerificationMiddleware)
        self.app.add_middleware(CorrelationIdMiddleware)
        self.app.add_middleware(RequestIdMiddleware)

        @self.app.get("/stack")
        def stack(request: Request):
            return JSONResponse({
                "request_id": getattr(request.state, "request_id", None),
                "correlation_id": getattr(request.state, "correlation_id", None),
                "hmac_ctx": getattr(request.state, "hmac_ctx", None),
            })

        self.client = TestClient(self.app)

    @patch("api.middleware.verify_hmac_headers", return_value=_VALID_CTX)
    def test_middleware_stack_order(self, mock_verify):
        """Full stack: response has both correlation headers + hmac_ctx."""
        cid = "stack-corr-001"
        rid = "stack-req-001"
        resp = self.client.get(
            "/stack",
            headers={"X-Correlation-ID": cid, "X-Request-ID": rid},
        )
        assert resp.status_code == 200
        assert resp.headers["X-Request-ID"] == rid
        assert resp.headers["X-Correlation-ID"] == cid
        body = resp.json()
        assert body["hmac_ctx"] == _VALID_CTX
        assert body["request_id"] == rid
        assert body["correlation_id"] == cid

    @patch("api.middleware.verify_hmac_headers")
    def test_middleware_hmac_failure_still_has_headers(self, mock_verify):
        """HMAC fails → 401 but response still has X-Request-ID and X-Correlation-ID."""
        from textiq_hmac import VerificationError
        mock_verify.side_effect = VerificationError("denied")
        cid = "corr-on-401"
        resp = self.client.get(
            "/stack",
            headers={"X-Correlation-ID": cid},
        )
        assert resp.status_code == 401
        assert resp.headers.get("X-Request-ID") is not None
        assert resp.headers["X-Correlation-ID"] == cid
