from relace_mcp.tools.search.handlers import _is_blocked_command

DEFAULT_BASE_DIR = "/repo"


class TestIsBlockedCommand:
    """Test command blocking logic."""

    def test_blocks_rm(self) -> None:
        blocked, _ = _is_blocked_command("rm file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_rm_rf(self) -> None:
        blocked, _ = _is_blocked_command("rm -rf /", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_full_path_rm(self) -> None:
        blocked, _ = _is_blocked_command("/bin/rm file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_allows_ls(self) -> None:
        blocked, _ = _is_blocked_command("ls -la", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_cat(self) -> None:
        blocked, _ = _is_blocked_command("cat file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_grep(self) -> None:
        blocked, _ = _is_blocked_command("grep pattern file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_find(self) -> None:
        blocked, _ = _is_blocked_command("find . -name '*.py'", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_git_log(self) -> None:
        blocked, _ = _is_blocked_command("git log -n 10", DEFAULT_BASE_DIR)
        assert not blocked

    def test_blocks_pipe(self) -> None:
        blocked, _ = _is_blocked_command("cat file | grep pattern", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_redirect_to_file(self) -> None:
        blocked, _ = _is_blocked_command("echo test > output.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_append_redirect(self) -> None:
        blocked, _ = _is_blocked_command("echo test >> output.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_empty_command(self) -> None:
        blocked, _ = _is_blocked_command("", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_semicolon_rm(self) -> None:
        blocked, _ = _is_blocked_command("ls; rm file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_and_rm(self) -> None:
        blocked, _ = _is_blocked_command("ls && rm file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_multiline_commands(self) -> None:
        blocked, _ = _is_blocked_command("ls\nls", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_process_substitution(self) -> None:
        """Should block process substitution (e.g., <(cmd)) which can execute arbitrary commands."""
        blocked, _ = _is_blocked_command("cat <(whoami)", DEFAULT_BASE_DIR)
        assert blocked


class TestAbsolutePathBlocking:
    """Test absolute path sandbox enforcement."""

    def test_blocks_cat_etc_passwd(self) -> None:
        """Should block reading /etc/passwd."""
        blocked, reason = _is_blocked_command("cat /etc/passwd", DEFAULT_BASE_DIR)
        assert blocked
        assert "/etc/passwd" in reason or "Absolute path" in reason

    def test_blocks_cat_etc_shadow(self) -> None:
        """Should block reading /etc/shadow."""
        blocked, reason = _is_blocked_command("cat /etc/shadow", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_find_root(self) -> None:
        """Should block find starting from root."""
        blocked, reason = _is_blocked_command("find / -name '*.py'", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_ls_home(self) -> None:
        """Should block listing home directory."""
        blocked, reason = _is_blocked_command("ls /home", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_head_var_log(self) -> None:
        """Should block reading system logs."""
        blocked, reason = _is_blocked_command("head /var/log/syslog", DEFAULT_BASE_DIR)
        assert blocked

    def test_allows_repo_path(self) -> None:
        """Should allow /repo paths."""
        blocked, _ = _is_blocked_command("cat /repo/file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_repo_subpath(self) -> None:
        """Should allow /repo/subdir paths."""
        blocked, _ = _is_blocked_command("ls /repo/src/", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_relative_path(self) -> None:
        """Should allow relative paths."""
        blocked, _ = _is_blocked_command("cat ./file.txt", DEFAULT_BASE_DIR)
        assert not blocked


class TestRelativeTraversalBlocking:
    """Test blocking traversal via relative paths."""

    def test_blocks_ls_parent_dir(self) -> None:
        """Should block listing parent directory."""
        blocked, _ = _is_blocked_command("ls ..", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_find_parent_dir(self) -> None:
        """Should block find starting from parent directory."""
        blocked, _ = _is_blocked_command("find .. -name '*.py'", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_dotdot_path_token(self) -> None:
        """Should block path tokens that end with /.."""
        blocked, _ = _is_blocked_command("ls ./..", DEFAULT_BASE_DIR)
        assert blocked


class TestWriteOperationBlocking:
    """Test blocking of write/modify operations."""

    def test_blocks_touch(self) -> None:
        """Should block touch command."""
        blocked, _ = _is_blocked_command("touch newfile.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_tee(self) -> None:
        """Should block tee command."""
        blocked, _ = _is_blocked_command("tee output.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_sed_inplace(self) -> None:
        """Should block sed -i (in-place edit)."""
        blocked, _ = _is_blocked_command("sed -i 's/old/new/g' file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_allows_sed_script_containing_dash_i(self) -> None:
        """Should not false-positive on '-i' inside sed script text."""
        blocked, _ = _is_blocked_command("sed 's/this-is-fine/ok/g' file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_blocks_sed_inplace_combined_short_options(self) -> None:
        """Should block -i even when combined with other short options."""
        blocked, _ = _is_blocked_command("sed -nri 's/old/new/g' file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_mkdir(self) -> None:
        """Should block mkdir command."""
        blocked, _ = _is_blocked_command("mkdir newdir", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_ln(self) -> None:
        """Should block ln (symlink creation)."""
        blocked, _ = _is_blocked_command("ln -s target link", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_find_exec(self) -> None:
        """Should block find -exec."""
        blocked, _ = _is_blocked_command("find . -name '*.py' -exec rm {} \\;", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_find_delete(self) -> None:
        """Should block find -delete."""
        blocked, _ = _is_blocked_command("find . -name '*.pyc' -delete", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_xargs_rm(self) -> None:
        """Should block xargs with rm."""
        blocked, _ = _is_blocked_command("find . -name '*.tmp' | xargs rm", DEFAULT_BASE_DIR)
        assert blocked


class TestGitSecurityBlocking:
    """Test git subcommand security."""

    def test_allows_git_log(self) -> None:
        """Should allow git log."""
        blocked, _ = _is_blocked_command("git log -n 10", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_git_show(self) -> None:
        """Should allow git show."""
        blocked, _ = _is_blocked_command("git show HEAD", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_git_diff(self) -> None:
        """Should allow git diff."""
        blocked, _ = _is_blocked_command("git diff HEAD~1", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_git_status(self) -> None:
        """Should allow git status."""
        blocked, _ = _is_blocked_command("git status", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_git_blame(self) -> None:
        """Should allow git blame."""
        blocked, _ = _is_blocked_command("git blame file.py", DEFAULT_BASE_DIR)
        assert not blocked

    def test_blocks_git_clone(self) -> None:
        """Should block git clone (network operation)."""
        blocked, _ = _is_blocked_command("git clone https://github.com/user/repo", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_fetch(self) -> None:
        """Should block git fetch (network operation)."""
        blocked, _ = _is_blocked_command("git fetch origin", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_pull(self) -> None:
        """Should block git pull (network operation)."""
        blocked, _ = _is_blocked_command("git pull origin main", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_push(self) -> None:
        """Should block git push (network operation)."""
        blocked, _ = _is_blocked_command("git push origin main", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_checkout(self) -> None:
        """Should block git checkout (modifies working tree)."""
        blocked, _ = _is_blocked_command("git checkout -- .", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_reset(self) -> None:
        """Should block git reset (modifies repo state)."""
        blocked, _ = _is_blocked_command("git reset --hard HEAD", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_clean(self) -> None:
        """Should block git clean (deletes files)."""
        blocked, _ = _is_blocked_command("git clean -fd", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_git_commit(self) -> None:
        """Should block git commit (modifies repo)."""
        blocked, _ = _is_blocked_command("git commit -m 'msg'", DEFAULT_BASE_DIR)
        assert blocked


class TestPythonSecurityBlocking:
    """Test Python command security."""

    def test_blocks_python_file_write(self) -> None:
        """Should block Python file write operations."""
        blocked, _ = _is_blocked_command("python -c \"open('f','w').write('x')\"", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_pathlib(self) -> None:
        """Should block Python pathlib usage."""
        blocked, _ = _is_blocked_command(
            "python3 -c \"import pathlib; print(pathlib.Path('x').read_text())\"",
            DEFAULT_BASE_DIR,
        )
        assert blocked

    def test_blocks_http_client(self) -> None:
        """Should block Python http.client usage."""
        blocked, _ = _is_blocked_command('python3 -c "import http.client"', DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_python_requests(self) -> None:
        """Should block Python requests (network)."""
        blocked, _ = _is_blocked_command(
            "python -c \"import requests; requests.get('http://x')\"",
            DEFAULT_BASE_DIR,
        )
        assert blocked

    def test_blocks_python_urllib(self) -> None:
        """Should block Python urllib (network)."""
        blocked, _ = _is_blocked_command('python -c "import urllib.request"', DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_python_subprocess(self) -> None:
        """Should block Python subprocess."""
        blocked, _ = _is_blocked_command('python -c "import subprocess"', DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_python_os_system(self) -> None:
        """Should block Python os.system."""
        blocked, _ = _is_blocked_command(
            "python -c \"import os; os.system('rm -rf /')\"",
            DEFAULT_BASE_DIR,
        )
        assert blocked

    def test_blocks_python_script_execution(self) -> None:
        """Should block Python script file execution."""
        blocked, _ = _is_blocked_command("python script.py", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_python3_script_execution(self) -> None:
        """Should block Python3 script file execution."""
        blocked, _ = _is_blocked_command("python3 script.py", DEFAULT_BASE_DIR)
        assert blocked


class TestPipeAllowedInQuotes:
    """Test that pipe is blocked everywhere for maximum safety (KISS)."""

    def test_blocks_grep_e_with_pipe_pattern(self) -> None:
        """Should block grep -E 'foo|bar' pattern due to strict pipe blocking."""
        blocked, _ = _is_blocked_command("grep -E 'foo|bar' file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_egrep_with_pipe_pattern(self) -> None:
        """Should block egrep 'foo|bar' pattern."""
        blocked, _ = _is_blocked_command("egrep 'foo|bar' file.txt", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_actual_pipe_operator(self) -> None:
        """Should still block actual pipe operator with spaces."""
        blocked, _ = _is_blocked_command("cat file | grep pattern", DEFAULT_BASE_DIR)
        assert blocked


class TestCommandNotInAllowlist:
    """Test that unknown commands are blocked."""

    def test_blocks_unknown_command(self) -> None:
        """Should block commands not in allowlist."""
        blocked, reason = _is_blocked_command("someunknowncommand arg", DEFAULT_BASE_DIR)
        assert blocked
        assert "allowlist" in reason.lower()

    def test_blocks_make(self) -> None:
        """Should block make (build tool)."""
        blocked, _ = _is_blocked_command("make all", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_npm(self) -> None:
        """Should block npm."""
        blocked, _ = _is_blocked_command("npm install", DEFAULT_BASE_DIR)
        assert blocked

    def test_blocks_pip(self) -> None:
        """Should block pip."""
        blocked, _ = _is_blocked_command("pip install requests", DEFAULT_BASE_DIR)
        assert blocked


class TestSandboxEscapeBypassBlocking:
    """Block common bypasses that still look 'read-only' at the shell level."""

    def test_blocks_rg_preprocessor(self) -> None:
        """ripgrep --pre spawns a subprocess per file; must be blocked."""
        blocked, reason = _is_blocked_command("rg --pre=cat pattern .", DEFAULT_BASE_DIR)
        assert blocked
        assert "pre" in reason.lower()

    def test_blocks_awk_system(self) -> None:
        """awk system() can execute arbitrary commands; must be blocked."""
        blocked, reason = _is_blocked_command(
            "awk 'BEGIN{system(\"ls\")}' file.txt", DEFAULT_BASE_DIR
        )
        assert blocked
        assert "awk" in reason.lower() or "system" in reason.lower()

    def test_blocks_sed_write_command(self) -> None:
        """sed `w` can write files without shell redirection; must be blocked."""
        blocked, reason = _is_blocked_command("sed 'w out.txt' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "write" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_script_file(self) -> None:
        """sed -f loads uninspected commands; must be blocked."""
        blocked, reason = _is_blocked_command("sed -f script.sed file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "script" in reason.lower()

    def test_blocks_sed_combined_flags_ew(self) -> None:
        """sed combined flags like /ew must be blocked (e executes shell commands)."""
        blocked, reason = _is_blocked_command("sed 's/foo/bar/ew' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_combined_flags_gew(self) -> None:
        """sed flags /gew must be blocked even when e is not at flag boundary."""
        blocked, reason = _is_blocked_command("sed 's/foo/bar/gew' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_e_flag_alone(self) -> None:
        """sed e flag executes replacement as shell command; must be blocked."""
        blocked, reason = _is_blocked_command("sed 's/foo/bar/e' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_allows_sed_normal_substitution(self) -> None:
        """sed normal substitution without dangerous flags should be allowed."""
        blocked, _ = _is_blocked_command("sed 's/foo/bar/gi' file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_sed_replacement_containing_e(self) -> None:
        """sed with 'e' in replacement text (not flags) should be allowed."""
        blocked, _ = _is_blocked_command("sed 's/test/new/g' file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_blocks_sed_multiple_subst_with_e_in_second(self) -> None:
        """sed with dangerous flag in second substitution must be blocked."""
        # Use -e syntax to avoid ; being caught by command chaining pattern
        blocked, reason = _is_blocked_command(
            "sed -e 's/a/b/g' -e 's/x/y/e' file.txt", DEFAULT_BASE_DIR
        )
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_address_prefixed_e(self) -> None:
        """sed address+e executes pattern space as shell command; must be blocked."""
        blocked, reason = _is_blocked_command("sed 5e file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_range_address_e(self) -> None:
        """sed range+e like 1,10e must be blocked."""
        blocked, reason = _is_blocked_command("sed 1,10e file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_last_line_e(self) -> None:
        """sed $e (last line execute) must be blocked."""
        blocked, reason = _is_blocked_command("sed '$e' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_blocks_sed_address_prefixed_w(self) -> None:
        """sed address+w writes to file; must be blocked."""
        blocked, reason = _is_blocked_command("sed '5w out.txt' file.txt", DEFAULT_BASE_DIR)
        assert blocked
        assert "sed" in reason.lower() or "e/w" in reason.lower()

    def test_allows_sed_print_command(self) -> None:
        """sed print command (p) should be allowed."""
        blocked, _ = _is_blocked_command("sed 5p file.txt", DEFAULT_BASE_DIR)
        assert not blocked

    def test_allows_sed_delete_command(self) -> None:
        """sed delete command (d) should be allowed."""
        blocked, _ = _is_blocked_command("sed 1,10d file.txt", DEFAULT_BASE_DIR)
        assert not blocked
