"""End-to-end integration tests for the Gitea webhook receiver
(v0.18.0 Slice 3 — webhook tightening per the email + webhook
hygiene proposal).

The release changes the receiver from "verifies the signature only
when a secret is configured; silently accepts unsigned POSTs
otherwise" to "requires the secret unless `RFC_APP_INSECURE_WEBHOOKS=1`
is set as an explicit dev-bypass." The startup-time check lives in
`config.load_config()`; the request-time check lives in
`webhooks.receive`.

These tests prove:

  * The framework refuses to start when `GITEA_WEBHOOK_SECRET` is
    empty and the dev-bypass is not set.
  * The dev-bypass (`RFC_APP_INSECURE_WEBHOOKS=1`) lets the
    framework boot with an empty secret AND lets webhook POSTs
    land without signature verification (a loud-warning log line
    surfaces, but the request is accepted).
  * Default path (secret bound): a POST with a valid signature
    lands; a POST with an invalid signature gets 401; a POST with
    no signature gets 401.
  * Unknown-repo POSTs surface in the log (the "stale Gitea hook"
    case the proposal targets).
"""
from __future__ import annotations

import hashlib
import hmac
import json
import logging

import pytest

from test_propose_vertical import (  # noqa: F401
    FakeGitea,
    app_with_fake_gitea,
    tmp_env,
)


# ---------------------------------------------------------------------------
# Startup-time secret check (config.load_config)
# ---------------------------------------------------------------------------


def test_config_refuses_to_load_with_empty_secret_and_no_bypass(monkeypatch, tmp_path):
    """The framework MUST refuse to start when `GITEA_WEBHOOK_SECRET`
    is empty unless `RFC_APP_INSECURE_WEBHOOKS=1` is set. This is
    the v0.18.0 startup-loud-failure shape — silent acceptance was
    the bug."""
    monkeypatch.setenv("GITEA_URL", "http://gitea.test")
    monkeypatch.setenv("GITEA_BOT_USER", "rfc-bot")
    monkeypatch.setenv("GITEA_BOT_TOKEN", "bot-token")
    monkeypatch.setenv("GITEA_ORG", "wiggleverse")
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "csec")
    monkeypatch.setenv("SECRET_KEY", "test-secret-key")
    monkeypatch.setenv("DATABASE_PATH", str(tmp_path / "test.db"))
    monkeypatch.setenv("GITEA_WEBHOOK_SECRET", "")
    monkeypatch.delenv("RFC_APP_INSECURE_WEBHOOKS", raising=False)

    from app.config import load_config
    with pytest.raises(RuntimeError, match="GITEA_WEBHOOK_SECRET"):
        load_config()


def test_config_loads_with_empty_secret_when_bypass_is_set(monkeypatch, tmp_path):
    """The explicit `RFC_APP_INSECURE_WEBHOOKS=1` opt-in lets the
    framework boot with an empty webhook secret. This is the
    local-dev escape hatch."""
    monkeypatch.setenv("GITEA_URL", "http://gitea.test")
    monkeypatch.setenv("GITEA_BOT_USER", "rfc-bot")
    monkeypatch.setenv("GITEA_BOT_TOKEN", "bot-token")
    monkeypatch.setenv("GITEA_ORG", "wiggleverse")
    monkeypatch.setenv("REGISTRY_REPO", "registry")
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "csec")
    monkeypatch.setenv("SECRET_KEY", "test-secret-key")
    monkeypatch.setenv("DATABASE_PATH", str(tmp_path / "test.db"))
    monkeypatch.setenv("GITEA_WEBHOOK_SECRET", "")
    monkeypatch.setenv("RFC_APP_INSECURE_WEBHOOKS", "1")

    from app.config import load_config
    cfg = load_config()  # MUST NOT raise
    assert cfg.webhook_secret == ""


def test_config_loads_with_secret_set(monkeypatch, tmp_path):
    """Sanity: the happy path (secret bound, bypass not set) loads
    cleanly."""
    monkeypatch.setenv("GITEA_URL", "http://gitea.test")
    monkeypatch.setenv("GITEA_BOT_USER", "rfc-bot")
    monkeypatch.setenv("GITEA_BOT_TOKEN", "bot-token")
    monkeypatch.setenv("GITEA_ORG", "wiggleverse")
    monkeypatch.setenv("REGISTRY_REPO", "registry")
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "csec")
    monkeypatch.setenv("SECRET_KEY", "test-secret-key")
    monkeypatch.setenv("DATABASE_PATH", str(tmp_path / "test.db"))
    monkeypatch.setenv("GITEA_WEBHOOK_SECRET", "my-real-secret")
    monkeypatch.delenv("RFC_APP_INSECURE_WEBHOOKS", raising=False)

    from app.config import load_config
    cfg = load_config()
    assert cfg.webhook_secret == "my-real-secret"


# ---------------------------------------------------------------------------
# Request-time signature verification (webhooks.receive)
#
# The default `app_with_fake_gitea` fixture binds
# `GITEA_WEBHOOK_SECRET=test-webhook-secret-for-signature-verification`,
# so these tests exercise the production path.
# ---------------------------------------------------------------------------


_SECRET = "test-webhook-secret-for-signature-verification"


def _sign(body: bytes) -> str:
    return hmac.new(_SECRET.encode("utf-8"), body, hashlib.sha256).hexdigest()


def test_webhook_post_with_valid_signature_accepted(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, _fake = app_with_fake_gitea
    with TestClient(app) as client:
        body = json.dumps({"repository": {"full_name": "wiggleverse/meta"}}).encode()
        sig = _sign(body)
        r = client.post(
            "/api/webhooks/gitea",
            content=body,
            headers={
                "X-Gitea-Event": "push",
                "X-Gitea-Signature": sig,
                "Content-Type": "application/json",
            },
        )
        assert r.status_code == 200, r.text


def test_webhook_post_with_invalid_signature_refused_401(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, _fake = app_with_fake_gitea
    with TestClient(app) as client:
        body = json.dumps({"repository": {"full_name": "wiggleverse/meta"}}).encode()
        r = client.post(
            "/api/webhooks/gitea",
            content=body,
            headers={
                "X-Gitea-Event": "push",
                "X-Gitea-Signature": "0" * 64,  # wrong signature
                "Content-Type": "application/json",
            },
        )
        assert r.status_code == 401


def test_webhook_post_with_missing_signature_refused_401(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, _fake = app_with_fake_gitea
    with TestClient(app) as client:
        body = json.dumps({"repository": {"full_name": "wiggleverse/meta"}}).encode()
        r = client.post(
            "/api/webhooks/gitea",
            content=body,
            headers={
                "X-Gitea-Event": "push",
                "Content-Type": "application/json",
            },
        )
        assert r.status_code == 401


# ---------------------------------------------------------------------------
# Unknown-repo logging (the "stale hook on a fork" surface)
# ---------------------------------------------------------------------------


def test_webhook_unknown_repo_logs_at_info(app_with_fake_gitea, caplog):
    """Per the proposal: a hook on a fork or a stale Gitea binding
    used to silently 200-OK. v0.18.0 surfaces it as an INFO log."""
    from fastapi.testclient import TestClient

    app, _fake = app_with_fake_gitea
    with TestClient(app) as client:
        body = json.dumps({"repository": {"full_name": "someone-else/unrelated"}}).encode()
        sig = _sign(body)
        with caplog.at_level(logging.INFO, logger="app.webhooks"):
            r = client.post(
                "/api/webhooks/gitea",
                content=body,
                headers={
                    "X-Gitea-Event": "push",
                    "X-Gitea-Signature": sig,
                    "Content-Type": "application/json",
                },
            )
        assert r.status_code == 200  # the handler still 200s; surface is the log line
        assert any(
            "unknown repo" in rec.message and "someone-else/unrelated" in rec.message
            for rec in caplog.records
        ), f"expected unknown-repo log line; got: {[r.message for r in caplog.records]}"