import sys
from io import StringIO
import pytest


def test_run_command_passthrough_writes_to_sys_streams(monkeypatch: pytest.MonkeyPatch) -> None:
    from l4d2web.services.host_commands import run_command

    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"


def test_run_command_streams_stdout_and_stderr_callbacks() -> None:
    from l4d2web.services.host_commands import run_command

    stdout: list[str] = []
    stderr: list[str] = []

    result = run_command(
        ["python3", "-c", "import sys; print('ok'); print('warn', file=sys.stderr)"],
        on_stdout=stdout.append,
        on_stderr=stderr.append,
    )

    assert stdout == ["ok"]
    assert stderr == ["warn"]
    assert result.returncode == 0
    assert result.stdout == "ok"
    assert result.stderr == "warn"


def test_run_command_raises_host_error_on_nonzero_exit() -> None:
    from l4d2web.services.host_commands import HostCommandError, run_command

    with pytest.raises(HostCommandError) as exc_info:
        run_command(["python3", "-c", "import sys; print('bad', file=sys.stderr); sys.exit(7)"])

    assert exc_info.value.returncode == 7
    assert exc_info.value.stderr == "bad"


def test_run_command_raises_cancelled_error_when_cancel_requested() -> None:
    from l4d2web.services.host_commands import CommandCancelledError, run_command

    stdout: list[str] = []

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

    assert stdout == ["ready"]


def test_stream_command_yields_stdout_lines() -> None:
    from l4d2web.services.host_commands import stream_command

    lines = list(stream_command(["python3", "-c", "print('one'); print('two')"]))

    assert lines == ["one", "two"]


def test_stream_command_emits_heartbeat_when_subprocess_silent() -> None:
    import time

    from l4d2web.services.host_commands import stream_command

    cmd = [
        "python3",
        "-c",
        "import time; time.sleep(0.4); print('done')",
    ]

    started = time.monotonic()
    items: list[str] = []
    for item in stream_command(cmd, heartbeat_interval=0.05):
        items.append(item)
        if time.monotonic() - started > 2.0:
            break

    assert "done" in items, items
    heartbeats = [i for i in items if i == ""]
    assert len(heartbeats) >= 2, f"expected ≥2 heartbeat ticks during the silent 0.4s window, got items={items!r}"


def test_stream_command_close_releases_subprocess_promptly() -> None:
    import time

    from l4d2web.services.host_commands import stream_command

    cmd = [
        "python3",
        "-c",
        "import time;\nwhile True:\n    time.sleep(60)",
    ]

    gen = stream_command(cmd, heartbeat_interval=0.05)
    assert next(gen) == ""

    started = time.monotonic()
    gen.close()
    elapsed = time.monotonic() - started
    assert elapsed < 1.0, f"gen.close() took {elapsed:.2f}s; subprocess cleanup must not block"
