"""Tests for useful_tools.py module."""

import os
import shutil
import signal
import sys
import tempfile
from pathlib import Path

import pytest

from kiss.agents.sorcar.useful_tools import (
    DISALLOWED_BASH_COMMANDS,
    UsefulTools,
    _extract_command_names,
    _truncate_output,
)


@pytest.fixture
def temp_test_dir():
    test_dir = Path(tempfile.mkdtemp()).resolve()
    original_dir = Path.cwd()
    os.chdir(test_dir)
    yield test_dir
    os.chdir(original_dir)
    shutil.rmtree(test_dir, ignore_errors=True)


@pytest.fixture
def tools(temp_test_dir):
    return UsefulTools(), temp_test_dir


class TestUsefulTools:

    def test_write_to_directory_path(self, tools):
        ut, test_dir = tools
        subdir = test_dir / "subdir"
        subdir.mkdir()
        result = ut.Write(str(subdir), "content")
        assert "Error:" in result


class TestExtractCommandNames:

    def test_empty_pipe_segment(self):
        assert _extract_command_names("echo hi | | cat") == ["echo", "cat"]


@pytest.fixture
def streaming_tools(temp_test_dir):
    streamed: list[str] = []
    ut = UsefulTools(stream_callback=streamed.append)
    return ut, temp_test_dir, streamed


@pytest.fixture(params=[False, True], ids=["nonstreaming", "streaming"])
def any_tools(request, temp_test_dir):
    if request.param:
        return UsefulTools(stream_callback=lambda _: None), temp_test_dir
    return UsefulTools(), temp_test_dir
@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test (signals, bash scripts)")
class TestAdversarial:
    """Adversarial tests to try to break the Popen/killpg changes."""

    def test_interrupt_kills_child(self, any_tools):
        """KeyboardInterrupt must kill the child process group."""
        import _thread
        import threading
        import time

        ut, test_dir = any_tools
        pid_file = test_dir / "interrupt_child.pid"
        script = test_dir / "interrupt_target.sh"
        script.write_text(
            f"#!/bin/bash\necho $$ > {pid_file}\nsleep 100\n"
        )
        script.chmod(0o755)

        child_pid = None

        def send_interrupt():
            nonlocal child_pid
            for _ in range(20):
                time.sleep(0.1)
                if pid_file.exists():
                    child_pid = int(pid_file.read_text().strip())
                    break
            if child_pid:
                _thread.interrupt_main()

        t = threading.Thread(target=send_interrupt, daemon=True)
        t.start()
        try:
            ut.Bash(str(script), "interruptible", timeout_seconds=30)
        except KeyboardInterrupt:
            pass
        t.join(timeout=5)

        if child_pid is None:
            pytest.skip("Script didn't start in time")

        time.sleep(0.3)
        alive = False
        try:
            os.kill(child_pid, 0)
            alive = True
            os.kill(child_pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        assert not alive, f"Child {child_pid} survived KeyboardInterrupt!"

class TestBugs:
    """Tests that expose bugs in useful_tools.py."""


    def test_source_is_blocked(self):
        assert "source" in DISALLOWED_BASH_COMMANDS, (
            "source is the bash synonym of . and should be disallowed"
        )


    def test_truncate_output_tiny_limit(self):
        big = "X" * 200
        result = _truncate_output(big, 5)
        assert len(result) <= 5


    def test_brace_group_eval_detected(self):
        names = _extract_command_names("{ eval foo; }")
        assert "eval" in names


    def test_fd_redirect_before_source(self):
        names = _extract_command_names("2>/dev/null source script.sh")
        assert "source" in names

    def test_redirect_output_before_exec(self):
        names = _extract_command_names("> /tmp/log exec cmd")
        assert "exec" in names


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test (signals, bash scripts)")
class TestStopEvent:
    """Tests that stop_event kills child processes promptly."""

    def test_stop_event_kills_streaming_child(self, temp_test_dir):
        """When stop_event is set, the streaming Bash child must be killed."""
        import threading
        import time

        stop_event = threading.Event()
        streamed: list[str] = []
        ut = UsefulTools(stream_callback=streamed.append, stop_event=stop_event)

        pid_file = temp_test_dir / "stop_child.pid"
        script = temp_test_dir / "stop_target.sh"
        script.write_text(
            f"#!/bin/bash\necho $$ > {pid_file}\nwhile true; do echo tick; sleep 0.2; done\n"
        )
        script.chmod(0o755)

        child_pid = None

        def set_stop_after_start():
            nonlocal child_pid
            for _ in range(50):
                time.sleep(0.1)
                if pid_file.exists():
                    child_pid = int(pid_file.read_text().strip())
                    break
            if child_pid:
                stop_event.set()

        t = threading.Thread(target=set_stop_after_start, daemon=True)
        t.start()
        ut.Bash(str(script), "stoppable", timeout_seconds=30)
        t.join(timeout=5)

        if child_pid is None:
            pytest.skip("Script didn't start in time")

        time.sleep(0.5)
        alive = False
        try:
            os.kill(child_pid, 0)
            alive = True
            os.kill(child_pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        assert not alive, f"Child {child_pid} survived stop_event!"

    def test_stop_event_kills_nonstreaming_child(self, temp_test_dir):
        """When stop_event is set, the non-streaming Bash child must be killed."""
        import threading
        import time

        stop_event = threading.Event()
        ut = UsefulTools(stop_event=stop_event)

        pid_file = temp_test_dir / "stop_child_ns.pid"
        script = temp_test_dir / "stop_target_ns.sh"
        script.write_text(
            f"#!/bin/bash\necho $$ > {pid_file}\nsleep 100\n"
        )
        script.chmod(0o755)

        child_pid = None

        def set_stop_after_start():
            nonlocal child_pid
            for _ in range(50):
                time.sleep(0.1)
                if pid_file.exists():
                    child_pid = int(pid_file.read_text().strip())
                    break
            if child_pid:
                stop_event.set()

        t = threading.Thread(target=set_stop_after_start, daemon=True)
        t.start()
        ut.Bash(str(script), "stoppable", timeout_seconds=30)
        t.join(timeout=5)

        if child_pid is None:
            pytest.skip("Script didn't start in time")

        time.sleep(0.5)
        alive = False
        try:
            os.kill(child_pid, 0)
            alive = True
            os.kill(child_pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        assert not alive, f"Child {child_pid} survived stop_event!"


class TestStreamingTimeout:
    """Test that streaming Bash correctly reports timeout."""

    def test_streaming_bash_timeout(self, temp_test_dir):
        """Streaming Bash must return timeout error when command exceeds timeout."""
        streamed: list[str] = []
        ut = UsefulTools(stream_callback=streamed.append)
        result = ut.Bash("sleep 10", "should timeout", timeout_seconds=0.5)
        assert "timeout" in result.lower()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
