"""Tests for nanobot.agent.tools.sandbox."""

import shlex

import pytest

from nanobot.agent.tools.sandbox import wrap_command


def _parse(cmd: str) -> list[str]:
    """Split a wrapped command back into tokens for assertion."""
    return shlex.split(cmd)


class TestBwrapBackend:
    def test_basic_structure(self, tmp_path):
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "echo hi", ws, ws)
        tokens = _parse(result)

        assert tokens[0] == "bwrap"
        assert "--new-session" in tokens
        assert "--die-with-parent" in tokens
        assert "--ro-bind" in tokens
        assert "--proc" in tokens
        assert "--dev" in tokens
        assert "--tmpfs" in tokens

        sep = tokens.index("--")
        assert tokens[sep + 1:] == ["sh", "-c", "echo hi"]

    def test_workspace_bind_mounted_rw(self, tmp_path):
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "ls", ws, ws)
        tokens = _parse(result)

        bind_idx = [i for i, t in enumerate(tokens) if t == "--bind"]
        assert any(tokens[i + 1] == ws and tokens[i + 2] == ws for i in bind_idx)

    def test_home_env_points_to_workspace(self, tmp_path):
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "echo $HOME", ws, ws)
        tokens = _parse(result)

        setenv_idx = [i for i, t in enumerate(tokens) if t == "--setenv"]
        assert any(
            tokens[i + 1] == "HOME" and tokens[i + 2] == str(tmp_path / "project")
            for i in setenv_idx
        )

    def test_parent_dir_masked_with_tmpfs(self, tmp_path):
        ws = tmp_path / "project"
        result = wrap_command("bwrap", "ls", str(ws), str(ws))
        tokens = _parse(result)

        tmpfs_indices = [i for i, t in enumerate(tokens) if t == "--tmpfs"]
        tmpfs_targets = {tokens[i + 1] for i in tmpfs_indices}
        assert str(ws.parent) in tmpfs_targets

    def test_tmp_dir_mounted_as_tmpfs(self, tmp_path):
        """Regression coverage for #1948: commands need writable scratch space."""
        ws = tmp_path / "project"
        result = wrap_command("bwrap", "touch /tmp/probe", str(ws), str(ws))
        tokens = _parse(result)

        tmpfs_indices = [i for i, t in enumerate(tokens) if t == "--tmpfs"]
        tmpfs_targets = {tokens[i + 1] for i in tmpfs_indices}
        assert "/tmp" in tmpfs_targets

    def test_parent_mask_precedes_workspace_recreation(self, tmp_path):
        ws = tmp_path / "project"
        result = wrap_command("bwrap", "ls", str(ws), str(ws))
        tokens = _parse(result)

        parent_mask = next(
            i for i, t in enumerate(tokens)
            if t == "--tmpfs" and tokens[i + 1] == str(ws.parent)
        )
        workspace_dir = next(
            i for i, t in enumerate(tokens)
            if t == "--dir" and tokens[i + 1] == str(ws)
        )
        workspace_bind = next(
            i for i, t in enumerate(tokens)
            if t == "--bind" and tokens[i + 1] == str(ws) and tokens[i + 2] == str(ws)
        )
        chdir = tokens.index("--chdir")

        assert parent_mask < workspace_dir < workspace_bind < chdir

    def test_cwd_inside_workspace(self, tmp_path):
        ws = tmp_path / "project"
        sub = ws / "src" / "lib"
        result = wrap_command("bwrap", "pwd", str(ws), str(sub))
        tokens = _parse(result)

        chdir_idx = tokens.index("--chdir")
        assert tokens[chdir_idx + 1] == str(sub)

    def test_cwd_outside_workspace_falls_back(self, tmp_path):
        ws = tmp_path / "project"
        outside = tmp_path / "other"
        result = wrap_command("bwrap", "pwd", str(ws), str(outside))
        tokens = _parse(result)

        chdir_idx = tokens.index("--chdir")
        assert tokens[chdir_idx + 1] == str(ws.resolve())

    def test_command_with_special_characters(self, tmp_path):
        ws = str(tmp_path / "project")
        cmd = "echo 'hello world' && cat \"file with spaces.txt\""
        result = wrap_command("bwrap", cmd, ws, ws)
        tokens = _parse(result)

        sep = tokens.index("--")
        assert tokens[sep + 1:] == ["sh", "-c", cmd]

    def test_system_dirs_ro_bound(self, tmp_path):
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "ls", ws, ws)
        tokens = _parse(result)

        ro_bind_indices = [i for i, t in enumerate(tokens) if t == "--ro-bind"]
        ro_targets = {tokens[i + 1] for i in ro_bind_indices}
        assert "/usr" in ro_targets

    def test_optional_dirs_use_ro_bind_try(self, tmp_path):
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "ls", ws, ws)
        tokens = _parse(result)

        try_indices = [i for i, t in enumerate(tokens) if t == "--ro-bind-try"]
        try_targets = {tokens[i + 1] for i in try_indices}
        assert "/bin" in try_targets
        assert "/etc/ssl/certs" in try_targets

    def test_media_dir_ro_bind(self, tmp_path, monkeypatch):
        """Media directory should be read-only mounted inside the sandbox."""
        fake_media = tmp_path / "media"
        fake_media.mkdir()
        monkeypatch.setattr(
            "nanobot.agent.tools.sandbox.get_media_dir",
            lambda: fake_media,
        )
        ws = str(tmp_path / "project")
        result = wrap_command("bwrap", "ls", ws, ws)
        tokens = _parse(result)

        try_indices = [i for i, t in enumerate(tokens) if t == "--ro-bind-try"]
        try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in try_indices}
        assert (str(fake_media), str(fake_media)) in try_pairs

    def test_custom_read_only_binds_use_ro_bind_try(self, tmp_path):
        ws = tmp_path / "project"
        tool_bin = tmp_path / "home" / ".local" / "bin"

        result = wrap_command(
            "bwrap",
            "uv --version",
            str(ws),
            str(ws),
            sandbox_ro_binds=[str(tool_bin)],
        )
        tokens = _parse(result)

        try_indices = [i for i, t in enumerate(tokens) if t == "--ro-bind-try"]
        try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in try_indices}
        assert (str(tool_bin.resolve(strict=False)), str(tool_bin.resolve(strict=False))) in try_pairs

    def test_custom_read_write_binds_use_bind_try(self, tmp_path):
        ws = tmp_path / "project"
        cache_dir = tmp_path / "cache"

        result = wrap_command(
            "bwrap",
            "touch cache/file",
            str(ws),
            str(ws),
            sandbox_rw_binds=[str(cache_dir)],
        )
        tokens = _parse(result)

        bind_try_indices = [i for i, t in enumerate(tokens) if t == "--bind-try"]
        bind_try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in bind_try_indices}
        resolved = str(cache_dir.resolve(strict=False))
        assert (resolved, resolved) in bind_try_pairs

    def test_custom_relative_bind_paths_are_ignored(self, tmp_path):
        ws = tmp_path / "project"

        result = wrap_command(
            "bwrap",
            "ls",
            str(ws),
            str(ws),
            sandbox_ro_binds=["relative/bin"],
            sandbox_rw_binds=["relative/cache"],
        )
        tokens = _parse(result)

        assert "relative/bin" not in tokens
        assert "relative/cache" not in tokens

    def test_custom_workspace_parent_binds_are_ignored(self, tmp_path):
        ws = tmp_path / "private" / "project"
        parent = ws.parent.resolve(strict=False)

        result = wrap_command(
            "bwrap",
            "cat ../config.json",
            str(ws),
            str(ws),
            sandbox_ro_binds=[str(parent)],
            sandbox_rw_binds=[str(parent)],
        )
        tokens = _parse(result)

        ro_try_indices = [i for i, token in enumerate(tokens) if token == "--ro-bind-try"]
        ro_try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in ro_try_indices}
        bind_try_indices = [i for i, token in enumerate(tokens) if token == "--bind-try"]
        bind_try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in bind_try_indices}
        assert (str(parent), str(parent)) not in ro_try_pairs
        assert (str(parent), str(parent)) not in bind_try_pairs


class TestUnknownBackend:
    def test_raises_value_error(self, tmp_path):
        ws = str(tmp_path / "project")
        with pytest.raises(ValueError, match="Unknown sandbox backend"):
            wrap_command("nonexistent", "ls", ws, ws)

    def test_empty_string_raises(self, tmp_path):
        ws = str(tmp_path / "project")
        with pytest.raises(ValueError):
            wrap_command("", "ls", ws, ws)
