import traceback
from types import SimpleNamespace
from typing import List, Optional
from uuid import uuid4

import pytest
import linecache

from e2b import AsyncTemplate, CopyItem, wait_for_timeout
from e2b.template.types import TemplateBuildStatus
import e2b.template_async.main as template_async_main
import e2b.template_async.build_api as build_api_mod

non_existent_path = "nonexistent/path"

# map template alias -> failed step index
failure_map: dict[str, Optional[int]] = {
    "from_image": 0,
    "from_template": 0,
    "from_dockerfile": 0,
    "from_image_registry": 0,
    "from_aws_registry": 0,
    "from_gcp_registry": 0,
    "copy": None,
    "copy_items": None,
    # multi-source copy produces two COPY instructions (steps 1 and 2),
    # the run_cmd after it is step 3
    "multi_source_copy_second_source": 2,
    "multi_source_copy_next_step": 3,
    "copy_items_second_item": 2,
    "copy_items_next_step": 3,
    "remove": 1,
    "rename": 1,
    "make_dir": 1,
    "make_symlink": 1,
    "run_cmd": 1,
    "set_workdir": 1,
    "set_user": 1,
    "pip_install": 1,
    "npm_install": 1,
    "apt_install": 1,
    "git_clone": 1,
    "set_start_cmd": 1,
    "add_mcp_server": None,
    "beta_dev_container_prebuild": 1,
    "beta_set_dev_container_start": 1,
}


@pytest.fixture(autouse=True)
def mock_template_build(monkeypatch):
    async def mock_request_build(
        client, name: str, tags: Optional[List[str]], cpu_count: int, memory_mb: int
    ):
        return SimpleNamespace(template_id=name, build_id=str(uuid4()), tags=tags or [])

    async def mock_trigger_build(client, template_id: str, build_id: str, template):
        return None

    async def mock_get_file_upload_link(
        client, template_id: str, files_hash: str, stack_trace=None
    ):
        return SimpleNamespace(present=True, url=None)

    async def mock_get_build_status(
        client, template_id: str, build_id: str, logs_offset: int
    ):
        step = failure_map[template_id]
        reason = SimpleNamespace(
            message="Mocked API build error",
            log_entries=[],
            step=str(step) if step is not None else None,
        )
        return SimpleNamespace(
            status=TemplateBuildStatus.ERROR,
            log_entries=[],
            reason=reason,
        )

    monkeypatch.setattr(template_async_main, "request_build", mock_request_build)
    monkeypatch.setattr(template_async_main, "trigger_build", mock_trigger_build)
    monkeypatch.setattr(
        template_async_main, "get_file_upload_link", mock_get_file_upload_link
    )
    monkeypatch.setattr(build_api_mod, "get_build_status", mock_get_build_status)


async def _expect_to_throw_and_check_trace(func, expected_method: str):
    try:
        await func()
        assert False, "Expected AsyncTemplate.build to raise an exception"
    except Exception as e:  # noqa: BLE001 - we want to assert on the traceback regardless of type
        tb = e.__traceback__
        saw_this_file = False
        saw_expected_method = False
        while tb is not None:
            traceback_file = tb.tb_frame.f_code.co_filename
            if traceback_file == __file__:
                saw_this_file = True
                caller_line = linecache.getline(traceback_file, tb.tb_lineno)
                if caller_line and f".{expected_method}(" in caller_line:
                    saw_expected_method = True
                    break
            tb = tb.tb_next
        assert saw_this_file, traceback.format_exc()
        assert saw_expected_method, traceback.format_exc()


@pytest.mark.skip_debug()
async def test_traces_on_from_image(async_build):
    template = AsyncTemplate().from_image("e2b.dev/this-image-does-not-exist")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_image", skip_cache=True), "from_image"
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_template(async_build):
    template = AsyncTemplate().from_template("this-template-does-not-exist")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_template", skip_cache=True),
        "from_template",
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_dockerfile(async_build):
    template = AsyncTemplate().from_dockerfile("FROM ubuntu:22.04\nRUN nonexistent")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_dockerfile", skip_cache=True),
        "from_dockerfile",
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_image_registry(async_build):
    template = AsyncTemplate().from_image(
        "registry.example.com/nonexistent:latest",
        username="test",
        password="test",
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_image_registry", skip_cache=True),
        "from_image",
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_image_credentials():
    await _expect_to_throw_and_check_trace(
        lambda: AsyncTemplate().from_image("ubuntu:22.04", username="user"),
        "from_image",
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_aws_registry(async_build):
    template = AsyncTemplate().from_aws_registry(
        "123456789.dkr.ecr.us-east-1.amazonaws.com/nonexistent:latest",
        access_key_id="test",
        secret_access_key="test",
        region="us-east-1",
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_aws_registry"), "from_aws_registry"
    )


@pytest.mark.skip_debug()
async def test_traces_on_from_gcp_registry(async_build):
    template = AsyncTemplate().from_gcp_registry(
        "gcr.io/nonexistent-project/nonexistent:latest",
        service_account_json={
            "type": "service_account",
        },
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="from_gcp_registry"), "from_gcp_registry"
    )


@pytest.mark.skip_debug()
async def test_traces_on_copy(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().copy(non_existent_path, non_existent_path)
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="copy"), "copy"
    )


@pytest.mark.skip_debug()
async def test_traces_on_copyItems(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().copy_items(
        [CopyItem(src=non_existent_path, dest=non_existent_path)]
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="copy_items"), "copy_items"
    )


@pytest.mark.skip_debug()
async def test_traces_on_second_source_of_multi_source_copy(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="multi_source_copy_second_source"), "copy"
    )


@pytest.mark.skip_debug()
async def test_traces_on_step_after_multi_source_copy(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
    template = template.run_cmd(f"cat {non_existent_path}")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="multi_source_copy_next_step"), "run_cmd"
    )


@pytest.mark.skip_debug()
async def test_traces_on_second_item_of_copy_items(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.copy_items(
        [
            CopyItem(src="test_stacktrace.py", dest="."),
            CopyItem(src="test_tags.py", dest="."),
        ]
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="copy_items_second_item"), "copy_items"
    )


@pytest.mark.skip_debug()
async def test_traces_on_step_after_copy_items(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.copy_items(
        [
            CopyItem(src="test_stacktrace.py", dest="."),
            CopyItem(src="test_tags.py", dest="."),
        ]
    )
    template = template.run_cmd(f"cat {non_existent_path}")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="copy_items_next_step"), "run_cmd"
    )


@pytest.mark.skip_debug()
async def test_traces_on_copy_absolute_path():
    await _expect_to_throw_and_check_trace(
        lambda: AsyncTemplate()
        .from_base_image()
        .copy("/absolute/path", "/absolute/path"),
        "copy",
    )


@pytest.mark.skip_debug()
async def test_traces_on_copyItems_absolute_path():
    await _expect_to_throw_and_check_trace(
        lambda: AsyncTemplate()
        .from_base_image()
        .copy_items([CopyItem(src="/absolute/path", dest="/absolute/path")]),
        "copy_items",
    )


@pytest.mark.skip_debug()
async def test_traces_on_remove(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().remove(non_existent_path)
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="remove"), "remove"
    )


@pytest.mark.skip_debug()
async def test_traces_on_rename(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().rename(non_existent_path, "/tmp/dest.txt")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="rename"), "rename"
    )


@pytest.mark.skip_debug()
async def test_traces_on_make_dir(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.set_user("root").skip_cache().make_dir("/root/.bashrc")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="make_dir"), "make_dir"
    )


@pytest.mark.skip_debug()
async def test_traces_on_make_symlink(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().make_symlink(".bashrc", ".bashrc")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="make_symlink"), "make_symlink"
    )


@pytest.mark.skip_debug()
async def test_traces_on_run_cmd(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().run_cmd(f"cat {non_existent_path}")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="run_cmd"), "run_cmd"
    )


@pytest.mark.skip_debug()
async def test_traces_on_set_workdir(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.set_user("root").skip_cache().set_workdir("/root/.bashrc")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="set_workdir"), "set_workdir"
    )


@pytest.mark.skip_debug()
async def test_traces_on_set_user(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().set_user("; exit 1")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="set_user"), "set_user"
    )


@pytest.mark.skip_debug()
async def test_traces_on_pip_install(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().pip_install("nonexistent-package")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="pip_install"), "pip_install"
    )


@pytest.mark.skip_debug()
async def test_traces_on_npm_install(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().npm_install("nonexistent-package")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="npm_install"), "npm_install"
    )


@pytest.mark.skip_debug()
async def test_traces_on_apt_install(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().apt_install("nonexistent-package")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="apt_install"), "apt_install"
    )


@pytest.mark.skip_debug()
async def test_traces_on_git_clone(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.skip_cache().git_clone("https://github.com/repo.git")
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="git_clone"), "git_clone"
    )


@pytest.mark.skip_debug()
async def test_traces_on_start_cmd(async_build):
    template = AsyncTemplate()
    template = template.from_base_image()
    template = template.set_start_cmd(
        f"./{non_existent_path}", wait_for_timeout(10_000)
    )
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="set_start_cmd"), "set_start_cmd"
    )


@pytest.mark.skip_debug()
async def test_traces_on_add_mcp_server():
    # needs mcp-gateway as base template, without it no mcp servers can be added
    await _expect_to_throw_and_check_trace(
        lambda: AsyncTemplate().from_base_image().skip_cache().add_mcp_server("exa"),
        "add_mcp_server",
    )


@pytest.mark.skip_debug()
async def test_traces_on_dev_container_prebuild(async_build):
    template = AsyncTemplate()
    template = template.from_template("devcontainer")
    template = template.skip_cache().beta_dev_container_prebuild(non_existent_path)
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="beta_dev_container_prebuild"),
        "beta_dev_container_prebuild",
    )


@pytest.mark.skip_debug()
async def test_traces_on_set_dev_container_start(async_build):
    template = AsyncTemplate()
    template = template.from_template("devcontainer")
    template = template.beta_set_dev_container_start(non_existent_path)
    await _expect_to_throw_and_check_trace(
        lambda: async_build(template, name="beta_set_dev_container_start"),
        "beta_set_dev_container_start",
    )
