//
// dap_evaluate.rs
//
// Copyright (C) 2026 Posit Software, PBC. All rights reserved.
//
//

use ark_test::DummyArkFrontend;

#[test]
fn test_dap_evaluate_variable() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  x <- 42
  y <- 'hello'
  browser()
})
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Evaluate a numeric variable
    let result = dap.evaluate("x", Some(frame_id));
    assert_eq!(result, "42");

    // Evaluate a string variable
    let result = dap.evaluate("y", Some(frame_id));
    assert_eq!(result, "\"hello\"");

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_expression() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  x <- 10
  y <- 5
  browser()
})
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Evaluate an arithmetic expression
    let result = dap.evaluate("x + y", Some(frame_id));
    assert_eq!(result, "15");

    // Evaluate a comparison
    let result = dap.evaluate("x > y", Some(frame_id));
    assert_eq!(result, "TRUE");

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_in_different_frames() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
outer <- function() {
  outer_var <- 'from_outer'
  inner()
}
inner <- function() {
  inner_var <- 'from_inner'
  browser()
}
outer()
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    assert!(stack.len() >= 2, "Expected at least 2 frames");

    let inner_frame_id = stack[0].id;
    let outer_frame_id = stack[1].id;

    // Evaluate in inner frame
    let result = dap.evaluate("inner_var", Some(inner_frame_id));
    assert_eq!(result, "\"from_inner\"");

    // Evaluate in outer frame
    let result = dap.evaluate("outer_var", Some(outer_frame_id));
    assert_eq!(result, "\"from_outer\"");

    // outer_var should not be visible in inner frame
    let err = dap.evaluate_error("outer_var", Some(inner_frame_id));
    assert!(
        err.contains("not found"),
        "Expected 'not found' error, got: {err}"
    );

    // inner_var should not be visible in outer frame
    let err = dap.evaluate_error("inner_var", Some(outer_frame_id));
    assert!(
        err.contains("not found"),
        "Expected 'not found' error, got: {err}"
    );

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_print() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  x <- c(1, 2, 3)
  df <- data.frame(a = 1:2, b = c('x', 'y'))
  browser()
})
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Using "/print " prefix returns printed output
    let result = dap.evaluate("/print x", Some(frame_id));
    assert!(
        result.contains("[1] 1 2 3"),
        "Expected printed vector output, got: {result}"
    );

    // Print a data frame
    let result = dap.evaluate("/print df", Some(frame_id));
    assert!(
        result.contains("a") && result.contains("b"),
        "Expected data frame output, got: {result}"
    );

    // Print an expression
    let result = dap.evaluate("/print sum(x)", Some(frame_id));
    assert!(
        result.contains("[1] 6"),
        "Expected sum output, got: {result}"
    );

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_error() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  x <- 42
  browser()
})
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Evaluate an unbound variable
    let err = dap.evaluate_error("nonexistent_variable", Some(frame_id));
    assert!(
        err.contains("not found"),
        "Expected 'not found' error, got: {err}"
    );
    assert!(
        !err.contains("backtrace"),
        "Error should not contain backtrace, got: {err}"
    );

    // Evaluate incomplete code
    let err = dap.evaluate_error("1 +", Some(frame_id));
    assert!(
        err.contains("Incomplete"),
        "Expected incomplete code error, got: {err}"
    );

    // Evaluate syntax error
    let err = dap.evaluate_error("foo)", Some(frame_id));
    assert_eq!(err, "Error: unexpected ')'");

    // Cause an R error during evaluation
    let err = dap.evaluate_error("stop('foo')", Some(frame_id));
    assert_eq!(err, "Error: foo");

    // Debug session should still be alive and stopped after all these errors
    let result = dap.evaluate("x", Some(frame_id));
    assert_eq!(result, "42");

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_top_level_browser() {
    // Top-level browser() frames have no function environment,
    // so they should fall back to global env
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    // Set a global variable
    frontend.send_execute_request("global_var <- 'from_global'", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();

    frontend.debug_send_browser();
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Evaluate with frame_id should work and use global env
    let result = dap.evaluate("global_var", Some(frame_id));
    assert_eq!(result, "\"from_global\"");

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_no_frame_id() {
    // Without frame ID, evaluation occurs in the global env per the DAP protocol
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    // Set a global variable and enter browser
    frontend.send_execute_request("global_var <- 123", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();

    frontend.debug_send_browser();
    dap.recv_stopped();

    // Evaluate without frame_id should use global environment
    let result = dap.evaluate("global_var", None);
    assert_eq!(result, "123");

    frontend.debug_send_quit();
    dap.recv_continued();
}

/// Test that selecting a frame via `.positron_selected_frame` causes console
/// evaluations to run in that frame's environment.
#[test]
fn test_dap_selected_frame_console_eval() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    // Create a stack with variables in different frames
    let _file = frontend.send_source(
        "
outer <- function() {
  outer_var <- 'from_outer'
  inner()
}
inner <- function() {
  inner_var <- 'from_inner'
  browser()
}
outer()
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    assert!(stack.len() >= 2);

    let inner_frame_id = stack[0].id;
    let outer_frame_id = stack[1].id;

    // Select the outer frame via the special DAP evaluate expression
    dap.evaluate(".positron_selected_frame", Some(outer_frame_id));

    // Now console eval should use the outer frame's environment.
    // Evaluate outer_var which only exists in the outer frame.
    // In debug mode, R prints to stdout instead of returning execute_result.
    frontend.send_execute_request("outer_var", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.assert_stream_stdout_contains("from_outer");
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();
    dap.recv_invalidated();

    // Frame IDs should remain valid after transient eval - we can still
    // select a different frame
    dap.evaluate(".positron_selected_frame", Some(inner_frame_id));

    // Console eval should now use the inner frame again
    frontend.send_execute_request("inner_var", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.assert_stream_stdout_contains("from_inner");
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();
    dap.recv_invalidated();

    // Clean exit
    frontend.debug_send_quit();
    dap.recv_continued();
}

/// After an error in a browser session, a selected frame should remain active.
/// https://github.com/posit-dev/positron/issues/12642
#[test]
fn test_dap_selected_frame_preserved_after_error() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
outer <- function() {
  outer_var <- 'from_outer'
  inner()
}
inner <- function() {
  inner_var <- 'from_inner'
  browser()
}
outer()
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let outer_frame_id = stack[1].id;

    // Select the outer frame
    dap.evaluate(".positron_selected_frame", Some(outer_frame_id));

    // Confirm we can see outer_var
    frontend.send_execute_request("outer_var", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.assert_stream_stdout_contains("from_outer");
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();
    dap.recv_invalidated();

    // Cause an error
    frontend.send_execute_request("stop('oops')", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.recv_iopub_execute_error();
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply_exception();
    dap.recv_invalidated();

    // The selected frame must still be the outer frame: outer_var is accessible
    frontend.send_execute_request("outer_var", Default::default());
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.assert_stream_stdout_contains("from_outer");
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();
    dap.recv_invalidated();

    // Clean exit
    frontend.debug_send_quit();
    dap.recv_continued();
}

/// A print method that raises an R error longjumps out of `Rf_PrintValue`. The
/// evaluation must surface that as an error and leave the `Dap` state usable, so
/// a follow-up evaluate still succeeds. Console and notebook share
/// `DapHandler::evaluate`; this checks the console (TCP) path, the notebook path
/// is checked in `test_notebook_evaluate_erroring_print_does_not_deadlock`.
#[test]
fn test_dap_evaluate_erroring_print_does_not_deadlock() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    // Define an S3 object whose print method errors.
    frontend.send_execute_request(
        "print.boom <- function(x, ...) stop('kaboom'); obj <- structure(1, class = 'boom')",
        Default::default(),
    );
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();

    frontend.debug_send_browser();
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // Printing `obj` dispatches `print.boom`, which longjumps out of R.
    let err = dap.evaluate_error("/print obj", Some(frame_id));
    assert!(err.contains("kaboom"), "got: {err}");

    // A follow-up evaluate still succeeds: the `Dap` state is left usable.
    let result = dap.evaluate("1 + 1", Some(frame_id));
    assert_eq!(result, "2");

    frontend.debug_send_quit();
    dap.recv_continued();
}

/// The watch pane evaluates expressions while stopped in a frame. A runaway
/// expression there (`repeat 1`) must time out and leave the debug session
/// intact rather than freezing the kernel. This is the console (TCP) path; the
/// notebook path is checked in `test_notebook_watch_pane_infloop_times_out`.
/// https://github.com/posit-dev/positron/issues/14481
#[test]
fn test_dap_evaluate_infloop_times_out() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  x <- 42
  browser()
})
",
    );
    dap.recv_stopped();

    let stack = dap.stack_trace();
    let frame_id = stack[0].id;

    // `repeat 1` never returns. The kernel must interrupt it after the timeout.
    let err = dap.evaluate_error("repeat 1", Some(frame_id));
    assert!(err.contains("timed out"), "got: {err}");

    // The session survived the interrupt: a normal watch expression in the same
    // frame still resolves.
    let result = dap.evaluate("x", Some(frame_id));
    assert_eq!(result, "42");

    frontend.debug_send_quit();
    dap.recv_continued();
}

#[test]
fn test_dap_evaluate_unknown_frame_id() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let _file = frontend.send_source(
        "
local({
  browser()
})
",
    );
    dap.recv_stopped();

    // Use a bogus frame_id that doesn't exist
    let err = dap.evaluate_error("1 + 1", Some(999999));
    assert!(
        err.contains("Unknown") && err.contains("frame_id"),
        "Expected 'Unknown frame_id' error, got: {err}"
    );

    // Debug session should still be alive
    let stack = dap.stack_trace();
    assert!(!stack.is_empty(), "Stack should still be available");

    frontend.debug_send_quit();
    dap.recv_continued();
}
