import multiprocessing
import time

import pytest

from soniclue_basekit.proc_runner import UnpickableError, run_in_process, run_in_process_decorator
from soniclue_basekit.proc_runner import _Result


# --- _Result ---


def test_result_from_ok_is_not_err():
    r = _Result.from_ok(42)
    assert not r.is_err


def test_result_from_err_is_err():
    try:
        raise ValueError("bad")
    except ValueError as ex:
        r = _Result.from_err(ex)
    assert r.is_err


def test_result_from_ok_roundtrips_value():
    import pickle
    r = _Result.from_ok("hello")
    assert pickle.loads(r.value) == "hello"


def test_result_from_err_roundtrips_exception():
    import pickle
    r = _Result.from_err(ValueError("oops"))
    ex = pickle.loads(r.value)
    assert isinstance(ex, ValueError)
    assert str(ex) == "oops"


def test_result_pickle_trace_is_string():
    r = _Result.from_ok(1)
    trace = r.pickle_trace
    assert isinstance(trace, str)
    assert len(trace) > 0


# --- UnpickableError ---


def test_unpickable_error_is_err_delegates():
    try:
        raise RuntimeError("x")
    except RuntimeError as ex:
        r = _Result.from_err(ex)
    e = UnpickableError(r)
    assert e.is_err is True


def test_unpickable_error_is_err_false_for_ok():
    r = _Result.from_ok(1)
    e = UnpickableError(r)
    assert e.is_err is False


def test_unpickable_error_str_contains_module():
    r = _Result.from_err(ValueError("v"))
    e = UnpickableError(r)
    assert "Module:" in str(e)


def test_unpickable_error_str_contains_is_error():
    r = _Result.from_err(ValueError("v"))
    e = UnpickableError(r)
    assert "Is Error:" in str(e)


# --- run_in_process ---


def test_run_in_process_returns_value():
    result = run_in_process(lambda: 42)
    assert result == 42


def test_run_in_process_returns_string():
    result = run_in_process(lambda: "hello")
    assert result == "hello"


def test_run_in_process_propagates_exception():
    with pytest.raises(ValueError, match="from subprocess"):
        run_in_process(lambda: (_ for _ in ()).throw(ValueError("from subprocess")))


def test_run_in_process_propagates_runtime_error():
    def _raise():
        raise RuntimeError("boom")

    with pytest.raises(RuntimeError, match="boom"):
        run_in_process(_raise)


def test_run_in_process_timeout_raises():
    with pytest.raises(multiprocessing.TimeoutError):
        run_in_process(lambda: time.sleep(60), timeout_sec=0.3)


def test_run_in_process_spawn_method():
    # Spawn method cannot use lambdas — use a named function.
    result = run_in_process(_double, start_method="spawn")
    assert result == 84


def _double():
    """Top-level helper for spawn-method test (must be picklable)."""
    return 42 * 2


def test_run_in_process_name_param():
    # Smoke test: named subprocess completes normally.
    result = run_in_process(lambda: "ok", name="my-worker")
    assert result == "ok"


def test_run_in_process_should_reset_timeout_allows_retry():
    """should_reset_timeout returning True once lets the process finish."""
    reset_calls = []

    def _reset():
        if not reset_calls:
            reset_calls.append(True)
            return True  # Reset once, then let it complete.
        return False

    # The function sleeps briefly; with a tiny timeout it would normally fail,
    # but should_reset_timeout grants one extra window.
    result = run_in_process(lambda: "done", timeout_sec=5, should_reset_timeout=_reset)
    assert result == "done"


# --- run_in_process_decorator ---


def test_run_in_process_decorator_returns_value():
    @run_in_process_decorator
    def _add(a, b):
        return a + b

    assert _add(3, 4) == 7


def test_run_in_process_decorator_preserves_name():
    @run_in_process_decorator
    def _my_func():
        return None

    assert _my_func.__name__ == "_my_func"


def test_run_in_process_decorator_propagates_exception():
    @run_in_process_decorator
    def _boom():
        raise ValueError("decorated-error")

    with pytest.raises(ValueError, match="decorated-error"):
        _boom()
