use crate::common::fixtures::TestFixture;
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::input::commands::Suggestion;
use fresh::input::keybindings::Action;
use fresh::services::live_grep_state::{GrepMatch, LiveGrepLastState};
use fresh::view::prompt::PromptType;
use std::fs;

/// End-to-end coverage of the git-grep provider path: in a real git
/// working tree the registry should select git-grep (priority 0),
/// shell out, parse the column-aware output, and surface results in
/// the floating overlay's title and result list.
#[test]
fn test_live_grep_git_grep_flow_finds_match_in_repo() {
    let git_check = std::process::Command::new("git").arg("--version").output();
    if git_check.is_err() || !git_check.as_ref().unwrap().status.success() {
        eprintln!("Skipping test: `git` is not installed or not in PATH");
        return;
    }

    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_root = temp_dir.path().canonicalize().unwrap().join("project_root");
    fs::create_dir(&project_root).unwrap();

    // Initialise a real git repo so `git rev-parse --is-inside-work-tree`
    // succeeds and `isAvailable` returns true for git-grep.
    let run_git = |args: &[&str]| {
        let out = std::process::Command::new("git")
            .args(args)
            .current_dir(&project_root)
            .env("GIT_AUTHOR_NAME", "t")
            .env("GIT_AUTHOR_EMAIL", "t@t")
            .env("GIT_COMMITTER_NAME", "t")
            .env("GIT_COMMITTER_EMAIL", "t@t")
            .output()
            .unwrap();
        assert!(
            out.status.success(),
            "git {:?} failed: {}",
            args,
            String::from_utf8_lossy(&out.stderr)
        );
    };
    run_git(&["init", "--quiet", "-b", "main"]);

    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    let unique = "GIT_GREP_TOKEN_8a2e";
    let target_path = project_root.join("target.rs");
    fs::write(&target_path, format!("// {unique}\nfn target() {{}}\n")).unwrap();
    // git-grep only reports tracked files, so add + commit.
    run_git(&["add", "target.rs"]);
    run_git(&["commit", "--quiet", "-m", "seed"]);

    let start_file = project_root.join("start.txt");
    fs::write(&start_file, "start\n").unwrap();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        140,
        30,
        Default::default(),
        project_root.clone(),
    )
    .unwrap();

    harness.open_file(&start_file).unwrap();
    harness.render().unwrap();

    // Open palette → invoke "Live Grep".
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Live Grep").unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Live Grep"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    harness.type_text(unique).unwrap();

    // Two assertions in the rendered overlay:
    //   1. the result list contains target.rs (proves git-grep ran
    //      and parseGrepOutput parsed its `path:line:col:content`
    //      output).
    //   2. the overlay title carries the active provider name
    //      (proves git-grep — not a fallback — was selected).
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("target.rs") && s.contains("git-grep")
        })
        .unwrap();

    // No "⚠" — the error sentinel pushed by the Finder catch when
    // a provider throws. Its absence is the post-fix invariant.
    let screen = harness.screen_to_string();
    assert!(
        !screen.contains('⚠'),
        "overlay must not show an error indicator on a successful search; got:\n{}",
        screen
    );

    // Pressing Enter should open the matched file at the unique line.
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains(unique))
        .unwrap();
    let content = harness.get_buffer_content().unwrap();
    assert!(
        content.contains(unique),
        "buffer must contain the matched marker after Enter; got: {content}"
    );
}

/// Test Live Grep plugin - basic search and preview functionality
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_live_grep_basic_search() {
    // Create a temporary project directory
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_root = temp_dir.path().join("project_root");
    fs::create_dir(&project_root).unwrap();

    // Create plugins directory and copy the live_grep plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();

    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    // Create test files with searchable content
    let file1_content = "fn main() {\n    println!(\"Hello, world!\");\n}\n";
    let file2_content = "fn helper() {\n    println!(\"Helper function\");\n}\n";
    let file3_content = "// This file contains UNIQUE_MARKER for testing\nlet x = 42;\n";

    fs::write(project_root.join("main.rs"), file1_content).unwrap();
    fs::write(project_root.join("helper.rs"), file2_content).unwrap();
    fs::write(project_root.join("test.rs"), file3_content).unwrap();

    // Create a file to open initially
    let fixture = TestFixture::new("initial.txt", "Initial file content\n").unwrap();

    // Create harness with the project directory (so plugins load)
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(100, 30, Default::default(), project_root)
            .unwrap();

    // Open the initial file
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Open command palette and find Live Grep
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    harness.type_text("Live Grep").unwrap();

    // Wait for Live Grep to appear in palette (plugin loaded)
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("Live Grep") || s.contains("Find in Files")
        })
        .unwrap();

    // Execute the command
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Now we should be in the live grep prompt
    // Type a search query
    harness.type_text("UNIQUE_MARKER").unwrap();

    // Wait for search results to appear
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("test.rs") || s.contains("UNIQUE_MARKER")
        })
        .unwrap();

    // Press Escape to cancel
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Verify we're back to normal state
    let final_screen = harness.screen_to_string();
    assert!(
        final_screen.contains("Initial file content")
            || final_screen.contains("Live grep cancelled"),
        "Should return to normal state after ESC. Got:\n{}",
        final_screen
    );
}

/// Test Live Grep - selecting a result opens the file
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_live_grep_select_result() {
    // Create harness with temp project directory
    let mut harness =
        EditorTestHarness::with_temp_project_and_config(100, 30, Default::default()).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create plugins directory and copy the live_grep plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();

    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    // Create a test file with unique content
    let target_content = "// TARGET_FILE\nfn target_function() {\n    let result = 123;\n}\n";
    fs::write(project_root.join("target.rs"), target_content).unwrap();

    // Create initial file in project dir
    let start_file = project_root.join("start.txt");
    fs::write(&start_file, "Starting point\n").unwrap();

    harness.open_file(&start_file).unwrap();
    harness.render().unwrap();

    // Start Live Grep via command palette
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Live Grep").unwrap();

    // Wait for Live Grep command to appear (plugin loaded)
    harness
        .wait_until(|h| h.screen_to_string().contains("Live Grep"))
        .unwrap();

    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Search for the target
    harness.type_text("TARGET_FILE").unwrap();

    // Wait for results to appear
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("target.rs") || s.contains("TARGET_FILE")
        })
        .unwrap();

    // Press Enter to select the result
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for target file to open
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("TARGET_FILE") || s.contains("target_function")
        })
        .unwrap();
}

/// Test Live Grep - preview split appears and closes on ESC
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_live_grep_preview_split() {
    // Create harness with temp project directory
    let mut harness =
        EditorTestHarness::with_temp_project_and_config(120, 30, Default::default()).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create plugins directory and copy the live_grep plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();

    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    // Create a test file with content to search
    let search_content = "PREVIEW_TEST_CONTENT\nLine 2\nLine 3\nLine 4\nLine 5\n";
    fs::write(project_root.join("preview_test.txt"), search_content).unwrap();

    // Create initial file in project dir
    let main_file = project_root.join("main.txt");
    fs::write(&main_file, "Main file\n").unwrap();

    harness.open_file(&main_file).unwrap();
    harness.render().unwrap();

    // Start Live Grep via command palette
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Live Grep").unwrap();

    // Wait for Live Grep command to appear (plugin loaded)
    harness
        .wait_until(|h| h.screen_to_string().contains("Live Grep"))
        .unwrap();

    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Search for content
    harness.type_text("PREVIEW_TEST").unwrap();

    // Wait for preview split to appear
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("*Preview*") || s.contains("PREVIEW_TEST_CONTENT")
        })
        .unwrap();

    // Press ESC to cancel
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();

    // Wait for preview split to close
    harness
        .wait_until(|h| !h.screen_to_string().contains("*Preview*"))
        .unwrap();
}

/// Test Live Grep - input is preserved when navigating results
#[test]
fn test_live_grep_input_preserved() {
    // Create a temporary project directory
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_root = temp_dir.path().join("project_root");
    fs::create_dir(&project_root).unwrap();

    // Create plugins directory and copy the live_grep plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();

    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    // Create multiple files with matching content
    for i in 1..=5 {
        let content = format!("MULTI_MATCH line in file {}\n", i);
        fs::write(project_root.join(format!("file{}.txt", i)), content).unwrap();
    }

    // Create initial file
    let fixture = TestFixture::new("start.txt", "Start\n").unwrap();

    // Create harness
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(100, 30, Default::default(), project_root)
            .unwrap();

    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Start Live Grep via command palette
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Live Grep").unwrap();

    // Wait for Live Grep command to appear (plugin loaded)
    harness
        .wait_until(|h| h.screen_to_string().contains("Live Grep"))
        .unwrap();

    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Type search query
    harness.type_text("MULTI_MATCH").unwrap();

    // Wait for results to appear
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("file1.txt") || s.contains("MULTI_MATCH")
        })
        .unwrap();

    // Navigate down through results
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let screen_after_nav = harness.screen_to_string();
    println!("Screen after navigation:\n{}", screen_after_nav);

    // The prompt should still show "MULTI_MATCH" (input preserved)
    // This verifies our fix that plugin prompts don't overwrite input on navigation
    assert!(
        screen_after_nav.contains("MULTI_MATCH"),
        "Search input should be preserved when navigating results. Got:\n{}",
        screen_after_nav
    );

    // Clean up
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
}

/// Test Live Grep searches in the working directory, not the process current directory
///
/// This test verifies that when the editor's working directory is set to a path
/// different from the process's current directory, Live Grep searches in the
/// working directory (where the user's project is) rather than where fresh was launched.
#[test]
fn test_live_grep_uses_working_dir() {
    // Check if ripgrep is available (required by live_grep plugin)
    let rg_check = std::process::Command::new("rg").arg("--version").output();

    if rg_check.is_err() || !rg_check.as_ref().unwrap().status.success() {
        eprintln!("Skipping test: ripgrep (rg) is not installed or not in PATH");
        eprintln!("Live Grep plugin requires ripgrep to function");
        return;
    }

    // Create a temporary project directory - this will be our working_dir
    // It is intentionally different from std::env::current_dir()
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_root = temp_dir.path().join("project_root");
    fs::create_dir(&project_root).unwrap();

    // Create plugins directory and copy the live_grep plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();

    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "live_grep");

    // Create a test file with a unique marker that only exists in our temp project
    // This marker should NOT exist in the fresh repo's actual directory
    let unique_marker = "WORKDIR_TEST_UNIQUE_7f3a9b2c";
    let test_content = format!(
        "// This file contains {}\n// It should be found by live grep\nlet x = 42;\n",
        unique_marker
    );
    fs::write(project_root.join("workdir_test.rs"), test_content).unwrap();

    // Create initial file in project dir
    let start_file = project_root.join("start.txt");
    fs::write(&start_file, "Starting point for workdir test\n").unwrap();

    // Create harness with working_dir set to project_root
    // This is the key: working_dir != current_dir()
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(100, 30, Default::default(), project_root)
            .unwrap();

    harness.open_file(&start_file).unwrap();
    harness.render().unwrap();

    // Start Live Grep via command palette
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Live Grep").unwrap();

    // Wait for Live Grep command to appear (plugin loaded)
    harness
        .wait_until(|h| h.screen_to_string().contains("Live Grep"))
        .unwrap();

    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Search for our unique marker
    harness.type_text(unique_marker).unwrap();

    // Wait for results - should find our file in the working directory
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("workdir_test.rs")
        })
        .unwrap();

    // Verify the result is from our working directory
    harness.assert_screen_contains("workdir_test.rs");

    // Press Enter to open the file at the match location
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for the file to open - look for the unique marker in the screen
    // (it will appear in the editor content area once the file is loaded)
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains(unique_marker)
        })
        .unwrap();

    // Verify the buffer content is from our working directory
    let content = harness.get_buffer_content().unwrap();
    assert!(
        content.contains(unique_marker),
        "Buffer should contain the unique marker from working_dir. Got: {}",
        content
    );

    // Verify we're on line 1 (where the marker is)
    // The status bar format is "Ln X, Col Y" (1-indexed)
    let status_bar = harness.get_status_bar();
    assert!(
        status_bar.contains("Ln 1"),
        "Cursor should be on line 1 (the match line). Status bar: {}",
        status_bar
    );
}

/// Regression test for issue #1796 (capture side): cancelling the
/// Live Grep prompt with no input and no streamed results must NOT
/// populate the Resume cache. Pre-fix, `cancel_prompt` stored
/// `Some(LiveGrepLastState { cached_results: Some(vec![]), .. })`,
/// which combined with the Resume gate's `cached_results.is_some()`
/// check caused Resume to open an empty static popup.
#[test]
fn test_resume_live_grep_capture_skips_empty_dismissal() {
    let mut harness = EditorTestHarness::new(80, 24).unwrap();

    harness
        .editor_mut()
        .start_prompt("Live grep: ".to_string(), PromptType::LiveGrep);
    // Press Esc immediately — no input typed, no results seeded.
    harness.editor_mut().cancel_prompt();

    assert!(
        harness.editor().live_grep_last_state_for_tests().is_none(),
        "Cancelling Live Grep with empty input must not populate the Resume cache; \
         pre-fix this stored Some(LiveGrepLastState {{ cached_results: Some(vec![]), .. }}) \
         which made Resume open an empty popup."
    );
}

/// Regression test for the bug surfaced after the initial fix shipped:
/// pressing Enter on a Live Grep result jumps to the file but loses
/// the Resume cache, so Alt+r returns the user to a fresh-empty popup
/// instead of their match list. Pre-fix `confirm_prompt` had no
/// caching for Live Grep prompts; only `cancel_prompt` did. Post-fix
/// the confirm path mirrors the cancel path's gates.
#[test]
fn test_resume_live_grep_capture_on_confirm_with_results() {
    let mut harness = EditorTestHarness::new(80, 24).unwrap();

    harness
        .editor_mut()
        .start_prompt("Live grep: ".to_string(), PromptType::LiveGrep);
    if let Some(prompt) = harness.editor_mut().prompt_mut() {
        prompt.input = "needle".to_string();
        prompt.cursor_pos = prompt.input.len();
        let mut s = Suggestion::new("src/foo.rs:42".to_string());
        s.description = Some("fn needle() {}".to_string());
        s.value = Some("src/foo.rs:42:1".to_string());
        prompt.suggestions = vec![s];
        prompt.selected_suggestion = Some(0);
    }
    let _ = harness.editor_mut().confirm_prompt();

    let cached = harness
        .editor()
        .live_grep_last_state_for_tests()
        .expect("Confirming Live Grep on a real result must populate the Resume cache");
    assert_eq!(cached.query, "needle");
    assert_eq!(cached.selected_index, Some(0));
    let results = cached
        .cached_results
        .as_ref()
        .expect("cached_results must be Some after confirm");
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].file, "src/foo.rs");
    assert_eq!(results[0].line, 42);
}

/// Regression test for issue #1796 (replay side): even if a degenerate
/// `Some(empty Vec)` cache is somehow present, `ResumeLiveGrep` must
/// fall through to the fresh-start path rather than seeding an empty
/// `PromptType::LiveGrep` overlay. Defends against any future code
/// path that writes such a state, independent of the capture-side gate.
#[test]
fn test_resume_live_grep_replay_skips_empty_cache() {
    let mut harness = EditorTestHarness::new(80, 24).unwrap();

    harness
        .editor_mut()
        .set_live_grep_last_state_for_tests(Some(LiveGrepLastState {
            query: String::new(),
            selected_index: None,
            cached_results: Some(Vec::<GrepMatch>::new()),
            cached_at: None,
            last_results_snapshot_id: None,
        }));
    harness
        .editor_mut()
        .dispatch_action_for_tests(Action::ResumeLiveGrep);

    // Plugins aren't loaded in this minimal harness, so the fresh-start
    // path can't create a plugin prompt. Pre-fix the replay branch would
    // still seed a PromptType::LiveGrep overlay from the empty cache —
    // post-fix the gate rejects empty results so no prompt opens.
    let prompt_input = harness.editor().prompt_input();
    assert!(
        prompt_input.is_none(),
        "Resume with an empty cached_results must fall through to the fresh-start \
         path, not seed a PromptType::LiveGrep overlay from the empty cache. \
         Got prompt_input = {:?}",
        prompt_input
    );
}

/// Companion test: when there *is* a real cached state (non-empty
/// query and non-empty results), Resume must restore it.
#[test]
fn test_resume_live_grep_restores_real_cached_state() {
    let mut harness = EditorTestHarness::new(80, 24).unwrap();

    harness
        .editor_mut()
        .set_live_grep_last_state_for_tests(Some(LiveGrepLastState {
            query: "cached_query".to_string(),
            selected_index: Some(0),
            cached_results: Some(vec![GrepMatch {
                file: "src/foo.rs".to_string(),
                line: 42,
                column: 1,
                content: "fn cached_query() {}".to_string(),
            }]),
            cached_at: None,
            last_results_snapshot_id: None,
        }));
    harness
        .editor_mut()
        .dispatch_action_for_tests(Action::ResumeLiveGrep);
    assert_eq!(
        harness.editor().prompt_input(),
        Some("cached_query"),
        "Resume with a real cached state must restore the prior query"
    );
}

/// Regression test: opening the Utility Dock when a side-by-side
/// (vertical) split already exists must place the dock as a sibling
/// of the *root*, so it spans the full width below both panes — not
/// nested under whichever pane was focused.
///
/// Pre-fix the dock-creation site used `split_active_positioned`,
/// which split the active leaf and produced a tree like
/// `Vertical(left, Horizontal(right, dock))` — visually the dock
/// appeared only under the right pane. Post-fix, `split_root_positioned`
/// produces `Horizontal(Vertical(left, right), dock)` so the dock
/// spans the full editor width.
#[test]
fn test_open_terminal_in_dock_spans_full_width_with_existing_vsplit() {
    use fresh::view::split::SplitNode;
    use fresh_core::SplitDirection;

    let mut harness = EditorTestHarness::new(120, 40).unwrap();
    // Set up: vertical (side-by-side) split.
    harness.editor_mut().split_pane_vertical();
    {
        let sm = harness.editor().split_manager_for_tests();
        assert!(
            matches!(
                sm.root(),
                SplitNode::Split {
                    direction: SplitDirection::Vertical,
                    ..
                }
            ),
            "precondition: root must be a Vertical Split, got {:?}",
            sm.root()
        );
        assert_eq!(sm.root().count_leaves(), 2);
    }

    // Act: open the dock. Action::OpenTerminalInDock spawns a
    // terminal after creating the dock leaf; the terminal spawn is
    // best-effort in the harness but the split-tree mutation runs
    // synchronously before it, which is what we care about here.
    harness
        .editor_mut()
        .dispatch_action_for_tests(Action::OpenTerminalInDock);

    // Assert: root is now a Horizontal Split whose first child is
    // the original Vertical split (the two side-by-side editor
    // panes) and whose second child is the dock leaf.
    let sm = harness.editor().split_manager_for_tests();
    match sm.root() {
        SplitNode::Split {
            direction: SplitDirection::Horizontal,
            first,
            ..
        } => {
            assert!(
                matches!(
                    first.as_ref(),
                    SplitNode::Split {
                        direction: SplitDirection::Vertical,
                        ..
                    }
                ),
                "first child of root must be the original Vertical split — pre-fix \
                 the dock got nested under the active pane, leaving the root as the \
                 original Vertical and the dock as a child of one of its leaves. Got {:?}",
                first
            );
        }
        other => panic!(
            "root must be a Horizontal Split after dock creation, got {:?}",
            other
        ),
    }
    assert_eq!(
        sm.root().count_leaves(),
        3,
        "expected 3 leaves (left, right, dock); got tree: {:?}",
        sm.root()
    );
}

/// Regression test: a freshly-created Utility Dock must contain only
/// the buffer that triggered its creation (the terminal). Pre-fix
/// the dock was seeded with the user's previously-active buffer as a
/// placeholder, and `open_terminal()` then added the terminal as a
/// second tab — leaving a phantom tab for whatever the user had been
/// editing alongside the terminal in the dock.
#[test]
fn test_open_terminal_in_dock_has_only_terminal_tab() {
    use fresh::view::split::SplitRole;

    let mut harness = EditorTestHarness::new(120, 40).unwrap();
    let initial_buffer = harness.editor().active_buffer();

    harness
        .editor_mut()
        .dispatch_action_for_tests(Action::OpenTerminalInDock);

    let sm = harness.editor().split_manager_for_tests();
    let dock_leaf = sm
        .find_leaf_by_role(SplitRole::UtilityDock)
        .expect("dock leaf must be created by OpenTerminalInDock");
    let view_state = harness
        .editor()
        .split_view_state_for_tests(dock_leaf)
        .expect("dock leaf must have a SplitViewState");
    let tabs: Vec<_> = view_state.buffer_tab_ids_vec();

    assert_eq!(
        tabs.len(),
        1,
        "fresh dock must contain exactly one tab (the terminal); got {:?}",
        tabs
    );
    assert_ne!(
        tabs[0], initial_buffer,
        "the single tab must NOT be the user's previously-active buffer — \
         pre-fix the dock was seeded with that buffer as a placeholder."
    );
    // The lone tab is the terminal: the leaf's buffer_id and the
    // editor's active_buffer should both point at it (open_terminal
    // sets terminal mode and makes the terminal active).
    let leaf_buffer = sm.get_buffer_id(dock_leaf.into()).expect("leaf has buffer");
    assert_eq!(tabs[0], leaf_buffer);
}
