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

use amalthea::fixtures::dummy_frontend::ExecuteRequestOptions;
use ark_test::DummyArkFrontend;
use ark_test::SourceFile;
use tower_lsp_server::ls_types as lsp_types;

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

    let file = SourceFile::new(
        "1
2
3
",
    );

    // Set breakpoints before sourcing - they should be unverified
    let breakpoints = dap.set_breakpoints(&file.path, &[2]);
    assert_eq!(breakpoints.len(), 1);
    assert!(!breakpoints[0].verified);
    assert_eq!(breakpoints[0].line, Some(2));
}

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

    let file = SourceFile::new(
        "x <- 1
y <- 2
z <- 3
",
    );

    // Set a breakpoint
    let breakpoints = dap.set_breakpoints(&file.path, &[2]);
    assert_eq!(breakpoints.len(), 1);

    // Clear all breakpoints by sending empty list
    let breakpoints = dap.set_breakpoints(&file.path, &[]);
    assert!(breakpoints.is_empty());
}

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

    let file = SourceFile::new(
        "a <- 1
b <- 2
c <- 3
",
    );

    // Set initial breakpoints
    let breakpoints = dap.set_breakpoints(&file.path, &[2, 3]);
    assert_eq!(breakpoints.len(), 2);
    let id1 = breakpoints[0].id;
    let id2 = breakpoints[1].id;

    // Re-submit the same breakpoints - IDs should be preserved
    let breakpoints = dap.set_breakpoints(&file.path, &[2, 3]);
    assert_eq!(breakpoints.len(), 2);
    assert_eq!(breakpoints[0].id, id1);
    assert_eq!(breakpoints[1].id, id2);
}

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

    // Create file with a function we can set breakpoints on
    let file = SourceFile::new(
        "
bar <- function() {
  a <- 1
  b <- 2
  c <- 3
}
",
    );

    // Set breakpoint BEFORE sourcing (on line 4: b <- 2)
    let breakpoints = dap.set_breakpoints(&file.path, &[4]);
    assert_eq!(breakpoints.len(), 1);
    let original_id = breakpoints[0].id;

    // Source the file - breakpoint becomes verified during evaluation
    frontend.source_file(&file);

    let bp = dap.recv_breakpoint_verified();
    assert_eq!(bp.id, original_id);

    // Now "disable" the breakpoint by omitting it from the request
    let breakpoints = dap.set_breakpoints(&file.path, &[]);
    assert!(breakpoints.is_empty());

    // Re-enable by submitting the same line again.
    // The breakpoint should have the same ID and be immediately verified
    // (restored from disabled state without needing re-sourcing).
    let breakpoints = dap.set_breakpoints(&file.path, &[4]);
    assert_eq!(breakpoints.len(), 1);
    assert_eq!(breakpoints[0].id, original_id);
    assert!(breakpoints[0].verified);
}

/// Test that document hash changes cause breakpoint state to be discarded.
///
/// This is part of the document change invalidation coverage: when a file's
/// content changes (detected via hash), breakpoint IDs are regenerated and
/// state is reset. This complements the LSP `did_change_document()` unit tests
/// in `dap.rs`.
#[test]
fn test_dap_breakpoint_doc_hash_change_discards_state() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    // Create initial file
    let mut file = SourceFile::new(
        "x <- 1
y <- 2
z <- 3
",
    );

    // Set breakpoints and record IDs
    let breakpoints = dap.set_breakpoints(&file.path, &[2, 3]);
    assert_eq!(breakpoints.len(), 2);
    let id1 = breakpoints[0].id;
    let id2 = breakpoints[1].id;

    // Modify the file content (different hash)
    file.rewrite("a <- 10\nb <- 20\nc <- 30\n");

    // Re-submit breakpoints at the same lines
    let breakpoints = dap.set_breakpoints(&file.path, &[2, 3]);
    assert_eq!(breakpoints.len(), 2);

    // IDs should be new (state was discarded due to hash change)
    assert_ne!(breakpoints[0].id, id1);
    assert_ne!(breakpoints[1].id, id2);

    // Breakpoints should be unverified since they're new
    assert!(!breakpoints[0].verified);
    assert!(!breakpoints[1].verified);
}

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

    // Create two separate files
    let file_a = SourceFile::new(
        "
foo <- function() {
  1
}
",
    );

    let file_b = SourceFile::new(
        "
bar <- function() {
  2
}
",
    );

    // Set breakpoints in both files
    let breakpoints_a = dap.set_breakpoints(&file_a.path, &[3]);
    assert_eq!(breakpoints_a.len(), 1);
    let id_a = breakpoints_a[0].id;

    let breakpoints_b = dap.set_breakpoints(&file_b.path, &[3]);
    assert_eq!(breakpoints_b.len(), 1);
    let id_b = breakpoints_b[0].id;

    // Source only file A
    frontend.source_file(&file_a);

    // Only file A's breakpoint should be verified
    let bp = dap.recv_breakpoint_verified();
    assert_eq!(bp.id, id_a);

    // Re-query file B's breakpoints - should still be unverified
    let breakpoints_b = dap.set_breakpoints(&file_b.path, &[3]);
    assert_eq!(breakpoints_b.len(), 1);
    assert_eq!(breakpoints_b[0].id, id_b);
    assert!(!breakpoints_b[0].verified);

    // Clear file A's breakpoints
    let breakpoints_a = dap.set_breakpoints(&file_a.path, &[]);
    assert!(breakpoints_a.is_empty());

    // File B's breakpoints should still exist
    let breakpoints_b = dap.set_breakpoints(&file_b.path, &[3]);
    assert_eq!(breakpoints_b.len(), 1);
    assert_eq!(breakpoints_b[0].id, id_b);
}

/// Test that breakpoints set on a file that doesn't exist on disk (e.g. an
/// untitled, unsaved editor) come back as unverified with a helpful message.
#[test]
fn test_dap_breakpoints_unsaved_file_unverified() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();

    let path = std::env::temp_dir()
        .join("nonexistent_file_for_test.R")
        .to_string_lossy()
        .replace('\\', "/");
    let breakpoints = dap.set_breakpoints(&path, &[1, 5]);
    assert_eq!(breakpoints.len(), 2);

    assert!(!breakpoints[0].verified);
    assert_eq!(breakpoints[0].line, Some(1));
    assert!(breakpoints[0]
        .message
        .as_ref()
        .unwrap()
        .contains("Can't read file"));

    assert!(!breakpoints[1].verified);
    assert_eq!(breakpoints[1].line, Some(5));
    assert!(breakpoints[1]
        .message
        .as_ref()
        .unwrap()
        .contains("Can't read file"));
}

/// When the LSP signals a document change while R is paused at a `browser()` prompt, DAP
/// must still invalidate that file's breakpoints. The `process_console_notifications()`
/// task is routed through `IDLE_ANY_TASKS` so it drains at browser prompts.
#[test]
fn test_dap_an_lsp_did_change_notification_at_browser_invalidates_breakpoints() {
    let frontend = DummyArkFrontend::lock();
    let mut dap = frontend.start_dap();
    let mut lsp = frontend.start_lsp();

    let file = SourceFile::new(
        "
foo <- function() {
  browser()
  1
}
foo()
",
    );

    // Set a breakpoint on the line after `browser()`, then source the file. The
    // breakpoint becomes verified when the function definition is evaluated. The call to
    // `foo()` then pauses R at `browser()`.
    let breakpoints = dap.set_breakpoints(&file.path, &[4]);
    assert_eq!(breakpoints.len(), 1);
    let bp_id = breakpoints[0].id;

    frontend.send_execute_request(
        &format!("source('{}')", file.path),
        ExecuteRequestOptions::default(),
    );
    frontend.recv_iopub_busy();
    frontend.recv_iopub_execute_input();

    let bp = dap.recv_breakpoint_verified();
    assert_eq!(bp.id, bp_id);

    frontend.recv_iopub_start_debug();
    frontend.recv_iopub_idle();
    frontend.recv_shell_execute_reply();
    dap.recv_stopped();

    // Open the file on the LSP using its real URL so the LSP-side `UrlId`
    // canonicalizes to the same one the DAP indexed the breakpoint under.
    let uri = lsp_types::Uri::from_file_path(&file.path).unwrap();
    lsp.send_notification(
        "textDocument/didOpen",
        serde_json::to_value(lsp_types::DidOpenTextDocumentParams {
            text_document: lsp_types::TextDocumentItem {
                uri: uri.clone(),
                language_id: String::from("r"),
                version: 0,
                text: String::from("x <- 1\n"),
            },
        })
        .unwrap(),
    );

    // Send a full-document change. The LSP `did_change` handler forwards a
    // `ConsoleNotification::DidChangeDocument` to the console, which the DAP
    // turns into a `BreakpointState{verified:false}` event.
    lsp.send_notification(
        "textDocument/didChange",
        serde_json::to_value(lsp_types::DidChangeTextDocumentParams {
            text_document: lsp_types::VersionedTextDocumentIdentifier {
                uri: uri.clone(),
                version: 1,
            },
            content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
                range: None,
                range_length: None,
                text: String::from("x <- 2\n"),
            }],
        })
        .unwrap(),
    );

    // The event must arrive while we are still paused in the browser
    let bp = dap.recv_breakpoint_event();
    assert_eq!(bp.id, bp_id);
    assert!(!bp.verified);

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