# tests/test_adapters.py
from unittest.mock import AsyncMock, MagicMock
from typing import Any
from agentsh_secure_sandbox.core.types import ExecOpts
from agentsh_secure_sandbox.adapters.e2b import e2b
from agentsh_secure_sandbox.adapters.daytona import daytona
from agentsh_secure_sandbox.adapters.cloudflare import cloudflare
from agentsh_secure_sandbox.adapters.blaxel import blaxel
from agentsh_secure_sandbox.adapters.vercel import vercel
from agentsh_secure_sandbox.adapters.sprites import sprites, sprites_defaults
from agentsh_secure_sandbox.adapters.modal import modal, modal_defaults


# --- E2B ---


async def test_e2b_exec_basic():
    mock_sb = MagicMock()
    mock_sb.commands.run = AsyncMock(
        return_value=MagicMock(
            stdout="hello",
            stderr="",
            exit_code=0,
        )
    )
    adapter = e2b(mock_sb)
    result = await adapter.exec("echo", ["hello"])
    assert result.stdout == "hello"
    assert result.exit_code == 0


async def test_e2b_exec_sudo():
    mock_sb = MagicMock()
    mock_sb.commands.run = AsyncMock(
        return_value=MagicMock(
            stdout="",
            stderr="",
            exit_code=0,
        )
    )
    adapter = e2b(mock_sb)
    await adapter.exec("ls", opts=ExecOpts(sudo=True))
    call_kwargs = mock_sb.commands.run.call_args
    assert call_kwargs.kwargs.get("user") == "root" or call_kwargs[1].get("user") == "root"


async def test_e2b_exec_nonzero_exception():
    """E2B throws on non-zero; adapter must normalize."""
    mock_sb = MagicMock()
    err = Exception("command failed")
    err.stdout = "partial"
    err.stderr = "error msg"
    err.exit_code = 42
    mock_sb.commands.run = AsyncMock(side_effect=err)
    adapter = e2b(mock_sb)
    result = await adapter.exec("bad_cmd")
    assert result.exit_code == 42
    assert result.stderr == "error msg"


async def test_e2b_write_file():
    mock_sb = MagicMock()
    mock_sb.files.write = AsyncMock()
    adapter = e2b(mock_sb)
    await adapter.write_file("/tmp/test.txt", "content")
    mock_sb.files.write.assert_called_once()


# --- Daytona ---


async def test_daytona_exec_basic():
    mock_sb = MagicMock()
    mock_sb.process.execute_command = AsyncMock(
        return_value=MagicMock(
            result="output",
            exit_code=0,
        )
    )
    adapter = daytona(mock_sb)
    result = await adapter.exec("echo", ["hi"])
    assert result.stdout == "output"
    assert result.stderr == ""  # Daytona limitation


async def test_daytona_exec_sudo():
    mock_sb = MagicMock()
    mock_sb.process.execute_command = AsyncMock(
        return_value=MagicMock(
            result="",
            exit_code=0,
        )
    )
    adapter = daytona(mock_sb)
    await adapter.exec("ls", opts=ExecOpts(sudo=True))
    cmd_arg = mock_sb.process.execute_command.call_args[0][0]
    assert cmd_arg.startswith("sudo ")


async def test_daytona_stop_is_noop():
    mock_sb = MagicMock()
    adapter = daytona(mock_sb)
    await adapter.stop()  # Should not raise


# --- Cloudflare ---


async def test_cloudflare_exec_basic():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(
        return_value=MagicMock(
            stdout="ok",
            stderr="",
            exit_code=0,
        )
    )
    adapter = cloudflare(mock_sb)
    result = await adapter.exec("echo", ["hi"])
    assert result.stdout == "ok"


async def test_cloudflare_write_file():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=MagicMock(exit_code=0, stderr=""))
    adapter = cloudflare(mock_sb)
    await adapter.write_file("/tmp/test", "hello")
    assert mock_sb.exec.called


async def test_cloudflare_read_file():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(
        return_value=MagicMock(
            stdout="file content",
            stderr="",
            exit_code=0,
        )
    )
    adapter = cloudflare(mock_sb)
    content = await adapter.read_file("/tmp/test")
    assert content == "file content"


# --- Blaxel ---


async def test_blaxel_exec_basic():
    mock_sb = MagicMock()
    mock_sb.process.exec = AsyncMock(
        return_value=MagicMock(
            stdout="ok",
            stderr="",
            exit_code=0,
        )
    )
    adapter = blaxel(mock_sb)
    result = await adapter.exec("echo", ["hi"])
    assert result.stdout == "ok"


# --- Vercel ---


async def test_vercel_exec_basic():
    mock_sb = MagicMock()
    mock_sb.run_command = AsyncMock(
        return_value=MagicMock(
            stdout="ok",
            stderr="",
            exit_code=0,
        )
    )
    adapter = vercel(mock_sb)
    result = await adapter.exec("echo", ["hi"])
    assert result.stdout == "ok"


# --- Sprites ---


def _sprites_adapter(
    stdout: str = "", stderr: str = "", exit_code: int = 0
) -> tuple[Any, MagicMock]:
    """Create a sprites adapter with _sh mocked — no sprites-py SDK needed."""
    mock_sprite = MagicMock()
    mock_sprite.delete = MagicMock()
    mock_sprite._last_sh_cmd = None

    adapter = sprites(mock_sprite)

    async def fake_sh(cmd: str, **opts: Any) -> tuple[str, str, int]:
        mock_sprite._last_sh_cmd = cmd
        return stdout, stderr, exit_code

    adapter._sh = fake_sh  # type: ignore[assignment]
    return adapter, mock_sprite


async def test_sprites_exec_basic():
    adapter, mock = _sprites_adapter(stdout="hello\n")
    result = await adapter.exec("echo", ["hello"])
    assert result.stdout == "hello\n"
    assert result.exit_code == 0
    assert "echo hello" in mock._last_sh_cmd


async def test_sprites_exec_sudo():
    adapter, mock = _sprites_adapter()
    await adapter.exec("ls", opts=ExecOpts(sudo=True))
    assert mock._last_sh_cmd.startswith("sudo ")


async def test_sprites_exec_error():
    adapter, _ = _sprites_adapter(
        stdout="partial", stderr="permission denied", exit_code=126
    )
    result = await adapter.exec("sudo", ["whoami"])
    assert result.exit_code == 126
    assert result.stderr == "permission denied"


async def test_sprites_write_file():
    adapter, mock = _sprites_adapter()
    await adapter.write_file("/tmp/test.txt", "hello world")
    assert "base64" in mock._last_sh_cmd


async def test_sprites_write_file_sudo():
    adapter, mock = _sprites_adapter()
    await adapter.write_file("/etc/agentsh/policy.yaml", "rules: []", sudo=True)
    cmd = mock._last_sh_cmd
    assert cmd.startswith("sudo sh -c ")
    # The inner command must be properly shell-quoted (shlex.quote wraps in single quotes
    # and escapes internal single quotes)
    assert "base64" in cmd
    assert "/etc/agentsh/policy.yaml" in cmd


async def test_sprites_write_file_sudo_special_path():
    import base64
    import shlex

    adapter, mock = _sprites_adapter()
    await adapter.write_file("/tmp/it's a test.txt", "data", sudo=True)
    cmd = mock._last_sh_cmd
    b64 = base64.b64encode(b"data").decode("ascii")
    safe_path = "/tmp/it's a test.txt".replace("'", "'\\''")
    expected_inner = f"printf '%s' '{b64}' | base64 -d > '{safe_path}'"
    assert cmd == f"sudo sh -c {shlex.quote(expected_inner)}"


async def test_sprites_read_file():
    adapter, mock = _sprites_adapter(stdout="file content")
    content = await adapter.read_file("/tmp/test.txt")
    assert content == "file content"
    assert "cat" in mock._last_sh_cmd


async def test_sprites_stop():
    adapter, mock = _sprites_adapter()
    await adapter.stop()
    mock.delete.assert_called_once()


async def test_sprites_defaults_values():
    cfg = sprites_defaults()
    assert cfg.install_strategy == "preinstalled"
    assert cfg.real_paths is True
    assert cfg.workspace == "/home/sprite"


# --- Modal ---


def _modal_proc(stdout="", stderr="", returncode=0):
    """Create a mock Modal process."""
    proc = MagicMock()
    proc.wait = AsyncMock()
    proc.stdout = MagicMock()
    proc.stdout.read = AsyncMock(return_value=stdout)
    proc.stderr = MagicMock()
    proc.stderr.read = AsyncMock(return_value=stderr)
    proc.returncode = returncode
    return proc


async def test_modal_exec_basic():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=_modal_proc(stdout="hello"))
    adapter = modal(mock_sb)
    result = await adapter.exec("echo", ["hello"])
    assert result.stdout == "hello"
    assert result.exit_code == 0


async def test_modal_exec_drops_sudo():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=_modal_proc(stdout="root"))
    adapter = modal(mock_sb)
    await adapter.exec("whoami", opts=ExecOpts(sudo=True))
    call_args = mock_sb.exec.call_args[0]
    cmd_str = " ".join(str(a) for a in call_args)
    assert "sudo" not in cmd_str


async def test_modal_exec_detached():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=_modal_proc())
    adapter = modal(mock_sb)
    result = await adapter.exec("sleep", ["100"], opts=ExecOpts(detached=True))
    assert result.exit_code == 0


async def test_modal_write_file():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=_modal_proc())
    adapter = modal(mock_sb)
    await adapter.write_file("/tmp/test.txt", "hello world")
    assert mock_sb.exec.called


async def test_modal_read_file():
    mock_sb = MagicMock()
    mock_sb.exec = AsyncMock(return_value=_modal_proc(stdout="file content"))
    adapter = modal(mock_sb)
    content = await adapter.read_file("/tmp/test.txt")
    assert content == "file content"


async def test_modal_stop():
    mock_sb = MagicMock()
    mock_sb.terminate = AsyncMock()
    adapter = modal(mock_sb)
    await adapter.stop()
    mock_sb.terminate.assert_called_once()


async def test_modal_stop_no_terminate():
    mock_sb = MagicMock(spec=[])  # No terminate attr
    adapter = modal(mock_sb)
    await adapter.stop()  # Should not raise


def test_modal_defaults_has_ptrace():
    cfg = modal_defaults()
    assert cfg.skip_shim is True
    assert cfg.real_paths is True
    assert cfg.install_strategy == "download"
    assert cfg.server_config is not None
    assert cfg.server_config.ptrace is not None
    assert cfg.server_config.ptrace.enabled is True
    assert cfg.server_config.cgroups.enabled is False
    assert cfg.server_config.unix_sockets.enabled is False


def test_modal_defaults_policy_has_system_paths():
    cfg = modal_defaults()
    assert cfg.policy is not None
    file_rules = cfg.policy.file
    all_allows = []
    for r in file_rules[:10]:
        if hasattr(r, "allow"):
            a = r.allow if isinstance(r.allow, list) else [r.allow]
            all_allows.extend(a)
    assert any("/lib/" in a or a == "/lib/**" for a in all_allows)


# --- Protocol conformance ---


async def test_all_adapters_satisfy_protocol():
    """All adapter return types should have the expected methods."""
    mock = MagicMock()
    mock.exec = AsyncMock(return_value=MagicMock(stdout="", stderr="", exit_code=0))
    mock.commands = MagicMock()
    mock.commands.run = AsyncMock(return_value=MagicMock(stdout="", stderr="", exit_code=0))
    mock.process = MagicMock()
    mock.process.execute_command = AsyncMock(return_value=MagicMock(result="", exit_code=0))
    mock.run_command = AsyncMock(return_value=MagicMock(stdout="", stderr="", exit_code=0))
    mock.files = MagicMock()
    mock.files.write = AsyncMock()
    mock.files.read = AsyncMock(return_value="")
    mock.fs = MagicMock()
    mock.fs.upload_file = AsyncMock()
    mock.fs.download_file = AsyncMock(return_value="")
    mock.read_file = AsyncMock(return_value="")
    mock.write_files = AsyncMock()
    mock.stop = AsyncMock()
    mock.kill = AsyncMock()
    mock.delete = AsyncMock()

    # For sprites: mock .command() to return a Cmd-like object
    def _sprites_cmd(*args, **kwargs):
        cmd = MagicMock()
        cmd._capture_stdout = False
        cmd._capture_stderr = False
        cmd._started = False
        cmd._finished = False
        cmd._stdout_data = b""
        cmd._stderr_data = b""
        cmd.timeout = None

        async def fake_run_async():
            return 0

        cmd._run_async = fake_run_async
        return cmd

    mock.command = MagicMock(side_effect=_sprites_cmd)

    for factory in [e2b, daytona, cloudflare, blaxel, vercel, sprites, modal]:
        adapter = factory(mock)
        assert hasattr(adapter, "exec")
        assert hasattr(adapter, "write_file")
        assert hasattr(adapter, "read_file")
        assert hasattr(adapter, "stop")
        assert hasattr(adapter, "file_exists")
