"""Tests for webhook (async) execution."""

from __future__ import annotations

import json
from unittest.mock import patch

import httpx
import pytest
import respx

from baponi import Baponi
from baponi._webhook import ExecutionHandle

FAKE_KEY = "sk-test-key-for-unit-tests"
EXECUTE_URL = "https://api.baponi.ai/v1/sandbox/execute"

# Server-generated trace_id (returned in 202 response, used by ExecutionHandle)
TRACE_ID = "trc_abc12345"
EXECUTION_URL = f"https://api.baponi.ai/v1/executions/{TRACE_ID}"
CANCEL_URL = f"https://api.baponi.ai/v1/executions/{TRACE_ID}/cancel"

MOCK_ACCEPTED = {"trace_id": TRACE_ID, "status": "running"}

MOCK_RUNNING_STATUS = {
    "trace_id": TRACE_ID,
    "status": "running",
    "duration_ms": 1000,
}

MOCK_COMPLETED_STATUS = {
    "trace_id": TRACE_ID,
    "status": "success",
    "exit_code": 0,
    "stdout": "done\n",
    "stderr": "",
    "duration_ms": 2000,
}


class TestExecuteWebhook:
    @respx.mock
    def test_sends_prefer_async_header(self):
        route = respx.post(EXECUTE_URL).mock(
            return_value=httpx.Response(202, json=MOCK_ACCEPTED)
        )

        with Baponi(api_key=FAKE_KEY) as client:
            handle = client.execute_webhook("import time; time.sleep(5)")

        assert route.calls[0].request.headers["prefer"] == "respond-async"
        assert isinstance(handle, ExecutionHandle)
        assert handle.trace_id == TRACE_ID

    @respx.mock
    def test_webhook_url_in_body(self):
        route = respx.post(EXECUTE_URL).mock(
            return_value=httpx.Response(202, json=MOCK_ACCEPTED)
        )

        with Baponi(api_key=FAKE_KEY) as client:
            client.execute_webhook(
                "print(1)", webhook_url="https://example.com/webhook"
            )

        body = json.loads(route.calls[0].request.content)
        assert body["webhook_url"] == "https://example.com/webhook"

    def test_http_webhook_url_rejected(self):
        with Baponi(api_key=FAKE_KEY) as client:
            with pytest.raises(ValueError, match="must use HTTPS"):
                client.execute_webhook(
                    "print(1)", webhook_url="http://example.com/webhook"
                )

    def test_too_long_webhook_url_rejected(self):
        with Baponi(api_key=FAKE_KEY) as client:
            long_url = "https://example.com/" + "a" * 2040
            with pytest.raises(ValueError, match="exceeds maximum length"):
                client.execute_webhook("print(1)", webhook_url=long_url)


class TestExecutionHandle:
    @respx.mock
    def test_poll(self):
        respx.post(EXECUTE_URL).mock(
            return_value=httpx.Response(202, json=MOCK_ACCEPTED)
        )
        respx.get(EXECUTION_URL).mock(
            return_value=httpx.Response(200, json=MOCK_COMPLETED_STATUS)
        )

        with Baponi(api_key=FAKE_KEY) as client:
            handle = client.execute_webhook("x")
            status = handle.poll()

        assert status.status == "success"
        assert status.trace_id == TRACE_ID

    @respx.mock
    def test_cancel(self):
        respx.post(EXECUTE_URL).mock(
            return_value=httpx.Response(202, json=MOCK_ACCEPTED)
        )
        respx.post(CANCEL_URL).mock(
            return_value=httpx.Response(
                200, json={"trace_id": TRACE_ID, "status": "cancelling"}
            )
        )

        with Baponi(api_key=FAKE_KEY) as client:
            handle = client.execute_webhook("x")
            cancel_resp = handle.cancel()

        assert cancel_resp.status == "cancelling"

    @respx.mock
    def test_wait_polls_until_terminal(self):
        respx.post(EXECUTE_URL).mock(
            return_value=httpx.Response(202, json=MOCK_ACCEPTED)
        )
        poll_route = respx.get(EXECUTION_URL).mock(
            side_effect=[
                httpx.Response(200, json=MOCK_RUNNING_STATUS),
                httpx.Response(200, json=MOCK_RUNNING_STATUS),
                httpx.Response(200, json=MOCK_COMPLETED_STATUS),
            ]
        )

        with Baponi(api_key=FAKE_KEY) as client:
            handle = client.execute_webhook("x")
            with patch("baponi._webhook.time.sleep"):
                status = handle.wait(poll_interval=0.01)

        assert status.status == "success"
        assert poll_route.call_count == 3


class TestGetExecution:
    @respx.mock
    def test_get_execution(self):
        respx.get(EXECUTION_URL).mock(
            return_value=httpx.Response(200, json=MOCK_COMPLETED_STATUS)
        )

        with Baponi(api_key=FAKE_KEY) as client:
            status = client.get_execution(TRACE_ID)

        assert status.status == "success"
        assert status.trace_id == TRACE_ID

    def test_invalid_trace_id_raises(self):
        with Baponi(api_key=FAKE_KEY) as client:
            with pytest.raises(ValueError, match="Invalid trace_id format"):
                client.get_execution("../../../etc/passwd")

    def test_missing_prefix_raises(self):
        with Baponi(api_key=FAKE_KEY) as client:
            with pytest.raises(ValueError, match="Invalid trace_id format"):
                client.get_execution("abc12345")


class TestCancelExecution:
    @respx.mock
    def test_cancel_execution(self):
        respx.post(CANCEL_URL).mock(
            return_value=httpx.Response(
                200, json={"trace_id": TRACE_ID, "status": "cancelling"}
            )
        )

        with Baponi(api_key=FAKE_KEY) as client:
            resp = client.cancel_execution(TRACE_ID)

        assert resp.status == "cancelling"
