//! Pins the invariant that `BufferInfo.splits` in the plugin state
//! snapshot reflects every split currently holding the buffer.
//!
//! Plugins like the devcontainer build-log opener read this to
//! implement "focus existing buffer if visible, else open new" —
//! the alternative is tracking split ids in module state, which
//! dies across editor restarts when split ids get reassigned.
//! Regression here would silently re-break the "Show Build Logs
//! spawns a new split every time" UX.

#![cfg(feature = "plugins")]

use crate::common::harness::EditorTestHarness;
use fresh_core::BufferId;
use std::fs;

/// Look the buffer up by id in the plugin state snapshot and return
/// its `splits` as plain `usize`s. Going through buffer id — not
/// path — sidesteps the filesystem canonicalisation that
/// `open_file_no_focus` applies: the path stored on `BufferInfo` is
/// the canonical form (symlinks resolved), which may or may not
/// match `tempfile::tempdir()` output depending on the CI host's
/// `/tmp` layout.
fn snapshot_splits_for_buffer(
    harness: &EditorTestHarness,
    buffer_id: BufferId,
) -> Option<Vec<usize>> {
    let snapshot_handle = harness.editor().plugin_manager().state_snapshot_handle()?;
    let snapshot = snapshot_handle.read().ok()?;
    snapshot
        .buffers
        .get(&buffer_id)
        .map(|b| b.splits.iter().map(|s| s.0).collect())
}

fn snapshot_active_split(harness: &EditorTestHarness) -> Option<usize> {
    let snapshot_handle = harness.editor().plugin_manager().state_snapshot_handle()?;
    let snapshot = snapshot_handle.read().ok()?;
    Some(snapshot.active_split_id)
}

/// Number of buffers the plugin state snapshot knows about. Used to
/// detect an orphan buffer leaked by a failed open.
fn snapshot_buffer_count(harness: &EditorTestHarness) -> usize {
    let snapshot_handle = harness
        .editor()
        .plugin_manager()
        .state_snapshot_handle()
        .expect("snapshot handle present");
    let snapshot = snapshot_handle.read().expect("snapshot readable");
    snapshot.buffers.len()
}

/// Whether any buffer in the snapshot corresponds to `path`. Matches on
/// the file name to sidestep the symlink canonicalisation that the file
/// open path applies (see the note on `snapshot_splits_for_buffer`).
fn snapshot_has_buffer_for_path(harness: &EditorTestHarness, path: &std::path::Path) -> bool {
    let want = path.file_name();
    let snapshot_handle = harness
        .editor()
        .plugin_manager()
        .state_snapshot_handle()
        .expect("snapshot handle present");
    let snapshot = snapshot_handle.read().expect("snapshot readable");
    snapshot
        .buffers
        .values()
        .filter_map(|b| b.path.as_ref())
        .any(|p| p.file_name() == want)
}

/// A buffer open in a single split should report exactly that
/// split id in `BufferInfo.splits`.
#[test]
fn buffer_info_splits_reports_single_split() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("hello.txt");
    fs::write(&path, "hi\n").unwrap();

    let mut harness = EditorTestHarness::new(80, 24).unwrap();
    let buffer_id = harness.editor_mut().open_file(&path).unwrap();
    harness.tick_and_render().unwrap();

    let splits = snapshot_splits_for_buffer(&harness, buffer_id)
        .expect("snapshot should know the buffer we just opened");
    assert_eq!(
        splits.len(),
        1,
        "Buffer open in one split must report exactly one split id. splits={:?}",
        splits
    );
}

/// Splitting the pane holding a buffer puts the same buffer in
/// both splits (the new split inherits the active buffer). The
/// snapshot must surface both split ids so a plugin's "already
/// visible?" check sees the full set.
#[test]
fn buffer_info_splits_reports_both_splits_after_split_horizontal() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("hello.txt");
    fs::write(&path, "hi\n").unwrap();

    let mut harness = EditorTestHarness::new(80, 24).unwrap();
    let buffer_id = harness.editor_mut().open_file(&path).unwrap();
    harness.tick_and_render().unwrap();

    harness
        .editor_mut()
        .dispatch_action_for_tests(fresh::input::keybindings::Action::SplitHorizontal);
    harness.tick_and_render().unwrap();

    let splits = snapshot_splits_for_buffer(&harness, buffer_id)
        .expect("snapshot should still know the buffer after the split");
    assert_eq!(
        splits.len(),
        2,
        "After split_horizontal, the buffer is in two splits. splits={:?}",
        splits
    );
    // The two split ids must be distinct.
    assert_ne!(
        splits[0], splits[1],
        "The two splits showing the buffer must have distinct ids. splits={:?}",
        splits
    );
}

/// A plugin can use `BufferInfo.splits` together with `focusSplit`
/// to implement the "focus existing buffer if visible" pattern.
/// Drive the exact sequence end-to-end: open a buffer in a split,
/// move focus to a different split, then simulate the plugin
/// reading the snapshot to refocus the original split.
#[test]
fn buffer_info_splits_drives_refocus_pattern() {
    let temp = tempfile::tempdir().unwrap();
    let hello = temp.path().join("hello.txt");
    let world = temp.path().join("world.txt");
    fs::write(&hello, "hello\n").unwrap();
    fs::write(&world, "world\n").unwrap();

    let mut harness = EditorTestHarness::new(80, 24).unwrap();
    let hello_id = harness.editor_mut().open_file(&hello).unwrap();
    harness.tick_and_render().unwrap();

    // Split horizontal and open world.txt in the new (now-active)
    // split. Two splits: top hello, bottom world.
    harness
        .editor_mut()
        .dispatch_action_for_tests(fresh::input::keybindings::Action::SplitHorizontal);
    harness.editor_mut().open_file(&world).unwrap();
    harness.tick_and_render().unwrap();

    // The active split is the bottom one (world.txt). Now simulate
    // a plugin that wants to focus the hello.txt split.
    let hello_splits = snapshot_splits_for_buffer(&harness, hello_id)
        .expect("hello.txt must still be in the snapshot");
    assert_eq!(hello_splits.len(), 1, "hello.txt is in exactly one split");
    let hello_split = hello_splits[0];

    // Sanity: the active split is NOT the hello split (it's the
    // bottom world split).
    let active = snapshot_active_split(&harness).unwrap();
    assert_ne!(
        active, hello_split,
        "Sanity: active split should be the world.txt split, not hello's"
    );

    // Drive the focus via the same PluginCommand the plugin would
    // send.
    harness
        .editor_mut()
        .handle_plugin_command(fresh::services::plugins::api::PluginCommand::FocusSplit {
            split_id: fresh_core::SplitId(hello_split),
        })
        .unwrap();
    harness.tick_and_render().unwrap();

    let active_after = snapshot_active_split(&harness).unwrap();
    assert_eq!(
        active_after, hello_split,
        "focusSplit(hello_split) must make that split active. was {} now {}",
        active, active_after
    );
}

/// Regression for #2769: `openFileInSplit` against a split id that no
/// longer resolves to a live split must report failure (an `Err` from
/// the command dispatcher) instead of masquerading as success — and it
/// must NOT leak an invisible orphan buffer for the file it failed to
/// place.
#[test]
fn open_file_in_split_fails_for_dead_split_id() {
    use fresh::services::plugins::api::PluginCommand;
    use fresh_core::SplitId;

    let temp = tempfile::tempdir().unwrap();
    let hello = temp.path().join("hello.txt");
    let world = temp.path().join("world.txt");
    fs::write(&hello, "hello\n").unwrap();
    fs::write(&world, "world\n").unwrap();

    let mut harness = EditorTestHarness::new(80, 24).unwrap();
    harness.editor_mut().open_file(&hello).unwrap();
    harness.tick_and_render().unwrap();

    // Split horizontally: the new (now-active) split is the one we will
    // kill, so its id becomes a dangling reference.
    harness
        .editor_mut()
        .dispatch_action_for_tests(fresh::input::keybindings::Action::SplitHorizontal);
    harness.tick_and_render().unwrap();
    let dead_split = snapshot_active_split(&harness).expect("active split id after split");

    // Close that split — collapses the layout back to a single pane and
    // leaves `dead_split` pointing at nothing.
    harness
        .editor_mut()
        .handle_plugin_command(PluginCommand::CloseSplit {
            split_id: SplitId(dead_split),
        })
        .unwrap();
    harness.tick_and_render().unwrap();

    let live_split = snapshot_active_split(&harness).expect("active split id after close");
    assert_ne!(
        live_split, dead_split,
        "sanity: the surviving split must differ from the closed one"
    );

    // Baseline buffer count before the doomed open. world.txt has not
    // been opened, so it must not appear.
    let buffers_before = snapshot_buffer_count(&harness);

    // The plugin targets the dead split. This must FAIL (Err), not
    // silently return success.
    let result = harness
        .editor_mut()
        .handle_plugin_command(PluginCommand::OpenFileInSplit {
            split_id: dead_split,
            path: world.clone(),
            line: Some(0),
            column: Some(0),
        });
    assert!(
        result.is_err(),
        "openFileInSplit against a dead split id must report failure, got {:?}",
        result
    );
    harness.tick_and_render().unwrap();

    // No orphan buffer: the failed open must not have loaded world.txt
    // into a hidden buffer that no split displays.
    let buffers_after = snapshot_buffer_count(&harness);
    assert_eq!(
        buffers_after, buffers_before,
        "failed openFileInSplit must not leak a buffer (before={buffers_before}, after={buffers_after})"
    );

    // And no split displays world.txt: every buffer known to the
    // snapshot is displayed in at least the original pane, none of them
    // is the file we tried (and failed) to place.
    assert!(
        !snapshot_has_buffer_for_path(&harness, &world),
        "world.txt must not be present in any buffer after the failed open"
    );
}
