from __future__ import annotations

from unittest.mock import MagicMock, patch

import httpx
import pytest

from langchain_monza.sandbox import MonzaSandbox


def _make_mock_response(
    *,
    status_code: int = 200,
    json_data: dict | None = None,
    content: bytes | None = None,
) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status_code
    resp.is_success = 200 <= status_code < 300
    resp.raise_for_status = MagicMock()
    if json_data is not None:
        resp.json.return_value = json_data
    if content is not None:
        resp.content = content
    return resp


@patch("langchain_monza.sandbox.httpx.Client")
def test_create_sandbox_sets_id(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(
        json_data={"id": "sb-123"},
    )

    sb = MonzaSandbox(base_url="http://localhost:8080")

    assert sb.id == "sb-123"
    mock_client.post.assert_called_once()
    call_kw = mock_client.post.call_args[1]
    assert call_kw["json"]["template"] == "python-3.12"


@patch("langchain_monza.sandbox.httpx.Client")
def test_attach_to_sandbox_validates_id(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.get.return_value = _make_mock_response(status_code=200)

    sb = MonzaSandbox(base_url="http://localhost:8080", sandbox_id="sb-existing")

    assert sb.id == "sb-existing"
    mock_client.get.assert_called_once()
    assert mock_client.get.call_args[0][0] == "/api/sandboxes/sb-existing"


@patch("langchain_monza.sandbox.httpx.Client")
def test_execute_returns_stdout(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.side_effect = [
        _make_mock_response(json_data={"id": "sb-123"}),
        _make_mock_response(
            json_data={
                "output": "hello world",
                "exitCode": 0,
                "truncated": False,
            },
        ),
    ]

    sb = MonzaSandbox(base_url="http://localhost:8080")
    result = sb.execute("echo hello world")

    assert result.output == "hello world"
    assert result.exit_code == 0
    assert result.truncated is False
    execute_call = mock_client.post.call_args_list[1]
    assert execute_call[0][0] == "/api/sandboxes/sb-123/execute"
    assert execute_call[1]["json"]["command"] == "echo hello world"


@patch("langchain_monza.sandbox.httpx.Client")
def test_execute_http_error_returns_error_response(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.side_effect = [
        _make_mock_response(json_data={"id": "sb-123"}),
        httpx.HTTPError("connection refused"),
    ]

    sb = MonzaSandbox(base_url="http://localhost:8080")
    result = sb.execute("echo hi")

    assert result.exit_code is None
    assert "HTTP error" in result.output
    assert result.truncated is False


@patch("langchain_monza.sandbox.httpx.Client")
def test_download_files_success(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})
    mock_client.get.return_value = _make_mock_response(content=b"file content")

    sb = MonzaSandbox(base_url="http://localhost:8080")
    responses = sb.download_files(["/workspace/foo.txt"])

    assert len(responses) == 1
    assert responses[0].path == "/workspace/foo.txt"
    assert responses[0].content == b"file content"
    assert responses[0].error is None


@patch("langchain_monza.sandbox.httpx.Client")
def test_download_files_invalid_path(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})

    sb = MonzaSandbox(base_url="http://localhost:8080")
    responses = sb.download_files(["relative/path"])

    assert len(responses) == 1
    assert responses[0].path == "relative/path"
    assert responses[0].content is None
    assert responses[0].error == "invalid_path"


@patch("langchain_monza.sandbox.httpx.Client")
def test_upload_files_success(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.side_effect = [
        _make_mock_response(json_data={"id": "sb-123"}),
        _make_mock_response(status_code=200),
    ]

    sb = MonzaSandbox(base_url="http://localhost:8080")
    responses = sb.upload_files([("/workspace/bar.txt", b"content")])

    assert len(responses) == 1
    assert responses[0].path == "/workspace/bar.txt"
    assert responses[0].error is None


@patch("langchain_monza.sandbox.httpx.Client")
def test_upload_files_invalid_path(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})

    sb = MonzaSandbox(base_url="http://localhost:8080")
    responses = sb.upload_files([("no_leading_slash", b"x")])

    assert len(responses) == 1
    assert responses[0].path == "no_leading_slash"
    assert responses[0].error == "invalid_path"


@patch("langchain_monza.sandbox.httpx.Client")
def test_delete_calls_api(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})

    sb = MonzaSandbox(base_url="http://localhost:8080")
    sb.delete()

    mock_client.delete.assert_called_once_with("/api/sandboxes/sb-123")


@patch("langchain_monza.sandbox.httpx.Client")
def test_close_clients_client(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})

    sb = MonzaSandbox(base_url="http://localhost:8080")
    sb.close()

    mock_client.close.assert_called_once()


@patch("langchain_monza.sandbox.httpx.Client")
def test_context_manager_closes_client(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    mock_client.post.return_value = _make_mock_response(json_data={"id": "sb-123"})

    with MonzaSandbox(base_url="http://localhost:8080") as sb:
        assert sb.id == "sb-123"

    mock_client.close.assert_called_once()


@patch("langchain_monza.sandbox.httpx.Client")
def test_attach_get_raises_for_status(mock_client_class: MagicMock) -> None:
    mock_client = MagicMock()
    mock_client_class.return_value = mock_client
    resp = _make_mock_response(status_code=404)
    resp.raise_for_status.side_effect = httpx.HTTPStatusError(
        "404", request=MagicMock(), response=MagicMock()
    )

    mock_client.get.return_value = resp

    with pytest.raises(httpx.HTTPStatusError):
        MonzaSandbox(base_url="http://localhost:8080", sandbox_id="nonexistent")
