"""Edge attestation (contract 2A.2): signing rules, and the fail-closed route guard."""

from __future__ import annotations

import json
from types import SimpleNamespace

import httpx
import pytest
from fastapi import Depends, FastAPI
from fastapi.responses import JSONResponse
from src.core import edge_guard
from src.core.edge_attestation import (
    ATTESTATION_HEADER,
    EDGE_SHOPIFY,
    EDGE_WP_GATEWAY,
    REQUEST_ID_HEADER,
    EdgeAttestationError,
    EdgeKey,
    parse_keys,
    sign,
    verify,
)
from src.core.edge_guard import require_edge, resolve_mode
from src.core.exceptions import ForbiddenError
from src.core.redis import get_redis

SECRET = "s" * 40
WP_KEY = EdgeKey(kid="wp-1", secret=SECRET, edge=EDGE_WP_GATEWAY)
SHOPIFY_KEY = EdgeKey(kid="sh-1", secret="h" * 40, edge=EDGE_SHOPIFY)
KEYS = {WP_KEY.kid: WP_KEY, SHOPIFY_KEY.kid: SHOPIFY_KEY}
NOW = 1_800_000_000
PATH = "/v1/external-admin/wordpress/health"
BODY = b'{"installation_id":"abc"}'


def _header(
    key=WP_KEY, *, method="POST", path=PATH, body=BODY, ts=NOW, nonce="n" * 24, rid="req-1"
):
    return sign(
        key, method=method, path=path, body=body, timestamp=ts, nonce=nonce, request_id=rid
    )


def _verify(header, **over):
    args = dict(method="POST", path=PATH, body=BODY, request_id="req-1", keys=KEYS, now=NOW)
    args.update(over)
    return verify(header, **args)


# ── pure verification ────────────────────────────────────────────────────────


def test_valid_attestation_identifies_the_edge():
    identity = _verify(_header())
    assert identity.edge == EDGE_WP_GATEWAY
    assert identity.kid == "wp-1"


@pytest.mark.parametrize(
    "override, code",
    [
        ({"body": b'{"installation_id":"other"}'}, "bad_signature"),
        ({"path": "/v1/external-admin/wordpress/chat/messages/send"}, "bad_signature"),
        ({"method": "GET"}, "bad_signature"),
        ({"request_id": "req-2"}, "bad_signature"),
        ({"now": NOW + 61}, "stale_timestamp"),
        ({"now": NOW - 61}, "stale_timestamp"),
    ],
)
def test_any_altered_input_is_rejected(override, code):
    with pytest.raises(EdgeAttestationError) as exc:
        _verify(_header(), **override)
    assert exc.value.code == code


def test_skew_boundary_is_accepted():
    assert _verify(_header(), now=NOW + 60).kid == "wp-1"


@pytest.mark.parametrize(
    "header, code",
    [
        (None, "missing_header"),
        ("", "missing_header"),
        ("v2;wp-1;1;" + "n" * 24 + ";ab", "malformed_header"),
        ("v1;wp-1;1;short;ab", "malformed_nonce"),
        ("v1;wp-1;notanint;" + "n" * 24 + ";ab", "malformed_timestamp"),
        ("v1;nope;1;" + "n" * 24 + ";ab", "unknown_kid"),
        ("v1;wp-1;1", "malformed_header"),
    ],
)
def test_malformed_headers_are_rejected(header, code):
    with pytest.raises(EdgeAttestationError) as exc:
        _verify(header)
    assert exc.value.code == code


def test_no_configured_keys_fails_closed():
    with pytest.raises(EdgeAttestationError) as exc:
        _verify(_header(), keys={})
    assert exc.value.code == "no_keys_configured"


def test_wrong_secret_is_rejected():
    forged = EdgeKey(kid="wp-1", secret="x" * 40, edge=EDGE_WP_GATEWAY)
    with pytest.raises(EdgeAttestationError) as exc:
        _verify(_header(forged))
    assert exc.value.code == "bad_signature"


def test_parse_keys_drops_unsafe_entries():
    raw = json.dumps(
        {
            "ok": {"secret": "a" * 32, "edge": "wp_gateway"},
            "short": {"secret": "tooshort", "edge": "wp_gateway"},
            "badedge": {"secret": "b" * 32, "edge": "dashboard"},
            "bad kid!": {"secret": "c" * 32, "edge": "wp_gateway"},
            "notdict": "x",
        }
    )
    assert set(parse_keys(raw)) == {"ok"}
    assert parse_keys("not json") == {}
    assert parse_keys("[]") == {}
    assert parse_keys("") == {}


# Fixed vector, computed independently with `openssl dgst -sha256 -hmac`. The Worker
# (P2HS-WP-Gateway/src/attestation.ts) must produce this exact value.
def test_signing_vector_matches_the_reference_hmac():
    key = EdgeKey(kid="vec", secret="k" * 32, edge=EDGE_WP_GATEWAY)
    header = sign(
        key,
        method="POST",
        path="/v1/external-admin/wordpress/health",
        body=b'{"a":1}',
        timestamp=1_700_000_000,
        nonce="abcdefghijklmnop",
        request_id="rid-1",
    )
    assert header == (
        "v1;vec;1700000000;abcdefghijklmnop;"
        "f96337ff45b23e117a13a650de190c1fcbb2cf402b141d0c285f22b1dafbe41d"
    )


def test_resolve_mode_fails_closed_on_unknown_values():
    assert resolve_mode("report") == "report"
    assert resolve_mode(" OFF ") == "off"
    assert resolve_mode("enforce") == "enforce"
    assert resolve_mode("") == "enforce"
    assert resolve_mode("permissive") == "enforce"
    assert resolve_mode(None) == "enforce"


# ── route guard ──────────────────────────────────────────────────────────────


class FakeRedis:
    def __init__(self, broken: bool = False):
        self.seen: set[str] = set()
        self.broken = broken

    async def set(self, key, value, nx=False, ex=None):
        if self.broken:
            raise ConnectionError("redis down")
        if nx and key in self.seen:
            return None
        self.seen.add(key)
        return True


def _settings(mode: str, keys: dict | None = None, legacy: str = ""):
    raw = json.dumps(
        {k.kid: {"secret": k.secret, "edge": k.edge} for k in (keys or {}).values()}
    ) if keys else ""
    return SimpleNamespace(
        edge_attestation_mode=mode, edge_attestation_keys=raw, wp_gateway_token=legacy
    )


def _app(monkeypatch, settings, redis, *, edge=EDGE_WP_GATEWAY, use_legacy=True):
    monkeypatch.setattr(edge_guard, "get_settings", lambda: settings)
    monkeypatch.setattr(edge_guard.time, "time", lambda: NOW)
    guard = require_edge(
        edge, legacy_token=(lambda s: s.wp_gateway_token) if use_legacy else None
    )
    app = FastAPI()
    app.dependency_overrides[get_redis] = lambda: redis

    @app.exception_handler(ForbiddenError)
    async def _forbidden(_request, exc):
        return JSONResponse({"detail": str(exc)}, status_code=403)

    @app.post("/v1/external-admin/wordpress/health")
    async def health(_g: None = Depends(guard)):
        return {"ok": True}

    return app


async def _post(app, headers=None, body=BODY):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://core") as client:
        return await client.post(
            "/v1/external-admin/wordpress/health", content=body, headers=headers or {}
        )


def _signed(key=WP_KEY, nonce="n" * 24, rid="req-1", body=BODY):
    return {
        ATTESTATION_HEADER: _header(key, nonce=nonce, rid=rid, body=body),
        REQUEST_ID_HEADER: rid,
    }


async def test_enforce_accepts_a_valid_attestation(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", KEYS), FakeRedis())
    assert (await _post(app, _signed())).status_code == 200


async def test_enforce_rejects_missing_attestation_with_a_generic_error(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", KEYS), FakeRedis())
    res = await _post(app)
    assert res.status_code == 403
    assert res.json() == {"detail": "edge_attestation_required"}


async def test_enforce_does_not_accept_the_legacy_static_token(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", KEYS, legacy="tok"), FakeRedis())
    res = await _post(app, {"X-P2HS-Gateway-Token": "tok"})
    assert res.status_code == 403


async def test_enforce_with_no_keys_configured_fails_closed(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", None), FakeRedis())
    assert (await _post(app, _signed())).status_code == 403


async def test_unknown_mode_fails_closed(monkeypatch):
    app = _app(monkeypatch, _settings("permissive", KEYS), FakeRedis())
    assert (await _post(app)).status_code == 403


async def test_enforce_rejects_replayed_nonce(monkeypatch):
    redis = FakeRedis()
    app = _app(monkeypatch, _settings("enforce", KEYS), redis)
    headers = _signed()
    assert (await _post(app, headers)).status_code == 200
    assert (await _post(app, headers)).status_code == 403


async def test_enforce_rejects_a_tampered_body(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", KEYS), FakeRedis())
    res = await _post(app, _signed(), body=b'{"installation_id":"evil"}')
    assert res.status_code == 403


async def test_enforce_rejects_another_edges_key(monkeypatch):
    """The Shopify Edge's key must not open WordPress routes (per-edge isolation)."""
    app = _app(monkeypatch, _settings("enforce", KEYS), FakeRedis())
    assert (await _post(app, _signed(SHOPIFY_KEY))).status_code == 403


async def test_enforce_fails_closed_when_the_nonce_store_is_down(monkeypatch):
    app = _app(monkeypatch, _settings("enforce", KEYS), FakeRedis(broken=True))
    assert (await _post(app, _signed())).status_code == 403


async def test_report_mode_keeps_legacy_behaviour_for_unsigned_traffic(monkeypatch):
    # Rollout: Core deploys first, the Edge does not sign yet. Nothing may break.
    app = _app(monkeypatch, _settings("report", KEYS, legacy="tok"), FakeRedis())
    assert (await _post(app, {"X-P2HS-Gateway-Token": "tok"})).status_code == 200
    assert (await _post(app, {"X-P2HS-Gateway-Token": "wrong"})).status_code == 403
    assert (await _post(app)).status_code == 403


async def test_report_mode_with_unset_legacy_token_is_the_old_open_behaviour(monkeypatch):
    app = _app(monkeypatch, _settings("report", None, legacy=""), FakeRedis())
    assert (await _post(app)).status_code == 200


async def test_report_mode_accepts_a_forged_attestation_only_via_legacy(monkeypatch):
    app = _app(monkeypatch, _settings("report", KEYS, legacy="tok"), FakeRedis())
    bad = {ATTESTATION_HEADER: "v1;wp-1;1;" + "n" * 24 + ";deadbeef"}
    assert (await _post(app, bad)).status_code == 403
    assert (await _post(app, {**bad, "X-P2HS-Gateway-Token": "tok"})).status_code == 200


async def test_routes_without_a_legacy_check_stay_open_until_enforce(monkeypatch):
    report = _settings("report", KEYS, legacy="tok")
    open_app = _app(monkeypatch, report, FakeRedis(), use_legacy=False)
    assert (await _post(open_app)).status_code == 200
    enforce = _settings("enforce", KEYS, legacy="tok")
    closed_app = _app(monkeypatch, enforce, FakeRedis(), use_legacy=False)
    assert (await _post(closed_app)).status_code == 403


async def test_off_mode_is_the_legacy_static_token_only(monkeypatch):
    app = _app(monkeypatch, _settings("off", KEYS, legacy="tok"), FakeRedis())
    assert (await _post(app, {"X-P2HS-Gateway-Token": "tok"})).status_code == 200
    assert (await _post(app)).status_code == 403


# ── every machine route is covered ───────────────────────────────────────────

# Called by the Dashboard on behalf of a user (browser -> Dash -> Core) with its own embed
# token; it is not a plugin/Edge route.
_NOT_EDGE_ROUTES = {"/v1/external-admin/wordpress/embed/exchange"}


def test_every_plugin_facing_route_requires_edge_attestation():
    from src.domain.external_admin.router import plugin_router

    unguarded = []
    for route in plugin_router.routes:
        if route.path in _NOT_EDGE_ROUTES:
            continue
        calls = [d.call for d in route.dependant.dependencies]
        if not any(getattr(c, "__module__", "") == "src.core.edge_guard" for c in calls):
            unguarded.append(route.path)
    assert unguarded == [], f"plugin routes without the Edge guard: {unguarded}"


def test_shopify_routes_use_the_shopify_edge_and_wordpress_routes_the_wp_gateway():
    from src.domain.external_admin.router import plugin_router

    for route in plugin_router.routes:
        if route.path in _NOT_EDGE_ROUTES:
            continue
        guard = next(
            d.call for d in route.dependant.dependencies
            if getattr(d.call, "__module__", "") == "src.core.edge_guard"
        )
        # The closure's `edge` cell is the Edge class it enforces.
        cells = (c.cell_contents for c in guard.__closure__)
        edge = dict(zip(guard.__code__.co_freevars, cells))["edge"]
        expected = EDGE_SHOPIFY if "/shopify/" in route.path else EDGE_WP_GATEWAY
        assert edge == expected, route.path
