"""Behavior tests for file-search ordering and ripgrep selection."""

import json
import re

import pytest

from tools.environments.local import LocalEnvironment
from tools.file_operations import SearchResult, ShellFileOperations
from tools.file_tools import search_tool
from tools.registry import registry


class RecordingEnvironment:
    is_local = False
    cwd = "/repo"

    def __init__(self, *, rg_output="/repo/one.py\n/repo/two.py\n", rg_code=0):
        self.commands = []
        self.rg_output = rg_output
        self.rg_code = rg_code

    def execute(self, command, **kwargs):
        self.commands.append(command)
        if command.startswith("test -e "):
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/opt/Rip Grep/rg\n", "returncode": 0}
        if "--version" in command:
            return {"output": "ripgrep 14.1.1\n", "returncode": 0}
        if "--files" in command:
            return {"output": self.rg_output, "returncode": self.rg_code}
        return {"output": "", "returncode": 1}

    @property
    def rg_commands(self):
        return [command for command in self.commands if "--files" in command]




def test_default_file_search_runs_one_bounded_unsorted_rg_command():
    env = RecordingEnvironment()
    ops = ShellFileOperations(env)

    result = ops.search("*.py", path="/repo", target="files", limit=1, offset=1)

    assert result.files == ["/repo/two.py"]
    assert len(env.rg_commands) == 1
    assert "--sortr" not in env.rg_commands[0]
    assert "head -n 3" in env.rg_commands[0]


@pytest.mark.parametrize("engine", ["rg", "find"])
def test_bounded_filename_total_is_serialized_as_a_lower_bound(engine, monkeypatch):
    conceptual_files = [f"/repo/file-{index:03}.py" for index in range(200)]
    env = RecordingEnvironment()

    def execute(command, **kwargs):
        env.commands.append(command)
        if command.startswith("test -e "):
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {
                "output": "/usr/bin/rg\n" if engine == "rg" else "",
                "returncode": 0 if engine == "rg" else 1,
            }
        if "--files" in command or command.startswith("set -o pipefail; find "):
            fetch_limit = int(re.search(r"head -n (\d+)", command).group(1))
            return {
                "output": "\n".join(conceptual_files[:fetch_limit]) + "\n",
                "returncode": 0,
            }
        return {"output": "", "returncode": 1}

    env.execute = execute
    ops = ShellFileOperations(env)
    if engine == "find":
        monkeypatch.setattr(ops, "_has_command", lambda command: command == "find")

    result = ops.search("*.py", path="/repo", target="files", limit=50)
    serialized = result.to_dict()

    assert result.total_count == 51
    assert len(result.files) == 50
    assert serialized["truncated"] is True
    assert serialized["total_count_is_lower_bound"] is True


def test_modified_file_search_runs_one_exact_order_rg_command():
    env = RecordingEnvironment()
    ops = ShellFileOperations(env)

    result = ops.search("*.py", path="/repo", target="files", order="modified")

    assert result.error is None
    assert len(env.rg_commands) == 1
    assert "--sortr=modified" in env.rg_commands[0]


def test_modified_zero_match_exit_one_is_valid_without_capability_error():
    env = RecordingEnvironment(rg_output="", rg_code=1)
    result = ShellFileOperations(env).search(
        "*.missing", path="/repo", target="files", order="modified"
    )
    assert result.error is None
    assert result.files == []
    assert len(env.rg_commands) == 1


@pytest.mark.parametrize(
    "version",
    [
        "ripgrep 13.0.0\n",
        "ripgrep unknown\n",
        "ripgrep 14.1\n",
        "ripgrep 14.1.1-alpha..1\n",
    ],
)
def test_modified_requires_parseable_ripgrep_14_before_search(version):
    env = RecordingEnvironment()

    def execute(command, **kwargs):
        env.commands.append(command)
        if command.startswith("test -e "):
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/opt/Rip Grep/rg\n", "returncode": 0}
        if "--version" in command:
            return {"output": version, "returncode": 0}
        raise AssertionError(f"search must not run: {command}")

    env.execute = execute
    ops = ShellFileOperations(env)
    first = ops.search("*.py", path="/repo", target="files", order="modified")
    second = ops.search("*.py", path="/repo", target="files", order="modified")
    assert "ripgrep 14" in (first.error or "").lower()
    assert second.error == first.error
    assert env.rg_commands == []
    assert len([c for c in env.commands if "--version" in c]) == 1


def test_modified_accepts_complete_ripgrep_semver_with_revision_text():
    env = RecordingEnvironment()
    original_execute = env.execute

    def execute(command, **kwargs):
        if "--version" in command:
            env.commands.append(command)
            return {"output": "ripgrep 14.1.1 (rev abc123)\n", "returncode": 0}
        return original_execute(command, **kwargs)

    env.execute = execute
    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", order="modified"
    )

    assert result.error is None
    assert len(env.rg_commands) == 1


def test_modified_accepts_ripgrep_semver_with_prerelease_and_build_metadata():
    env = RecordingEnvironment()
    original_execute = env.execute

    def execute(command, **kwargs):
        if "--version" in command:
            env.commands.append(command)
            return {
                "output": "ripgrep 14.1.1-alpha.1+build.2\n",
                "returncode": 0,
            }
        return original_execute(command, **kwargs)

    env.execute = execute
    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", order="modified"
    )

    assert result.error is None
    assert len(env.rg_commands) == 1


def test_empty_discovery_output_is_zero_matches_without_retry():
    env = RecordingEnvironment(rg_output="", rg_code=0)
    ops = ShellFileOperations(env)

    result = ops.search("*.missing", path="/repo", target="files")

    assert result.error is None
    assert result.files == []
    assert result.total_count == 0
    assert len(env.rg_commands) == 1


def test_modified_capability_failure_is_actionable_and_not_downgraded():
    env = RecordingEnvironment(rg_output="", rg_code=2)
    ops = ShellFileOperations(env)

    result = ops.search("*.py", path="/repo", target="files", order="modified")

    assert len(env.rg_commands) == 1
    assert result.error is not None
    assert "exact modification-time order" in result.error.lower()
    assert "ripgrep" in result.error


@pytest.mark.parametrize("order", ["discovery", "modified"])
def test_rg_partial_output_with_error_exit_fails_closed(order):
    env = RecordingEnvironment(rg_output="/repo/partial.py\n", rg_code=2)

    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", order=order
    )

    assert result.error is not None
    assert result.files == []
    assert len(env.rg_commands) == 1


@pytest.mark.parametrize("order", ["discovery", "modified"])
def test_rg_sigpipe_is_benign_only_after_fetch_limit_paths(order):
    output = "".join(f"/repo/{name}.py\n" for name in ("a", "b", "c", "d"))
    env = RecordingEnvironment(rg_output=output, rg_code=141)

    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", limit=2, offset=1, order=order
    )

    assert result.error is None
    assert result.files == ["/repo/b.py", "/repo/c.py"]
    assert result.truncated is True


@pytest.mark.parametrize("order", ["discovery", "modified"])
def test_rg_sigpipe_with_fewer_than_fetch_limit_paths_fails_closed(order):
    env = RecordingEnvironment(rg_output="/repo/partial.py\n", rg_code=141)

    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", limit=2, offset=1, order=order
    )

    assert result.error is not None
    assert result.files == []
    assert result.total_count == 0


def test_invalid_direct_file_order_returns_structured_error():
    env = RecordingEnvironment()
    ops = ShellFileOperations(env)

    result = ops.search("*.py", path="/repo", target="files", order="random")

    assert isinstance(result, SearchResult)
    assert result.error and "random" in result.error
    assert env.rg_commands == []




@pytest.mark.parametrize("blank_path", ["", " \t ", None])
def test_handler_normalizes_blank_path_to_current_directory(monkeypatch, blank_path):
    """#112424: a present-but-blank (or null) path must fall back to the documented '.' default."""
    captured = {}

    def fake_search_tool(**kwargs):
        captured.update(kwargs)
        return "{}"

    monkeypatch.setattr("tools.file_tools.search_tool", fake_search_tool)

    registry.dispatch("search_files", {"pattern": "needle", "target": "files", "path": blank_path})

    assert captured["path"] == "."




def test_repeated_search_key_distinguishes_order(monkeypatch):
    class StubOperations:
        def search(self, **kwargs):
            return SearchResult()

    monkeypatch.setattr("tools.file_tools._get_file_ops", lambda task_id: StubOperations())
    task_id = "engine-order-key"
    for _ in range(3):
        assert "BLOCKED" not in json.loads(
            search_tool("*.py", target="files", order="discovery", task_id=task_id)
        ).get("error", "")

    changed = json.loads(
        search_tool("*.py", target="files", order="modified", task_id=task_id)
    )

    assert "BLOCKED" not in changed.get("error", "")


class RipgrepInvocationEnvironment(RecordingEnvironment):
    def execute(self, command, **kwargs):
        self.commands.append(command)
        if command.startswith("test -e "):
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/opt/Rip Grep/rg\n", "returncode": 0}
        if "--version" in command:
            return {"output": "ripgrep 14.1.1\n", "returncode": 0}
        if "--files" in command:
            return {"output": "/repo/a.py\n", "returncode": 0}
        if "--line-number" in command:
            return {"output": "/repo/a.py:1:needle\n", "returncode": 0}
        if "--count-matches" in command:
            return {"output": "", "returncode": 1}
        return {"output": "", "returncode": 1}


def test_resolved_executable_with_spaces_is_used_by_every_rg_invocation():
    env = RipgrepInvocationEnvironment()
    ops = ShellFileOperations(env)

    assert ops.search("*.py", path="/repo", target="files").files
    assert ops.search("needle", path="/repo", target="content").matches
    assert ops._zero_match_probe("absent", "/repo", None) is None

    invocations = [
        command for command in env.commands
        if any(flag in command for flag in ("--files", "--line-number", "--count-matches"))
    ]
    assert invocations
    assert all("'/opt/Rip Grep/rg'" in command for command in invocations)
    assert all(not re.search(r"(?:^|[; ])rg\s", command) for command in invocations)
    assert len([c for c in env.commands if c.startswith("command -v rg")]) == 1




@pytest.mark.platforms("windows")
def test_off_path_windows_rg_miss_is_reprobed_then_success_is_cached(
    tmp_path, monkeypatch
):
    local_app_data = tmp_path / "Local Data"
    candidate = local_app_data / "Microsoft" / "WinGet" / "Links" / "rg.exe"
    monkeypatch.setenv("LOCALAPPDATA", str(local_app_data))
    monkeypatch.setenv("USERPROFILE", str(tmp_path / "User Profile"))
    monkeypatch.delenv("SCOOP", raising=False)
    ops = ShellFileOperations(LocalEnvironment(str(tmp_path)))
    probes = []

    def command_v_miss(command, **kwargs):
        probes.append(command)
        from tools.file_operations import ExecuteResult
        return ExecuteResult(stdout="", exit_code=1)

    monkeypatch.setattr(ops, "_exec", command_v_miss)

    assert ops._resolve_command("rg") is None
    candidate.parent.mkdir(parents=True)
    candidate.write_text("")
    expected = str(candidate).replace("\\", "/")
    assert ops._resolve_command("rg") == expected
    assert ops._resolve_command("rg") == expected
    assert probes == ["command -v rg 2>/dev/null", "command -v rg 2>/dev/null"]


def test_remote_resolution_never_probes_controller_host_paths(tmp_path, monkeypatch):
    monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "controller-local"))
    monkeypatch.setenv("USERPROFILE", str(tmp_path / "controller-user"))
    env = RecordingEnvironment()

    def miss(command, **kwargs):
        env.commands.append(command)
        return {"output": "", "returncode": 1}

    env.execute = miss
    ops = ShellFileOperations(env)

    assert ops._resolve_command("rg") is None
    assert env.commands == ["command -v rg 2>/dev/null"]
    assert str(tmp_path) not in env.commands[0]


@pytest.mark.platforms("windows")
def test_remote_msys_shaped_executable_is_not_rewritten_as_controller_path():
    env = RecordingEnvironment()

    def execute(command, **kwargs):
        env.commands.append(command)
        if command.startswith("test -e "):
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/c/remote-tools/rg\n", "returncode": 0}
        if "--files" in command:
            return {"output": "/repo/a.py\n", "returncode": 0}
        return {"output": "", "returncode": 1}

    env.execute = execute

    result = ShellFileOperations(env).search("*.py", path="/repo", target="files")

    assert result.files
    assert "'/c/remote-tools/rg' --files" in env.rg_commands[0]
    assert "C:/remote-tools/rg" not in env.rg_commands[0]


@pytest.mark.platforms("windows")
def test_every_windows_drive_root_is_broad_even_when_home_is_on_another_drive(
    tmp_path, monkeypatch
):
    import tools.file_operations as file_operations

    monkeypatch.setattr(file_operations, "_HOME", "C:/Users/alice")
    ops = ShellFileOperations(LocalEnvironment(str(tmp_path)))

    assert ops._is_broad_local_search_root("D:/") is True
    assert ops._is_broad_local_search_root("D:/repo") is False


def test_modified_multi_path_search_preserves_exact_order_request():
    env = RecordingEnvironment()

    def execute(command, **kwargs):
        env.commands.append(command)
        if command.startswith("test -e "):
            if "'/one /two'" in command:
                return {"output": "not_found\n", "returncode": 0}
            return {"output": "exists\n", "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/usr/bin/rg\n", "returncode": 0}
        if "--version" in command:
            return {"output": "ripgrep 14.1.1\n", "returncode": 0}
        if "--files" in command:
            return {"output": "/two/new.py\n/one/old.py\n", "returncode": 0}
        return {"output": "", "returncode": 1}

    env.execute = execute
    result = ShellFileOperations(env).search(
        "*.py", path="/one /two", target="files", order="modified"
    )

    assert result.files == ["/two/new.py", "/one/old.py"]
    assert len(env.rg_commands) == 1
    assert "--sortr=modified" in env.rg_commands[0]
    assert "'/one'" in env.rg_commands[0]
    assert "'/two'" in env.rg_commands[0]


def test_comma_delimited_file_roots_preserve_internal_spaces_in_one_search():
    env = RecordingEnvironment(rg_output="C:/root one/a.py\nC:/root two/b.py\n")
    combined = "C:/root one, C:/root two"

    path_checks = 0

    def execute(command, **kwargs):
        nonlocal path_checks
        env.commands.append(command)
        if command.startswith("test -e "):
            path_checks += 1
            output = "not_found\n" if path_checks == 1 else "exists\n"
            return {"output": output, "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/usr/bin/rg\n", "returncode": 0}
        if "--files" in command:
            return {"output": env.rg_output, "returncode": 0}
        return {"output": "", "returncode": 1}

    env.execute = execute
    result = ShellFileOperations(env).search(
        "*.py", path=combined, target="files"
    )

    assert result.error is None
    assert result.files == ["C:/root one/a.py", "C:/root two/b.py"]
    assert len(env.rg_commands) == 1
    assert "'C:/root one' 'C:/root two'" in env.rg_commands[0]


def test_multi_path_modified_capability_error_propagates():
    env = RecordingEnvironment()

    def execute(command, **kwargs):
        env.commands.append(command)
        if command.startswith("test -e "):
            output = "not_found\n" if "'/one /two'" in command else "exists\n"
            return {"output": output, "returncode": 0}
        if command.startswith("command -v rg"):
            return {"output": "/usr/bin/rg\n", "returncode": 0}
        if "--version" in command:
            return {"output": "ripgrep 13.0.0\n", "returncode": 0}
        raise AssertionError(command)

    env.execute = execute
    result = ShellFileOperations(env).search(
        "*.py", path="/one /two", target="files", order="modified"
    )
    assert "ripgrep 14" in (result.error or "").lower()
    assert env.rg_commands == []


def test_modified_timeout_preserves_partial_results_and_limit_reason():
    env = RecordingEnvironment(
        rg_output="/repo/partial.py\n[Command timed out after 60s]\n",
        rg_code=124,
    )

    result = ShellFileOperations(env).search(
        "*.py", path="/repo", target="files", order="modified"
    )

    assert result.files == ["/repo/partial.py"]
    assert result.truncated is True
    assert result.limit_reason == "search_timeout"


def test_order_is_ignored_for_content_search():
    env = RipgrepInvocationEnvironment()

    result = ShellFileOperations(env).search(
        "needle", path="/repo", target="content", order="not-a-file-order"
    )

    assert result.error is None
    assert result.matches
