import inspect
import subprocess

import pytest

from l4d2host.process import CommandCancelledError, run_command


def test_callbacks_receive_lines() -> None:
    out: list[str] = []
    err: list[str] = []
    run_command(
        ["python3", "-c", "import sys; print('ok'); print('warn', file=sys.stderr)"],
        on_stdout=out.append,
        on_stderr=err.append,
    )
    assert out == ["ok"]
    assert err == ["warn"]


def test_nonzero_exit_raises() -> None:
    with pytest.raises(subprocess.CalledProcessError):
        run_command(["python3", "-c", "import sys; sys.exit(7)"])


def test_cancelled_command_raises_cancelled_error() -> None:
    should_cancel = False
    lines: list[str] = []

    def on_stdout(line: str) -> None:
        nonlocal should_cancel
        lines.append(line)
        should_cancel = True

    with pytest.raises(CommandCancelledError):
        run_command(
            ["python3", "-c", "import time; print('ready', flush=True); time.sleep(30)"],
            on_stdout=on_stdout,
            should_cancel=lambda: should_cancel,
            cancel_poll_seconds=0.01,
            cancel_terminate_timeout=0.2,
        )

    assert lines == ["ready"]


def test_run_command_avoids_runtime_unsafe_nested_annotations() -> None:
    source = inspect.getsource(run_command)
    assert "subprocess.Popen[str].stdout" not in source


def test_run_command_passthrough_writes_to_sys_streams(monkeypatch: pytest.MonkeyPatch) -> None:
    import sys
    from io import StringIO

    mock_stdout = StringIO()
    mock_stderr = StringIO()
    monkeypatch.setattr(sys, "stdout", mock_stdout)
    monkeypatch.setattr(sys, "stderr", mock_stderr)

    run_command(
        ["python3", "-c", "import sys; print('passed out'); print('passed err', file=sys.stderr)"],
        passthrough=True,
    )

    assert mock_stdout.getvalue() == "passed out\n"
    assert mock_stderr.getvalue() == "passed err\n"
