from __future__ import annotations

import os
import time
from pathlib import Path

import pytest

import processes


def _wait(predicate, timeout: float = 10.0) -> None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if predicate():
            return
        time.sleep(0.05)
    raise AssertionError("condition never held")


def _start(registry: processes.ProcessRegistry, script: str, cwd: Path) -> processes.ManagedProcess:
    return registry.start(["bash", "-c", script], command=script, cwd=cwd, env={"PATH": os.environ["PATH"]})


def test_output_is_readable_while_the_process_is_still_running(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    managed = _start(registry, "echo first; sleep 30", tmp_path)
    _wait(lambda: managed.read(0)[0] == "first\n")
    assert managed.running
    managed.stop()


def test_reads_resume_from_the_returned_offset(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    managed = _start(registry, "while read line; do echo got:$line; done", tmp_path)
    managed.write_stdin("one\n")
    _wait(lambda: managed.read(0)[0] == "got:one\n")
    _, offset, _ = managed.read(0)
    managed.write_stdin("two\n")
    _wait(lambda: managed.read(offset)[0] == "got:two\n")
    managed.stop()


def test_stopping_kills_the_whole_process_group(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    managed = _start(registry, "sleep 300 & echo $!; wait", tmp_path)
    _wait(lambda: managed.read(0)[0].strip().isdigit())
    child_pid = int(managed.read(0)[0].strip())
    managed.stop()
    _wait(lambda: not managed.running)
    with pytest.raises(ProcessLookupError):
        os.kill(child_pid, 0)


def test_stopping_an_exited_process_is_reported_not_raised(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    managed = _start(registry, "exit 7", tmp_path)
    _wait(lambda: not managed.running)
    assert managed.exit_code == 7
    assert "already exited" in managed.stop()


def test_writing_to_an_exited_process_is_refused(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    managed = _start(registry, "true", tmp_path)
    _wait(lambda: not managed.running)
    with pytest.raises(ValueError, match="already exited"):
        managed.write_stdin("late\n")


def test_the_buffer_drops_the_oldest_output_and_says_how_much(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(processes, "OUTPUT_BUFFER_BYTES", 64)
    registry = processes.ProcessRegistry()
    managed = _start(registry, "printf 'x%.0s' $(seq 1 500); echo", tmp_path)
    _wait(lambda: managed.read(0)[1] == 501)
    output, next_offset, dropped = managed.read(0)
    assert len(output) <= 64
    assert dropped >= 437
    assert next_offset == 501


def test_an_unknown_process_id_is_refused() -> None:
    with pytest.raises(ValueError, match="no such process"):
        processes.ProcessRegistry().get("proc-99")


def test_stop_all_ends_every_running_process(tmp_path: Path) -> None:
    registry = processes.ProcessRegistry()
    first = _start(registry, "sleep 300", tmp_path)
    second = _start(registry, "sleep 300", tmp_path)
    registry.stop_all()
    assert not first.running and not second.running
