"""
Tests for the e2b code execution sandbox primitive.

Unit tests inject a fake async HTTP client (dependency injection, no
monkeypatching) and assert request shapes and result mapping. Real-network
integration tests live in tests/integration/sandbox/test_e2b_sandbox.py.
"""

import json

import httpx
import pytest

import litellm
from litellm.llms.base_llm.sandbox.transformation import ContainerHandle
from litellm.llms.e2b.sandbox.transformation import (
    MAX_OUTPUT_BYTES,
    E2BSandboxConfig,
)


class FakeResponse:
    def __init__(self, *, json_data=None, lines=None, status_code=200):
        self._json = json_data
        self._lines = lines or []
        self.status_code = status_code

    def json(self):
        return self._json

    async def aiter_lines(self):
        for line in self._lines:
            yield line


class FakeHTTPClient:
    """Records outbound requests and returns canned responses keyed by URL."""

    def __init__(
        self,
        *,
        create_json=None,
        execute_lines=None,
        delete_status=204,
        execute_raises=None,
    ):
        self.create_json = create_json or {
            "sandboxID": "sbx_123",
            "domain": "e2b.app",
            "envdAccessToken": "tok_abc",
        }
        self.execute_lines = execute_lines or []
        self.delete_status = delete_status
        self.execute_raises = execute_raises
        self.calls = []

    async def post(self, url, headers=None, json=None, stream=False, **kwargs):
        self.calls.append(("POST", url, headers, json))
        if url.endswith("/sandboxes"):
            return FakeResponse(json_data=self.create_json)
        if url.endswith("/execute"):
            if self.execute_raises is not None:
                raise self.execute_raises
            return FakeResponse(lines=self.execute_lines)
        raise AssertionError(f"unexpected POST {url}")

    async def delete(self, url, headers=None, **kwargs):
        self.calls.append(("DELETE", url, headers, None))
        if not (200 <= self.delete_status < 300):
            raise httpx.HTTPStatusError(
                f"status {self.delete_status}",
                request=httpx.Request("DELETE", url),
                response=httpx.Response(self.delete_status),
            )
        return FakeResponse(status_code=self.delete_status)


# ---------- pure parser ----------


def test_parse_lines_stdout_and_count():
    lines = [
        json.dumps({"type": "stdout", "text": "6\n", "timestamp": 1}),
        json.dumps({"type": "number_of_executions", "execution_count": 1}),
    ]
    result = E2BSandboxConfig._parse_lines(lines)
    assert result.stdout == "6\n"
    assert result.execution_count == 1
    assert result.error is None


def test_parse_lines_error_surfaces_name_and_traceback():
    lines = [
        json.dumps(
            {
                "type": "error",
                "name": "ZeroDivisionError",
                "value": "division by zero",
                "traceback": "Traceback (most recent call last): ...",
            }
        )
    ]
    result = E2BSandboxConfig._parse_lines(lines)
    assert result.error["name"] == "ZeroDivisionError"
    assert "Traceback" in result.error["traceback"]


def test_parse_lines_result_carries_png():
    lines = [
        json.dumps({"type": "result", "png": "BASE64DATA", "is_main_result": True})
    ]
    result = E2BSandboxConfig._parse_lines(lines)
    assert result.results and result.results[0]["png"] == "BASE64DATA"
    assert "type" not in result.results[0]


# ---------- request shapes ----------


@pytest.mark.asyncio
async def test_template_flows_into_create_request_as_templateID():
    client = FakeHTTPClient()
    cfg = E2BSandboxConfig()
    handle = await cfg.acreate_sandbox(
        template="my-custom-template", api_key="e2b_key", client=client
    )

    method, url, headers, body = client.calls[0]
    assert method == "POST"
    assert url.endswith("/sandboxes")
    assert body["templateID"] == "my-custom-template"  # not "template"
    assert body["secure"] is True
    assert headers["X-API-Key"] == "e2b_key"
    assert handle.id == "sbx_123"
    assert handle._hidden_params["envd_access_token"] == "tok_abc"


@pytest.mark.asyncio
async def test_create_defaults_template_when_omitted():
    client = FakeHTTPClient()
    await E2BSandboxConfig().acreate_sandbox(api_key="e2b_key", client=client)
    _, _, _, body = client.calls[0]
    assert body["templateID"] == "code-interpreter-v1"


@pytest.mark.asyncio
async def test_run_code_targets_jupyter_host_with_access_token():
    client = FakeHTTPClient(
        execute_lines=[json.dumps({"type": "stdout", "text": "42\n", "timestamp": 1})]
    )
    handle = ContainerHandle(id="sbx_xyz", provider="e2b", domain="e2b.app")
    handle._hidden_params = {"envd_access_token": "tok_run"}

    result = await E2BSandboxConfig().arun_code(
        container=handle, code="print(6*7)", client=client
    )

    method, url, headers, body = client.calls[0]
    assert url == "https://49999-sbx_xyz.e2b.app/execute"
    assert headers["X-Access-Token"] == "tok_run"
    assert body["code"] == "print(6*7)"
    assert result.stdout.strip() == "42"


@pytest.mark.asyncio
async def test_delete_issues_delete_to_sandbox_id():
    client = FakeHTTPClient(delete_status=204)
    handle = ContainerHandle(id="sbx_del", provider="e2b", domain="e2b.app")
    handle._hidden_params = {"api_key": "e2b_key"}

    ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)

    method, url, headers, _ = client.calls[0]
    assert method == "DELETE"
    assert url.endswith("/sandboxes/sbx_del")
    assert ok is True


@pytest.mark.asyncio
async def test_delete_returns_false_on_404():
    client = FakeHTTPClient(delete_status=404)
    handle = ContainerHandle(id="sbx_gone", provider="e2b", domain="e2b.app")
    handle._hidden_params = {"api_key": "e2b_key"}
    ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
    assert ok is False


# ---------- ephemeral teardown ----------


@pytest.mark.asyncio
async def test_code_interpreter_tool_deletes_even_when_run_raises():
    client = FakeHTTPClient(execute_raises=RuntimeError("boom"))

    with pytest.raises(RuntimeError, match="boom"):
        await litellm.acode_interpreter_tool(
            provider="e2b", code="1/0", api_key="e2b_key", client=client
        )

    methods = [c[0] for c in client.calls]
    urls = [c[1] for c in client.calls]
    assert methods == ["POST", "POST", "DELETE"]  # create, run(raises), delete
    assert urls[0].endswith("/sandboxes")
    assert urls[1].endswith("/execute")
    assert urls[2].endswith("/sandboxes/sbx_123")


# ---------- correctness guards ----------


@pytest.mark.asyncio
async def test_delete_reraises_non_404_http_error():
    client = FakeHTTPClient(delete_status=500)
    handle = ContainerHandle(id="sbx_err", provider="e2b", domain="e2b.app")
    handle._hidden_params = {"api_key": "e2b_key"}
    with pytest.raises(httpx.HTTPStatusError):
        await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)


@pytest.mark.asyncio
async def test_create_preserves_explicit_zero_timeout():
    client = FakeHTTPClient()
    await E2BSandboxConfig().acreate_sandbox(
        timeout=0, api_key="e2b_key", client=client
    )
    _, _, _, body = client.calls[0]
    assert body["timeout"] == 0


@pytest.mark.asyncio
async def test_run_code_rejects_bare_id_without_access_token():
    client = FakeHTTPClient()
    with pytest.raises(ValueError, match="access token"):
        await E2BSandboxConfig().arun_code(
            container="sbx_no_token", code="print(1)", client=client
        )
    assert client.calls == []  # never reached the network


def test_parse_lines_skips_non_json_lines():
    lines = [
        "not-json-heartbeat",
        json.dumps({"type": "stdout", "text": "ok\n"}),
        "",
        "{partial",
    ]
    result = E2BSandboxConfig._parse_lines(lines)
    assert result.stdout == "ok\n"
    assert result.error is None


@pytest.mark.asyncio
async def test_run_code_aborts_on_output_over_cap():
    big_line = "x" * (MAX_OUTPUT_BYTES + 1)
    client = FakeHTTPClient(execute_lines=[big_line])
    handle = ContainerHandle(id="sbx_big", provider="e2b", domain="e2b.app")
    handle._hidden_params = {"envd_access_token": "tok"}
    with pytest.raises(ValueError, match="exceeded"):
        await E2BSandboxConfig().arun_code(
            container=handle, code="print('x'*999)", client=client
        )


# ---------- public entrypoints ----------


@pytest.mark.asyncio
async def test_public_lifecycle_create_run_delete():
    client = FakeHTTPClient(
        execute_lines=[json.dumps({"type": "stdout", "text": "42\n"})]
    )
    container = await litellm.acreate_sandbox(
        provider="e2b", api_key="e2b_key", client=client
    )
    assert container.id == "sbx_123"

    result = await litellm.arun_code(
        provider="e2b",
        container=container,
        api_key="e2b_key",
        code="print(6*7)",
        client=client,
    )
    assert result.stdout.strip() == "42"

    assert (
        await litellm.adelete_sandbox(
            provider="e2b", container=container, api_key="e2b_key", client=client
        )
        is True
    )


@pytest.mark.asyncio
async def test_unsupported_provider_raises():
    with pytest.raises(ValueError, match="not-a-provider' is not a valid SandboxProviders"):
        await litellm.acreate_sandbox(provider="not-a-provider")


# ---------- api_base override ----------


@pytest.mark.asyncio
async def test_create_uses_api_base_override():
    client = FakeHTTPClient()
    await E2BSandboxConfig().acreate_sandbox(
        api_base="http://my-sandbox:8080", api_key="k", client=client
    )
    _, url, _, _ = client.calls[0]
    assert url == "http://my-sandbox:8080/sandboxes"


@pytest.mark.asyncio
async def test_create_defaults_to_e2b_api_base():
    client = FakeHTTPClient()
    await E2BSandboxConfig().acreate_sandbox(api_key="k", client=client)
    _, url, _, _ = client.calls[0]
    assert url == "https://api.e2b.app/sandboxes"
