import io
import subprocess
import threading
import time
import pytest
from unittest.mock import patch
from r_bridge.utils import drain_stderr, strip_sentinel, get_r_executable
from r_bridge.protocol import SENTINEL


def test_strip_sentinel_valid():
    from r_bridge.utils import strip_sentinel
    body = strip_sentinel(SENTINEL + '{"a":1}\n')
    assert body == '{"a":1}'


def test_strip_sentinel_invalid():
    assert strip_sentinel("random line\n") is None


def test_strip_sentinel_empty():
    assert strip_sentinel("") is None


def test_get_r_executable_finds_rscript():
    path = get_r_executable()
    assert "Rscript" in path


def test_drain_stderr_collects_output():
    proc = subprocess.Popen(
        ["bash", "-c", "echo error line 1 >&2; echo error line 2 >&2; sleep 0.05"],
        stderr=subprocess.PIPE,
    )
    buf = []
    t = drain_stderr(proc, buf)
    proc.wait()
    t.join(timeout=2)
    combined = "".join(buf)
    assert "error line 1" in combined
    assert "error line 2" in combined


def test_drain_stderr_is_daemon():
    proc = subprocess.Popen(["bash", "-c", "echo x >&2"], stderr=subprocess.PIPE)
    buf = []
    t = drain_stderr(proc, buf)
    assert t.daemon is True
    proc.wait()
    t.join(timeout=2)


def test_drain_stderr_on_line_callback():
    proc = subprocess.Popen(
        ["bash", "-c", "echo line_a >&2; echo line_b >&2; sleep 0.05"],
        stderr=subprocess.PIPE,
    )
    buf = []
    seen = []
    t = drain_stderr(proc, buf, on_line=seen.append)
    proc.wait()
    t.join(timeout=2)
    assert any("line_a" in s for s in seen)
    assert any("line_b" in s for s in seen)
    # callback receives the same text that lands in buf
    assert seen == buf


def test_drain_stderr_no_callback_is_unchanged():
    """Passing on_line=None must not break existing behaviour."""
    proc = subprocess.Popen(["bash", "-c", "echo ok >&2"], stderr=subprocess.PIPE)
    buf = []
    t = drain_stderr(proc, buf, on_line=None)
    proc.wait()
    t.join(timeout=2)
    assert any("ok" in s for s in buf)


def test_get_r_executable_explicit_path():
    result = get_r_executable("/explicit/path/to/Rscript")
    assert result == "/explicit/path/to/Rscript"


def test_get_r_executable_not_found():
    with patch("r_bridge.utils.shutil.which", return_value=None):
        with pytest.raises(FileNotFoundError, match="Rscript not found"):
            get_r_executable()


def test_drain_stderr_handles_exception_in_loop():
    """Drain thread must exit cleanly even if stderr iteration raises."""
    class RaisingIter:
        def __iter__(self): return self
        def __next__(self): raise OSError("pipe closed")

    from unittest.mock import MagicMock
    proc = MagicMock()
    proc.stderr = RaisingIter()
    buf = []
    t = drain_stderr(proc, buf)
    t.join(timeout=1)
    assert not t.is_alive()
