"""terminal_exec — envelope shape, semantic exits, warnings, auto-promotion."""

from __future__ import annotations

import time

import pytest


@pytest.fixture
def exec_tool(mcp):
    from terminal_tools.exec import register_exec_tools

    register_exec_tools(mcp)
    return mcp._tool_manager._tools["terminal_exec"].fn


def test_envelope_shape_simple_echo(exec_tool):
    result = exec_tool(command="echo hello world")
    assert result["exit_code"] == 0
    assert result["stdout"].strip() == "hello world"
    assert result["stderr"] == ""
    assert result["semantic_status"] == "ok"
    assert result["timed_out"] is False
    assert result["auto_backgrounded"] is False
    assert result["job_id"] is None
    assert result["warning"] is None
    assert result["pid"] is not None


def test_grep_no_matches_is_ok_not_error(exec_tool, tmp_path):
    f = tmp_path / "haystack.txt"
    f.write_text("apples\nbananas\n")
    result = exec_tool(command=f"grep zzz {f}")
    assert result["exit_code"] == 1
    assert result["semantic_status"] == "ok"
    assert "No matches found" in (result["semantic_message"] or "")


def test_diff_files_differ_is_ok_not_error(exec_tool, tmp_path):
    a = tmp_path / "a.txt"
    a.write_text("hi\n")
    b = tmp_path / "b.txt"
    b.write_text("bye\n")
    result = exec_tool(command=f"diff {a} {b}")
    assert result["exit_code"] == 1
    assert result["semantic_status"] == "ok"
    assert "differ" in (result["semantic_message"] or "")


def test_destructive_warning_for_rm_rf(exec_tool, tmp_path):
    # Don't actually delete anything — point at a missing path so the
    # command exits non-zero but the warning still fires from regex.
    target = tmp_path / "definitely_missing_dir"
    result = exec_tool(command=f"rm -rf {target}")
    assert result["warning"] is not None
    assert "force-remove" in result["warning"] or "recursively" in result["warning"]


def test_destructive_warning_drop_table(exec_tool):
    # Run `true` so the test doesn't depend on echo behavior; pass the
    # destructive text via stdin so the regex still matches the command.
    result = exec_tool(command="echo 'DROP TABLE users;'", shell=True)
    assert result["warning"] is not None
    assert "drop" in result["warning"].lower() or "truncate" in result["warning"].lower()


def test_command_not_found(exec_tool):
    result = exec_tool(command="this_command_does_not_exist_xyzzy")
    assert result["exit_code"] is None or result["exit_code"] != 0
    # Either pre-spawn FileNotFoundError or shell exit 127 — both are fine
    # as long as semantic_status reflects an error or the error field is set.
    assert (
        result["semantic_status"] == "error"
        or result.get("error")
        or "not found" in (result["semantic_message"] or "").lower()
    )


def test_zsh_refused(exec_tool):
    result = exec_tool(command="echo hi", shell=True)
    # shell=True (the bool) → /bin/bash → succeeds
    assert result["exit_code"] == 0


def test_zsh_string_refused():
    """Calling _resolve_shell with zsh path raises ZshRefused."""
    from terminal_tools.common.limits import ZshRefused, _resolve_shell

    with pytest.raises(ZshRefused):
        _resolve_shell("/bin/zsh")
    with pytest.raises(ZshRefused):
        _resolve_shell("/usr/local/bin/zsh")


def test_truncation_via_handle(exec_tool):
    """Generate >256 KB of output, verify output_handle is returned."""
    # Generate ~300 KB of output
    result = exec_tool(
        command="python3 -c 'import sys; sys.stdout.write(\"x\" * 300_000)'",
        shell=True,
        max_output_kb=128,  # smaller cap to force truncation
    )
    assert result["exit_code"] == 0
    assert result["stdout_truncated_bytes"] > 0
    assert result["output_handle"] is not None
    assert result["output_handle"].startswith("out_")


def test_output_handle_round_trip(exec_tool, mcp):
    from terminal_tools.output import register_output_tools

    register_output_tools(mcp)
    output_get = mcp._tool_manager._tools["terminal_output_get"].fn

    result = exec_tool(
        command="python3 -c 'import sys; sys.stdout.write(\"x\" * 300_000)'",
        shell=True,
        max_output_kb=64,
    )
    handle = result["output_handle"]
    assert handle is not None

    # First page
    page = output_get(output_handle=handle, since_offset=0, max_kb=64)
    assert page["expired"] is False
    assert len(page["data"]) > 0
    assert page["next_offset"] > 0

    # Bogus handle
    bogus = output_get(output_handle="out_doesnotexist", since_offset=0, max_kb=64)
    assert bogus["expired"] is True


def test_timed_out_marker(exec_tool):
    result = exec_tool(command="sleep 5", timeout_sec=1, auto_background_after_sec=0)
    assert result["timed_out"] is True


def test_auto_shell_for_pipelines(exec_tool):
    """Regression for the queen_technology session 152038 silent-mangling bug.

    The agent passed shell=False (default) with a pipeline command. The naive
    command.split() spawned the first program with the rest as junk argv —
    `ps aux | sort ...` produced "ps: error: garbage option", and `echo "..."
    && ps ...` produced fake-success output where echo printed the entire
    rest of the command verbatim. Fix: detect shell metacharacters and
    transparently route through bash -c.
    """
    # Case 1: pipeline. Was: ps spawned with "aux | sort ..." as argv → garbage option.
    result = exec_tool(command="ps aux | head -1")
    assert result["exit_code"] == 0, result
    assert result["auto_shell"] is True
    assert "USER" in result["stdout"] or "PID" in result["stdout"]
    assert "garbage option" not in (result.get("stderr") or "")

    # Case 2: && + pipe + awk. Was: echo printed the whole rest of the line.
    result = exec_tool(
        command="echo HEADER && echo line1 | head -1",
    )
    assert result["exit_code"] == 0, result
    assert result["auto_shell"] is True
    assert "HEADER" in result["stdout"]
    assert "line1" in result["stdout"]
    # The literal "&&" must NOT appear in stdout — that would mean echo
    # captured it as an argument again.
    assert "&&" not in result["stdout"]

    # Case 3: redirect + glob. Was: '*' passed as a literal arg to ls.
    import os
    import tempfile

    with tempfile.TemporaryDirectory() as tmp:
        with open(os.path.join(tmp, "a.txt"), "w") as f:
            f.write("x")
        with open(os.path.join(tmp, "b.txt"), "w") as f:
            f.write("y")
        result = exec_tool(command=f"ls {tmp}/*.txt")
        assert result["exit_code"] == 0, result
        assert result["auto_shell"] is True
        assert "a.txt" in result["stdout"]
        assert "b.txt" in result["stdout"]


def test_no_auto_shell_for_argv_commands(exec_tool):
    """Plain argv commands (no metacharacters) should NOT auto-route to bash.
    Direct exec is faster and avoids quoting hazards."""
    result = exec_tool(command="echo hello")
    assert result["exit_code"] == 0
    assert result["auto_shell"] is False
    assert result["stdout"].strip() == "hello"


def test_explicit_shell_true_unchanged(exec_tool):
    """When the agent explicitly opts in via shell=True, auto_shell stays
    False — auto-detection only fires for shell=False."""
    result = exec_tool(command="echo a | tr a b", shell=True)
    assert result["exit_code"] == 0
    assert result["auto_shell"] is False
    assert result["stdout"].strip() == "b"


def test_auto_promotion(exec_tool, mcp):
    """Past auto_background_after_sec, the call returns auto_backgrounded=True."""
    from terminal_tools.jobs.tools import register_job_tools

    register_job_tools(mcp)
    # Use a 1s budget so the test runs quickly.
    start = time.monotonic()
    result = exec_tool(
        command="sleep 5",
        auto_background_after_sec=1,
        timeout_sec=10,
    )
    elapsed = time.monotonic() - start
    assert result["auto_backgrounded"] is True, result
    assert result["job_id"] is not None
    assert result["exit_code"] is None
    assert elapsed < 3, "auto-promotion should return quickly past the budget"

    # Take over via terminal_job_logs
    job_logs = mcp._tool_manager._tools["terminal_job_logs"].fn
    log_result = job_logs(job_id=result["job_id"], wait_until_exit=True, wait_timeout_sec=10)
    assert log_result["status"] == "exited"
    assert log_result["exit_code"] == 0
