import json
import logging
import queue as queue_module
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer

import httpx
import pytest

from grantflow.api import webhooks


def test_send_job_webhook_event_retries_on_request_error_and_logs(monkeypatch, caplog):
    attempts = {"count": 0}
    sleeps = []

    monkeypatch.setenv("GRANTFLOW_WEBHOOK_MAX_ATTEMPTS", "3")
    monkeypatch.setenv("GRANTFLOW_WEBHOOK_BACKOFF_BASE_MS", "1")
    monkeypatch.setenv("GRANTFLOW_WEBHOOK_BACKOFF_MAX_MS", "2")

    def fake_sleep(delay_s: float):
        sleeps.append(delay_s)

    def fake_post(url, content, headers, timeout):
        attempts["count"] += 1
        request = httpx.Request("POST", url)
        if attempts["count"] == 1:
            raise httpx.ConnectError("network down", request=request)
        return httpx.Response(200, request=request)

    monkeypatch.setattr(webhooks, "is_safe_webhook_url", lambda url: True)
    monkeypatch.setattr(webhooks.time, "sleep", fake_sleep)
    monkeypatch.setattr(webhooks.httpx, "post", fake_post)

    with caplog.at_level(logging.INFO):
        webhooks.send_job_webhook_event(
            url="https://example.com/webhook",
            secret="secret123",
            event="job.completed",
            job_id="job-1",
            job={"status": "done"},
        )

    assert attempts["count"] == 2
    assert sleeps and sleeps[0] > 0
    text = caplog.text
    assert "Webhook delivery failed" in text
    assert "Webhook delivery succeeded" in text


def test_send_job_webhook_event_adds_delivery_timestamp_and_verifiable_signature(monkeypatch):
    captured = {}

    monkeypatch.setattr(webhooks, "is_safe_webhook_url", lambda url: True)
    monkeypatch.setattr(webhooks.time, "time", lambda: 1_800_000_000)

    def fake_post(url, content, headers, timeout):
        captured["content"] = content
        captured["headers"] = headers
        request = httpx.Request("POST", url)
        return httpx.Response(200, request=request)

    monkeypatch.setattr(webhooks.httpx, "post", fake_post)

    webhooks.send_job_webhook_event(
        url="https://example.com/webhook",
        secret="secret123",
        event="job.completed",
        job_id="job-1",
        job={"status": "done"},
    )

    headers = captured["headers"]
    assert headers["X-GrantFlow-Delivery"].startswith("gfd_")
    assert headers["X-GrantFlow-Timestamp"] == "1800000000"
    assert webhooks.verify_webhook_signature(
        secret="secret123",
        body=captured["content"],
        signature=headers["X-GrantFlow-Signature"],
        timestamp=headers["X-GrantFlow-Timestamp"],
    )
    assert not webhooks.verify_webhook_signature(
        secret="secret123",
        body=captured["content"],
        signature="sha256=bad",
        timestamp=headers["X-GrantFlow-Timestamp"],
    )


def test_verify_webhook_signature_rejects_stale_or_invalid_timestamp(monkeypatch):
    body = b'{"event":"job.completed"}'
    signature = webhooks._signature_header("secret123", body)

    monkeypatch.setattr(webhooks.time, "time", lambda: 1_800_000_000)

    assert not webhooks.verify_webhook_signature(
        secret="secret123",
        body=body,
        signature=signature,
        timestamp="1799990000",
    )
    assert not webhooks.verify_webhook_signature(
        secret="secret123",
        body=body,
        signature=signature,
        timestamp="not-a-timestamp",
    )


def test_send_job_webhook_event_retries_on_http_500_and_gives_up(monkeypatch, caplog):
    attempts = {"count": 0}
    sleeps = []

    monkeypatch.setenv("GRANTFLOW_WEBHOOK_MAX_ATTEMPTS", "2")
    monkeypatch.setenv("GRANTFLOW_WEBHOOK_BACKOFF_BASE_MS", "1")
    monkeypatch.setenv("GRANTFLOW_WEBHOOK_BACKOFF_MAX_MS", "1")

    def fake_sleep(delay_s: float):
        sleeps.append(delay_s)

    def fake_post(url, content, headers, timeout):
        attempts["count"] += 1
        request = httpx.Request("POST", url)
        return httpx.Response(500, request=request)

    monkeypatch.setattr(webhooks, "is_safe_webhook_url", lambda url: True)
    monkeypatch.setattr(webhooks.time, "sleep", fake_sleep)
    monkeypatch.setattr(webhooks.httpx, "post", fake_post)

    with caplog.at_level(logging.WARNING):
        with pytest.raises(httpx.HTTPStatusError):
            webhooks.send_job_webhook_event(
                url="https://example.com/webhook",
                secret=None,
                event="job.failed",
                job_id="job-2",
                job={"status": "error"},
            )

    assert attempts["count"] == 2
    assert sleeps and len(sleeps) == 1
    assert "retrying" in caplog.text
    assert "giving up" in caplog.text


def test_send_job_webhook_event_rejects_unsafe_url(monkeypatch):
    called = {"count": 0}

    def fake_post(url, content, headers, timeout):
        called["count"] += 1
        request = httpx.Request("POST", url)
        return httpx.Response(200, request=request)

    monkeypatch.setattr(webhooks.httpx, "post", fake_post)

    with pytest.raises(ValueError, match="SSRF policy"):
        webhooks.send_job_webhook_event(
            url="http://127.0.0.1:8080/hook",
            secret=None,
            event="job.pending_hitl",
            job_id="job-unsafe",
            job={"status": "pending_hitl"},
        )

    assert called["count"] == 0


def test_send_job_webhook_event_does_not_retry_on_http_400(monkeypatch):
    attempts = {"count": 0}

    monkeypatch.setenv("GRANTFLOW_WEBHOOK_MAX_ATTEMPTS", "3")

    def fake_post(url, content, headers, timeout):
        attempts["count"] += 1
        request = httpx.Request("POST", url)
        return httpx.Response(400, request=request)

    monkeypatch.setattr(webhooks, "is_safe_webhook_url", lambda url: True)
    monkeypatch.setattr(webhooks.httpx, "post", fake_post)

    with pytest.raises(httpx.HTTPStatusError):
        webhooks.send_job_webhook_event(
            url="https://example.com/webhook",
            secret=None,
            event="job.pending_hitl",
            job_id="job-3",
            job={"status": "pending_hitl"},
        )


def test_send_job_webhook_event_delivers_real_http_post_with_valid_signature(monkeypatch):
    """End-to-end: spin up a local HTTP server, send a real POST, verify HMAC signature.

    The SSRF guard is monkeypatched to allow 127.0.0.1 so we can use a loopback
    receiver without touching the production allow-list.
    """
    received: queue_module.Queue = queue_module.Queue()

    class _Handler(BaseHTTPRequestHandler):
        def do_POST(self):
            length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(length)
            received.put({"body": body, "headers": dict(self.headers)})
            self.send_response(200)
            self.end_headers()

        def log_message(self, *args):
            pass  # suppress server output during tests

    server = HTTPServer(("127.0.0.1", 0), _Handler)
    port = server.server_address[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()

    # Both SSRF guards must be relaxed so a loopback receiver is reachable.
    monkeypatch.setattr(webhooks, "is_safe_webhook_url", lambda url: True)
    monkeypatch.setattr(webhooks, "_validate_webhook_url_or_raise", lambda url: url)

    try:
        webhooks.send_job_webhook_event(
            url=f"http://127.0.0.1:{port}/hook",
            secret="conformance-secret-xyz",
            event="job.completed",
            job_id="job-real-http",
            job={"status": "done", "output": {"sections": []}},
        )
    finally:
        server.shutdown()

    assert not received.empty(), "No HTTP POST was received by the local server"
    item = received.get_nowait()
    body: bytes = item["body"]
    hdrs: dict = item["headers"]

    # Delivery headers
    assert hdrs.get("X-GrantFlow-Delivery", "").startswith("gfd_")
    assert hdrs.get("X-GrantFlow-Timestamp", "").isdigit()
    sig = hdrs.get("X-GrantFlow-Signature", "")
    assert sig.startswith("sha256=")

    # Signature must be valid
    assert webhooks.verify_webhook_signature(
        secret="conformance-secret-xyz",
        body=body,
        signature=sig,
        timestamp=hdrs["X-GrantFlow-Timestamp"],
    ), "HMAC signature verification failed"

    # Wrong secret must fail
    assert not webhooks.verify_webhook_signature(
        secret="wrong-secret",
        body=body,
        signature=sig,
        timestamp=hdrs["X-GrantFlow-Timestamp"],
    )

    # Payload shape
    payload = json.loads(body)
    assert payload["event"] == "job.completed"
    assert payload["job_id"] == "job-real-http"
    assert payload["status"] == "done"
    assert isinstance(payload["job"], dict)
