"""Integration tests — spin up a real R subprocess."""

import math
import datetime
import numpy as np
import pandas as pd
import pytest
from r_bridge import RBridge, start_bridge
from r_bridge.bridge import _format_verbose_call, _RFuncProxy
from r_bridge.exceptions import RError, RTimeoutError, RBridgeError


@pytest.fixture(scope="module")
def bridge():
    with RBridge() as b:
        yield b


# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------

def test_is_alive(bridge):
    assert bridge.is_alive()


def test_ping_returns_latency(bridge):
    latency = bridge.ping()
    assert isinstance(latency, float)
    assert latency >= 0


# ---------------------------------------------------------------------------
# set / get via attribute access
# ---------------------------------------------------------------------------

def test_set_and_get_int(bridge):
    bridge.x = 42
    assert bridge.x == 42


def test_set_and_get_float(bridge):
    bridge.pi_val = 3.14
    assert bridge.pi_val == pytest.approx(3.14)


def test_set_and_get_str(bridge):
    bridge.greeting = "hello"
    assert bridge.greeting == "hello"


def test_set_and_get_bool(bridge):
    bridge.flag = True
    assert bridge.flag is True


def test_set_and_get_none(bridge):
    bridge.nothing = None
    assert bridge.nothing is None


def test_set_and_get_list(bridge):
    bridge.nums = [1, 2, 3]
    result = bridge.nums
    np.testing.assert_array_equal(result, [1, 2, 3])


def test_set_and_get_dict(bridge):
    bridge.mapping = {"a": 1, "b": 2}
    result = bridge.mapping
    assert isinstance(result, dict)
    assert result["a"] == 1


def test_set_and_get_numpy_array(bridge):
    arr = np.array([1.0, 2.0, 3.0])
    bridge.arr = arr
    result = bridge.arr
    np.testing.assert_allclose(result, arr)


def test_set_and_get_dataframe(bridge):
    df = pd.DataFrame({"x": [1, 2, 3], "y": [4.0, 5.0, 6.0]})
    bridge.df = df
    result = bridge.df
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["x", "y"]
    np.testing.assert_array_equal(result["x"].values, [1, 2, 3])


def test_set_and_get_datetime(bridge):
    dt = datetime.datetime(2026, 3, 21, 12, 0, 0)
    bridge.dt = dt
    result = bridge.dt
    assert isinstance(result, datetime.datetime)
    assert result.year == 2026
    assert result.month == 3


# ---------------------------------------------------------------------------
# Function calls via attribute access
# ---------------------------------------------------------------------------

def test_call_builtin_sum(bridge):
    bridge.v = [1, 2, 3, 4, 5]
    result = bridge.sum(bridge.get("v"))
    assert result == pytest.approx(15.0)


def test_call_with_kwargs(bridge):
    result = bridge.seq(1, 10, by=2)
    np.testing.assert_array_equal(result, [1, 3, 5, 7, 9])


def test_call_returns_scalar_with_hint(bridge):
    result = bridge.call("sum", [1.0, 2.0, 3.0], result_type_hint="scalar")
    assert result == pytest.approx(6.0)
    assert isinstance(result, float)


def test_call_paste(bridge):
    result = bridge.paste("hello", "world", sep=" ")
    assert result == "hello world"


# ---------------------------------------------------------------------------
# eval
# ---------------------------------------------------------------------------

def test_eval_expression(bridge):
    result = bridge.eval("2 ^ 10")
    assert result == pytest.approx(1024.0)


def test_eval_assigns_and_returns(bridge):
    bridge.eval("tmp_val <- 99L")
    assert bridge.get("tmp_val") == 99


# ---------------------------------------------------------------------------
# ls
# ---------------------------------------------------------------------------

def test_ls_includes_set_vars(bridge):
    bridge.ls_test_var = 123
    names = bridge.ls()
    assert "ls_test_var" in names


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------

def test_r_error_raises_rerror(bridge):
    with pytest.raises(RError) as exc_info:
        bridge.eval("stop('something went wrong')")
    assert "something went wrong" in exc_info.value.message


def test_r_error_has_message_attr(bridge):
    with pytest.raises(RError) as exc_info:
        bridge.eval("stop('check message attr')")
    assert exc_info.value.message == "check message attr"
    assert isinstance(exc_info.value.traceback, list)


def test_r_warning_returned(bridge):
    result = bridge.call("log", -1.0)  # produces NaN + warning
    assert math.isnan(result)


def test_undefined_variable_raises(bridge):
    with pytest.raises(RError):
        bridge.get("this_var_does_not_exist_xyz")


def test_bridge_survives_r_error(bridge):
    """Bridge must stay alive and usable after an R error."""
    with pytest.raises(RError):
        bridge.eval("stop('transient error')")
    assert bridge.is_alive()
    assert bridge.ping() >= 0


# ---------------------------------------------------------------------------
# Context manager (separate bridge instance)
# ---------------------------------------------------------------------------

def test_context_manager_cleans_up():
    with RBridge() as b:
        b.z = 1
        assert b.is_alive()
    assert not b.is_alive()


# ---------------------------------------------------------------------------
# Timeout
# ---------------------------------------------------------------------------

def test_timeout_raises(tmp_path):
    with RBridge(call_timeout=0.5) as b:
        with pytest.raises(RTimeoutError):
            b.eval("Sys.sleep(5)")


# ---------------------------------------------------------------------------
# execute()
# ---------------------------------------------------------------------------

def test_execute_returns_none(bridge):
    result = bridge.execute("x_exec <- 42L")
    assert result is None


def test_execute_side_effects_visible(bridge):
    bridge.execute("exec_a <- 10L\nexec_b <- 20L")
    assert bridge.get("exec_a") == 10
    assert bridge.get("exec_b") == 20


def test_execute_defines_function(bridge):
    bridge.execute("square <- function(x) x ^ 2")
    assert bridge.call("square", 5.0, result_type_hint="scalar") == pytest.approx(25.0)


def test_execute_multiline_script(bridge):
    bridge.execute(
        "acc <- 0\n"
        "for (i in 1:5) { acc <- acc + i }\n"
    )
    assert bridge.get("acc", result_type_hint="scalar") == pytest.approx(15.0)


def test_execute_error_raises(bridge):
    with pytest.raises(RError):
        bridge.execute("stop('execute error')")


def test_execute_bridge_survives_error(bridge):
    with pytest.raises(RError):
        bridge.execute("stop('boom')")
    assert bridge.is_alive()


def test_execute_verbose_logged(capsys):
    with RBridge(verbose=True) as b:
        b.execute("x <- 1\ny <- 2")
    assert "[R exec]" in capsys.readouterr().err


# ---------------------------------------------------------------------------
# _format_verbose_call unit tests (no R process needed)
# ---------------------------------------------------------------------------

def test_format_verbose_call_func():
    out = _format_verbose_call("call_func", {"func": "mean", "args": [[1, 2]], "kwargs": {}})
    assert "[R call]" in out
    assert "mean" in out


def test_format_verbose_call_eval():
    out = _format_verbose_call("eval_expr", {"expr": "1 + 1"})
    assert "[R eval]" in out
    assert "1 + 1" in out


def test_format_verbose_set_var():
    out = _format_verbose_call("set_var", {"name": "x", "value": 42})
    assert "[R set]" in out
    assert "x" in out


def test_format_verbose_get_var():
    out = _format_verbose_call("get_var", {"name": "y"})
    assert "[R get]" in out
    assert "y" in out


def test_format_verbose_ls():
    assert "ls()" in _format_verbose_call("ls", {})


def test_format_verbose_ping():
    assert "ping()" in _format_verbose_call("ping", {})


def test_format_verbose_unknown_op():
    out = _format_verbose_call("some_future_op", {})
    assert "some_future_op" in out


def test_format_verbose_exec_script_single_line():
    out = _format_verbose_call("exec_script", {"code": "x <- 1"})
    assert "[R exec]" in out
    assert "x <- 1" in out


def test_format_verbose_exec_script_multiline():
    out = _format_verbose_call("exec_script", {"code": "x <- 1\ny <- 2"})
    assert "[R exec]" in out
    assert "x <- 1" in out
    assert "..." in out


# ---------------------------------------------------------------------------
# Verbose mode integration tests
# ---------------------------------------------------------------------------

def test_verbose_false_produces_no_stderr(capsys):
    with RBridge(verbose=False) as b:
        b.eval("1 + 1")
    assert capsys.readouterr().err == ""


def test_verbose_call_func_logged(capsys):
    with RBridge(verbose=True) as b:
        b.call("sum", [1.0, 2.0, 3.0])
    assert "[R call] sum" in capsys.readouterr().err


def test_verbose_eval_logged(capsys):
    with RBridge(verbose=True) as b:
        b.eval("2 ^ 8")
    assert "[R eval]" in capsys.readouterr().err


def test_verbose_set_get_logged(capsys):
    with RBridge(verbose=True) as b:
        b.set("myvar", 7)
        b.get("myvar")
    err = capsys.readouterr().err
    assert "[R set]" in err
    assert "[R get]" in err


def test_verbose_r_stdout_captured(capsys):
    """cat() in R writes to stdout; should appear as [R stdout] in verbose mode."""
    with RBridge(verbose=True) as b:
        b.eval('cat("hello from R\\n")')
    assert "[R stdout]" in capsys.readouterr().err


def test_verbose_r_stderr_captured(capsys):
    """message() in R writes to stderr; should appear as [R stderr] in verbose mode."""
    with RBridge(verbose=True) as b:
        b.eval('message("r stderr line")')
    assert "[R stderr]" in capsys.readouterr().err


# ---------------------------------------------------------------------------
# last_stderr, _call_r not running, r_libs, __setattr__/__getattr__ edge cases
# ---------------------------------------------------------------------------

def test_last_stderr_is_string(bridge):
    assert isinstance(bridge.last_stderr, str)


def test_call_r_not_running_raises():
    b = RBridge()
    b.start()
    b.stop()
    with pytest.raises(RBridgeError):
        b.eval("1 + 1")


def test_setattr_private_name_goes_to_object(bridge):
    """Assigning a _-prefixed name routes to object.__setattr__, not R."""
    bridge._verbose = False
    assert bridge._verbose is False


def test_getattr_private_name_raises(bridge):
    """Accessing a nonexistent _-prefixed attribute raises AttributeError."""
    with pytest.raises(AttributeError):
        _ = bridge._nonexistent_private_attr_xyz


def test_r_libs_parameter_accepted(tmp_path):
    """r_libs is accepted and the bridge starts cleanly."""
    with RBridge(r_libs=[str(tmp_path)]) as b:
        assert b.is_alive()


def test_start_bridge_helper():
    """start_bridge() is a convenience wrapper for RBridge().start()."""
    b = start_bridge()
    try:
        assert b.is_alive()
    finally:
        b.stop()


def test_getattr_on_dead_bridge_returns_proxy():
    """When _ls_raw raises (bridge dead), __getattr__ falls back to _RFuncProxy."""
    b = RBridge()
    b.start()
    b.stop()
    proxy = b.__getattr__("some_func")
    assert isinstance(proxy, _RFuncProxy)
