//! Plugin-related tests

use crate::common::fake_lsp::FakeLspServer;
use crate::common::fixtures::TestFixture;
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use fresh::services::lsp::LspServerConfig;
use fresh::services::process_limits::ProcessLimits;
use fresh::types::LspLanguageConfig;
use std::fs;
use std::time::Duration;

/// Test that render-line hook receives args properly
#[test]
fn test_render_line_hook_with_args() {
    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a simple plugin that captures render-line hook args
    let test_plugin = r###"
const editor = getEditor();
// Test plugin to verify render-line hook receives args
let line_count = 0;
let found_marker = false;

globalThis.onRenderLine = function(args: {
    buffer_id: number;
    line_number: number;
    byte_start: number;
    byte_end: number;
    content: string;
}): boolean {
    editor.debug("render-line hook called!");
    // Verify args are present
    if (args && args.buffer_id !== undefined && args.line_number !== undefined && args.content !== undefined) {
        line_count = line_count + 1;
        editor.debug(`Line ${args.line_number}: ${args.content}`);

        // Look for "TEST_MARKER" in the content
        if (args.content.includes("TEST_MARKER")) {
            found_marker = true;
            editor.debug("Found TEST_MARKER!");
            editor.setStatus(`Found TEST_MARKER on line ${args.line_number} at byte ${args.byte_start}`);
        }
    } else {
        editor.debug("ERROR: args is nil or missing fields!");
    }
    return true;
};

editor.on("render_line", "onRenderLine");

globalThis.test_show_count = function(): void {
    editor.setStatus(`Rendered ${line_count} lines, found=${found_marker}`);
    line_count = 0;  // Reset counter
    found_marker = false;
};

editor.registerCommand(
    "Test: Show Line Count",
    "Show how many lines were rendered",
    "test_show_count",
    "normal"
);

editor.setStatus("Test plugin loaded!");
"###;

    let test_plugin_path = plugins_dir.join("test_render_hook.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create test file with marker
    let test_file_content = "Line 1\nLine 2\nTEST_MARKER line\nLine 4\n";
    let fixture = TestFixture::new("test_render.txt", test_file_content).unwrap();

    // Create harness with the project directory
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
            .unwrap();

    // Open the test file - this should trigger render-line hooks
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Check that the test file is visible
    harness.assert_screen_contains("TEST_MARKER");

    // Run the "Show Line Count" command to verify hooks executed
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Show Line Count").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Check the rendered screen content - should still contain the test content
    harness.assert_screen_contains("TEST_MARKER");

    // The plugin should have detected the marker via render-line hooks
    // We verify this by checking that the screen was rendered successfully
}

/// Test plugin overlay API - plugins can add and clear overlays
#[test]
fn test_plugin_overlay_api() {
    init_tracing_from_env();

    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a test plugin that adds overlays
    let test_plugin = r#"
const editor = getEditor();

editor.registerCommand(
    "Test: Add Overlay",
    "Add a test overlay",
    "test_add_overlay",
    null
);

globalThis.test_add_overlay = function(): void {
    const bufferId = editor.getActiveBufferId();
    if (bufferId === null || bufferId === undefined) {
        editor.setStatus("No active buffer");
        return;
    }

    // Add an overlay with RGB color
    editor.addOverlay(bufferId, "test-ns", 0, 4, {
        fg: [255, 0, 0],  // Red foreground
    });

    editor.setStatus("Overlay added");
};

editor.setStatus("Overlay test plugin loaded");
"#;

    fs::write(plugins_dir.join("test_overlay.ts"), test_plugin).unwrap();

    // Create test file
    let test_file_content = "TEST content here\n";
    let fixture = TestFixture::new("test.txt", test_file_content).unwrap();

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

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

    // Execute the overlay command
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Add Overlay").unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Add Overlay"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for overlay to be applied
    harness
        .wait_until(|h| {
            let overlays = h.editor().active_state().overlays.all();
            !overlays.is_empty()
        })
        .unwrap();

    // Verify overlay was created
    let overlays = harness.editor().active_state().overlays.all();
    assert!(!overlays.is_empty(), "Expected at least one overlay");

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

/// Test plugin render-line hook receives correct arguments
#[test]
fn test_plugin_render_line_hook() {
    init_tracing_from_env();

    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a plugin that tracks render-line calls
    let test_plugin = r#"
const editor = getEditor();

let lineCount = 0;
let foundMarker = false;

globalThis.onRenderLine = function(args: {
    buffer_id: number;
    line_number: number;
    byte_start: number;
    byte_end: number;
    content: string;
}): boolean {
    if (args && args.content !== undefined) {
        lineCount++;
        if (args.content.includes("MARKER")) {
            foundMarker = true;
        }
    }
    return true;
};

editor.on("render_line", "onRenderLine");

globalThis.test_check_render = function(): void {
    editor.setStatus(`Lines: ${lineCount}, Marker: ${foundMarker}`);
};

editor.registerCommand(
    "Test: Check Render",
    "Check render-line results",
    "test_check_render",
    "normal"
);

editor.setStatus("Render hook test loaded");
"#;

    fs::write(plugins_dir.join("test_render_hook.ts"), test_plugin).unwrap();

    // Create test file with marker
    let test_file_content = "Line 1\nMARKER line\nLine 3\n";
    let fixture = TestFixture::new("test.txt", test_file_content).unwrap();

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

    // Open the test file - triggers render-line hooks
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Verify content is visible
    harness.assert_screen_contains("MARKER");

    // Check the render hook was called
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Check Render").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // The status should show lines were rendered and marker was found
    let screen = harness.screen_to_string();
    println!("Screen after check:\n{}", screen);
}

/// Test diagnostics panel plugin loads and creates a virtual buffer split
/// This verifies the full implementation with LSP-like diagnostics display
#[test]
#[cfg_attr(windows, ignore)] // Uses bash script for fake LSP server
fn test_diagnostics_panel_plugin_loads() {
    use crate::common::fake_lsp::FakeLspServer;
    init_tracing_from_env();

    // Create a temporary project directory
    let temp_dir = tempfile::TempDir::new().unwrap();

    // Create a fake LSP server that sends diagnostics
    let _fake_server = FakeLspServer::spawn_many_diagnostics(temp_dir.path(), 3).unwrap();
    let project_root = temp_dir.path().to_path_buf();

    // Create plugins directory and copy the diagnostics panel plugin
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin(&plugins_dir, "diagnostics_panel");
    copy_plugin_lib(&plugins_dir); // Copy lib/ for results-panel.ts import

    // Create a simple test file in the project directory (not via TestFixture!)
    let test_file_content = "fn main() {\n    println!(\"test\");\n}\n";
    let test_file = project_root.join("test_diagnostics.rs");
    fs::write(&test_file, test_file_content).unwrap();

    // Configure editor to use the fake LSP server that sends diagnostics
    let mut config = fresh::config::Config::default();
    config.lsp.insert(
        "rust".to_string(),
        fresh::types::LspLanguageConfig::Multi(vec![fresh::services::lsp::LspServerConfig {
            command: FakeLspServer::many_diagnostics_script_path(temp_dir.path())
                .to_string_lossy()
                .to_string(),
            args: Some(vec![]),
            enabled: true,
            auto_start: true,
            process_limits: fresh::services::process_limits::ProcessLimits::default(),
            initialization_options: None,
            env: Default::default(),
            language_id_overrides: Default::default(),
            root_markers: Default::default(),
            name: None,
            only_features: None,
            except_features: None,
        }]),
    );

    // Create harness with the project directory and LSP config
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(80, 24, config, project_root).unwrap();

    // Open the test file - this should trigger plugin loading and LSP
    harness.open_file(&test_file).unwrap();
    harness.render().unwrap();

    // Check that file content is visible
    harness.assert_screen_contains("fn main()");

    // Wait for LSP to send diagnostics (the fake server sends them on didOpen/didChange)
    // We wait for diagnostic overlays to appear since the status bar E: count may be
    // truncated on narrow terminals (the test uses 80 columns)
    harness
        .wait_until(|h| {
            // Check if diagnostic overlays have been applied
            let overlays = h.editor().active_state().overlays.all();
            let diagnostic_ns = fresh::services::lsp::diagnostics::lsp_diagnostic_namespace();
            overlays
                .iter()
                .any(|o| o.namespace.as_ref() == Some(&diagnostic_ns))
        })
        .unwrap();

    // The plugin should have loaded successfully without Lua errors
    // If the Lua scoping is wrong (update_panel_content not defined before create_panel calls it),
    // the plugin would fail to load with: "attempt to call a nil value (global 'update_panel_content')"

    // The plugin sets a status message on successful load
    // Look for evidence that the plugin loaded by checking the screen
    let screen = harness.screen_to_string();
    println!("Screen after plugin load:\n{}", screen);

    // Now try to execute the "Show Diagnostics Panel" command
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Type to search for the command
    harness.type_text("Show Diagnostics Panel").unwrap();
    harness.render().unwrap();

    let palette_screen = harness.screen_to_string();
    println!("Command palette screen:\n{}", palette_screen);

    // The command should be visible in the palette (registered by the plugin)
    // If the plugin failed to load due to Lua errors, this command wouldn't be registered
    assert!(
        palette_screen.contains("Show Diagnostics Panel")
            || palette_screen.contains("diagnostics")
            || palette_screen.contains("Diagnostics"),
        "The 'Show Diagnostics Panel' command should be registered by the plugin. \
         If the plugin had Lua scoping errors, it wouldn't load and the command wouldn't exist."
    );

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

    // Wait for the async panel creation to complete
    // The panel shows "Diagnostics" header when open
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("*Diagnostics*") || screen.contains("Diagnostics (")
        })
        .unwrap();

    let final_screen = harness.screen_to_string();
    println!("Final screen after executing command:\n{}", final_screen);

    // Verify the diagnostics panel content is displayed in a horizontal split
    assert!(
        final_screen.contains("Diagnostics"),
        "Expected to see 'Diagnostics' header in the panel"
    );
    assert!(
        final_screen.contains("[E]") || final_screen.contains("[W]"),
        "Expected to see severity icons like [E] or [W] in the diagnostics"
    );
    // The plugin uses background highlighting for selection, not a '>' marker
    assert!(
        final_screen.contains("*Diagnostics*"),
        "Expected to see buffer name '*Diagnostics*' in status bar"
    );
    // Verify horizontal split view (separator line should be visible)
    assert!(
        final_screen.contains("───") || final_screen.contains("---"),
        "Expected to see horizontal split separator"
    );
    // The original code buffer should still be visible above the diagnostics
    assert!(
        final_screen.contains("fn main()"),
        "Expected to see original code buffer in upper split"
    );
}

/// Test editor <-> plugin message queue architecture
///
/// This test exercises the complete bidirectional message flow:
/// 1. Editor sends action request to plugin thread (fire-and-forget)
/// 2. Plugin executes action and sends commands back via command queue
/// 3. Editor polls command queue with try_recv (non-blocking)
/// 4. Plugin awaits async response for buffer ID
///
/// This ensures no deadlocks occur in the message passing architecture.
#[test]
#[ignore = "need to revise TestFixture to put files under test-specific project root"]
fn test_plugin_message_queue_architecture() {
    init_tracing_from_env();

    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a simplified test plugin
    let test_plugin = r#"
const editor = getEditor();

editor.registerCommand(
    "Test: Create Virtual Buffer",
    "Create a virtual buffer",
    "test_create_virtual_buffer",
    null  // Use null for context to ensure command is visible
);

globalThis.test_create_virtual_buffer = function(): void {
    editor.setStatus("Virtual buffer command executed!");
};

editor.setStatus("Test plugin loaded");
"#;

    let test_plugin_path = plugins_dir.join("test_message_queue.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create test file in separate temp directory (like passing test)
    let test_file_content = "Test file content\nLine 2\nLine 3\n";
    let fixture = TestFixture::new("test_file.txt", test_file_content).unwrap();

    // Create harness with wide screen to avoid status message truncation
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(120, 30, Default::default(), project_root)
            .unwrap();

    // Open the test file from the separate temp directory
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Verify file content is visible
    harness.assert_screen_contains("Test file content");

    println!("opening command palette");
    // Execute the command via Quick Open
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Create Virtual Buffer").unwrap();

    println!("waiting for command palette");
    harness
        .wait_until(|h| h.screen_to_string().contains("Create Virtual Buffer"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for command execution status

    println!("opening command execution");
    harness
        .wait_until(|h| {
            h.screen_to_string()
                .contains("Virtual buffer command executed")
        })
        .unwrap();

    let final_screen = harness.screen_to_string();

    // Verify command executed
    assert!(
        final_screen.contains("Virtual buffer command executed"),
        "Expected status message to show. Got screen:\n{}",
        final_screen
    );
}

/// Test that multiple plugin actions can be queued without deadlock
#[test]
fn test_plugin_multiple_actions_no_deadlock() {
    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a plugin with multiple commands that all set status
    let test_plugin = r#"
const editor = getEditor();
// Test plugin for multiple concurrent actions

editor.registerCommand("Action A", "Set status to A", "action_a", null);
editor.registerCommand("Action B", "Set status to B", "action_b", null);
editor.registerCommand("Action C", "Set status to C", "action_c", null);

globalThis.action_a = function(): void {
    editor.setStatus("Status: A executed");
};

globalThis.action_b = function(): void {
    editor.setStatus("Status: B executed");
};

globalThis.action_c = function(): void {
    editor.setStatus("Status: C executed");
};

editor.setStatus("Multi-action plugin loaded");
"#;

    let test_plugin_path = plugins_dir.join("test_multi_action.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create a simple test file
    let test_file_content = "Test content\n";
    let fixture = TestFixture::new("test.txt", test_file_content).unwrap();

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

    // Open file and load plugins
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Execute multiple commands in sequence rapidly
    // This tests that the message queue handles multiple actions correctly
    // The main assertion is that the commands execute without deadlock or hanging
    let start = std::time::Instant::now();

    for action_name in ["Action A", "Action B", "Action C"] {
        // Open command palette
        harness
            .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
            .unwrap();
        harness.render().unwrap();

        // Type and execute command
        harness.type_text(action_name).unwrap();
        harness
            .send_key(KeyCode::Enter, KeyModifiers::NONE)
            .unwrap();

        // Process async and render a few times to let the action execute
        for _ in 0..3 {
            harness.process_async_and_render().unwrap();
            harness.sleep(Duration::from_millis(20));
        }
    }

    let elapsed = start.elapsed();

    // The key assertion: all commands should execute quickly without deadlock
    // If there's a deadlock or blocking issue, this would timeout
    assert!(
        elapsed < Duration::from_secs(2),
        "Multiple actions should complete without deadlock. Took {:?}",
        elapsed
    );

    // Verify the editor is still responsive
    let screen = harness.screen_to_string();
    assert!(
        screen.contains("Test content"),
        "Editor should still show content after multiple actions. Got:\n{}",
        screen
    );
}

/// Test that plugin action execution is non-blocking
///
/// This verifies that the editor doesn't hang when executing a plugin action
/// even if the action takes time. The fire-and-forget pattern should allow
/// the editor to continue processing events.
#[test]
fn test_plugin_action_nonblocking() {
    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a plugin that does some work
    let test_plugin = r#"
const editor = getEditor();
// Test plugin to verify non-blocking action execution

editor.registerCommand(
    "Slow Action",
    "An action that does some computation",
    "slow_action",
    "normal"
);

globalThis.slow_action = function(): void {
    // Simulate some work (this is synchronous but should not block editor)
    let sum = 0;
    for (let i = 0; i < 1000; i++) {
        sum += i;
    }
    editor.setStatus(`Completed: sum = ${sum}`);
};

editor.setStatus("Nonblocking test plugin loaded");
"#;

    let test_plugin_path = plugins_dir.join("test_nonblocking.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create a simple test file
    let test_file_content = "Test\n";
    let fixture = TestFixture::new("test.txt", test_file_content).unwrap();

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

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

    // Execute the slow action
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Slow Action").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // The key test: we should be able to render without blocking while action runs
    let start = std::time::Instant::now();

    // Process several render cycles - this verifies the editor isn't blocked
    for _ in 0..5 {
        harness.process_async_and_render().unwrap();
        harness.sleep(Duration::from_millis(50));
    }

    let elapsed = start.elapsed();

    // If the action was blocking, renders would stall
    // The action execution is async, so renders complete immediately
    assert!(
        elapsed < Duration::from_secs(1),
        "Rendering should complete quickly even with action running. Took {:?}",
        elapsed
    );

    // Verify the screen is still responsive (command palette closed)
    let screen = harness.screen_to_string();
    assert!(
        screen.contains("Test"),
        "Editor should show file content. Got:\n{}",
        screen
    );
}

/// Ensure the clangd plugin reacts to file-status notifications
#[test]
#[ignore]
fn test_clangd_plugin_file_status_notification() -> anyhow::Result<()> {
    init_tracing_from_env();

    let temp_dir = tempfile::TempDir::new().unwrap();
    let _fake_server = FakeLspServer::spawn(temp_dir.path())?;

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

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

    let src_dir = project_root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    let source_file = src_dir.join("main.cpp");
    fs::write(&source_file, "int main() { return 0; }\n").unwrap();

    let mut config = Config::default();
    config.lsp.insert(
        "cpp".to_string(),
        LspLanguageConfig::Multi(vec![LspServerConfig {
            command: FakeLspServer::script_path(temp_dir.path())
                .to_string_lossy()
                .to_string(),
            args: Some(vec![]),
            enabled: true,
            auto_start: true,
            process_limits: ProcessLimits::default(),
            initialization_options: None,
            env: Default::default(),
            language_id_overrides: Default::default(),
            root_markers: Default::default(),
            name: None,
            only_features: None,
            except_features: None,
        }]),
    );

    let mut harness =
        EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
            .unwrap();

    harness.open_file(&source_file)?;
    harness.render()?;
    for _ in 0..10 {
        harness.sleep(Duration::from_millis(100));
        let _ = harness.editor_mut().process_async_messages();
        harness.render()?;
    }

    let mut seen_status = false;
    for _ in 0..20 {
        harness.sleep(Duration::from_millis(50));
        let _ = harness.editor_mut().process_async_messages();
        harness.render()?;
        if let Some(msg) = harness.editor().get_status_message() {
            if msg == "Clangd file status: ready" {
                seen_status = true;
                break;
            }
        }
    }

    assert!(
        seen_status,
        "Expected clangd file status notification to set the plugin status"
    );

    Ok(())
}

/// Ensure the clangd plugin uses editor.sendLspRequest successfully
#[test]
#[cfg_attr(windows, ignore)] // Uses bash script for fake LSP server
fn test_clangd_plugin_switch_source_header() -> anyhow::Result<()> {
    init_tracing_from_env();

    let temp_dir = tempfile::TempDir::new().unwrap();
    let _fake_server = FakeLspServer::spawn(temp_dir.path())?;

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

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

    let src_dir = project_root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    let source_file = src_dir.join("main.cpp");
    fs::write(&source_file, "int main() { return 0; }\n").unwrap();
    let header_file = src_dir.join("main.h");
    fs::write(&header_file, "// header content\n").unwrap();

    let mut config = Config::default();
    config.lsp.insert(
        "cpp".to_string(),
        LspLanguageConfig::Multi(vec![LspServerConfig {
            command: FakeLspServer::script_path(temp_dir.path())
                .to_string_lossy()
                .to_string(),
            args: Some(vec![]),
            enabled: true,
            auto_start: true,
            process_limits: ProcessLimits::default(),
            initialization_options: None,
            env: Default::default(),
            language_id_overrides: Default::default(),
            root_markers: Default::default(),
            name: None,
            only_features: None,
            except_features: None,
        }]),
    );

    let mut harness =
        EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
            .unwrap();

    harness.open_file(&source_file)?;
    harness.render()?;

    // Open command palette and run the switch command
    // Wait for the command to appear (meaning plugin is loaded)
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Clangd: Switch Source/Header").unwrap();
    harness.wait_until(|h| h.screen_to_string().contains("Switch Source/Header"))?;
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for the header file content to appear
    harness.wait_until(|h| h.screen_to_string().contains("header content"))?;

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("header content"),
        "Expected header file to be visible, got:\n{}",
        screen
    );

    Ok(())
}

/// Test that plugin commands show the plugin name as source in command palette
#[test]
fn test_plugin_command_source_in_palette() {
    // Initialize tracing and signal handlers for debugging
    init_tracing_from_env();
    fresh::services::signal_handler::install_signal_handlers();

    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a simple plugin that registers a command
    // IMPORTANT: description must NOT contain "test_source_plugin" so we can verify
    // that the source column shows it (not the description)
    let test_plugin = r#"
const editor = getEditor();
// Simple test plugin to verify command source is shown correctly
editor.registerCommand(
    "Test Source Plugin Command",
    "A special command for testing",
    "test_source_action",
    null
);

editor.setStatus("Test source plugin loaded!");
"#;

    let test_plugin_path = plugins_dir.join("test_source_plugin.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create a test file
    let fixture = TestFixture::new("test.txt", "Test content\n").unwrap();

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

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

    // Wait for plugins to load
    for _ in 0..5 {
        harness.process_async_and_render().unwrap();
        harness.sleep(Duration::from_millis(50));
    }

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

    // Search for our plugin command
    harness.type_text("Test Source Plugin").unwrap();

    // Process to update suggestions
    for _ in 0..3 {
        harness.process_async_and_render().unwrap();
        harness.sleep(Duration::from_millis(50));
    }

    let screen = harness.screen_to_string();
    println!("Screen showing command palette:\n{}", screen);

    // Verify the command appears
    assert!(
        screen.contains("Test Source Plugin Command"),
        "Plugin command should appear in palette. Got:\n{}",
        screen
    );

    // Verify the source shows the plugin name, NOT "builtin"
    // The source column should show "test_source_p..." (truncated filename without .ts)
    assert!(
        screen.contains("test_source_p"),
        "Command source should show plugin name 'test_source_p...', not 'builtin'. Got:\n{}",
        screen
    );
    // Also verify it does NOT show "builtin" for this command
    // (Since the command is on screen, if it showed "builtin" we'd see it)
    // Note: We can't easily check the specific line, but the fact that test_source_p
    // appears AND builtin commands show "builtin" confirms the feature works

    // Also verify that builtin commands still show "builtin"
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Open palette again and search for a builtin command
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Save File").unwrap();

    for _ in 0..3 {
        harness.process_async_and_render().unwrap();
        harness.sleep(Duration::from_millis(50));
    }

    let screen2 = harness.screen_to_string();
    println!("Screen showing Save File command:\n{}", screen2);

    // Save File should show "builtin" as source
    assert!(
        screen2.contains("builtin"),
        "Builtin command should show 'builtin' as source. Got:\n{}",
        screen2
    );
}

/// Test that diagnostics from fake LSP are stored and accessible via getAllDiagnostics API
#[test]
#[cfg_attr(windows, ignore)] // Uses bash script for fake LSP server
fn test_diagnostics_api_with_fake_lsp() -> anyhow::Result<()> {
    init_tracing_from_env();

    let temp_dir = tempfile::TempDir::new().unwrap();
    let _fake_server = FakeLspServer::spawn(temp_dir.path())?;

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

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

    let lib_source_dir = std::env::current_dir().unwrap().join("plugins/lib");
    let lib_dest_dir = plugins_dir.join("lib");
    fs::create_dir(&lib_dest_dir).unwrap();
    for entry in fs::read_dir(&lib_source_dir).unwrap() {
        let entry = entry.unwrap();
        if entry.path().extension().map(|e| e == "ts").unwrap_or(false) {
            fs::copy(entry.path(), lib_dest_dir.join(entry.file_name())).unwrap();
        }
    }

    // Create a simple plugin that captures diagnostics via getAllDiagnostics
    let test_plugin = r#"/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();

// Test plugin to verify getAllDiagnostics API works with real LSP data
let diagnosticCount = 0;

globalThis.on_test_diagnostics_updated = function(data: { uri: string; count: number }): void {
    // When diagnostics update, query them and store count
    const allDiags = editor.getAllDiagnostics();
    diagnosticCount = allDiags.length;
    editor.setStatus(`Diagnostics received: ${diagnosticCount} total, URI count: ${data.count}`);
};

globalThis.get_diagnostic_count = function(): void {
    const allDiags = editor.getAllDiagnostics();
    diagnosticCount = allDiags.length;
    editor.setStatus(`Current diagnostics: ${diagnosticCount}`);
};

editor.on("diagnostics_updated", "on_test_diagnostics_updated");

editor.registerCommand(
    "Test: Get Diagnostic Count",
    "Report the number of diagnostics",
    "get_diagnostic_count",
    "normal"
);

editor.setStatus("Test diagnostics plugin loaded");
"#;

    fs::write(plugins_dir.join("test_diagnostics.ts"), test_plugin).unwrap();

    // Create a test Rust file
    let test_file = project_root.join("test.rs");
    fs::write(&test_file, "fn main() {\n    let x = 1;\n}\n").unwrap();

    // Configure fake LSP for Rust files
    let mut config = Config::default();
    config.lsp.insert(
        "rust".to_string(),
        LspLanguageConfig::Multi(vec![LspServerConfig {
            command: FakeLspServer::script_path(temp_dir.path())
                .to_string_lossy()
                .to_string(),
            args: Some(vec![]),
            enabled: true,
            auto_start: true,
            process_limits: ProcessLimits::default(),
            initialization_options: None,
            env: Default::default(),
            language_id_overrides: Default::default(),
            root_markers: Default::default(),
            name: None,
            only_features: None,
            except_features: None,
        }]),
    );

    let mut harness =
        EditorTestHarness::with_config_and_working_dir(100, 30, config, project_root.clone())
            .unwrap();

    // Wait for the plugin to load before anything can emit the event it
    // subscribes to. Its last statement sets a status message, so "the plugin
    // finished evaluating, and `editor.on("diagnostics_updated", …)` is
    // registered" is readable straight off the status bar. Without this the
    // plugin can subscribe after the diagnostics have already been published,
    // and `getAllDiagnostics` — the API under test — is never exercised at
    // all, silently.
    //
    // Readable here specifically because nothing is open yet: the status bar
    // elides elements that don't fit the terminal width, and once a file and
    // its diagnostics are on screen this message no longer fits at 100
    // columns.
    harness.wait_for_screen_contains("Test diagnostics plugin loaded")?;

    // Open the test file - this will start LSP
    harness.open_file(&test_file)?;
    harness.render()?;

    // Wait for the server to finish initializing before saving.
    //
    // This replaces `for _ in 0..10 { harness.sleep(100ms); … }`, which looks
    // like a one-second warm-up and is not one: `harness.sleep` advances
    // *logical* time only (see its doc comment), so the loop ran ten
    // iterations back-to-back and waited no real time whatsoever. The save
    // below therefore raced LSP startup. The fake server publishes
    // diagnostics only in reply to `textDocument/didSave`, so losing that
    // race means no diagnostics are ever published and the wait that follows
    // can never resolve — a 180 s nextest timeout, and precisely the
    // didOpen-before-everything-else ordering CONTRIBUTING.md Code §3 is
    // about.
    harness.wait_until(|h| h.editor().active_window().is_lsp_server_ready("rust"))?;

    // Save the file to trigger diagnostics from fake LSP
    harness
        .send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render()?;

    // Wait for the published diagnostic to reach the screen: the status bar's
    // diagnostics element shows `E:1` once one error is known for the buffer.
    //
    // Deliberately *not* the plugin's own status text. That message does
    // reach the status bar, but the bar lays its elements out within the
    // terminal width and elides what doesn't fit — at this test's 100
    // columns "Diagnostics received: 1 total, URI count: 1" renders as
    // "Diagnostics ...", so waiting for the full string can never resolve.
    // `E:1` is the compact, layout-stable rendering of the same fact.
    harness.wait_for_screen_contains("E:1")?;

    // Verify the diagnostics content
    let stored = harness.editor().get_stored_diagnostics();
    assert_eq!(stored.len(), 1, "Expected diagnostics for one file");

    // Get the diagnostics for our file
    // Note: On macOS, temp paths like /var/folders/... get canonicalized to /private/var/folders/...
    // so we need to canonicalize the path before constructing the URI
    let canonical_path = test_file
        .canonicalize()
        .unwrap_or_else(|_| test_file.clone());
    let file_uri = format!("file://{}", canonical_path.to_string_lossy());
    let diags = stored
        .get(&file_uri)
        .expect("Should have diagnostics for test file");
    assert_eq!(diags.len(), 1, "Expected exactly one diagnostic");

    // Verify the diagnostic content matches what fake LSP sends
    let diag = &diags[0];
    assert_eq!(
        diag.message, "Test error from fake LSP",
        "Diagnostic message should match fake LSP"
    );
    assert_eq!(
        diag.severity,
        Some(lsp_types::DiagnosticSeverity::ERROR),
        "Diagnostic severity should be error"
    );

    harness.assert_no_plugin_errors();

    Ok(())
}

/// Test theme-aware overlay support
///
/// Verifies that overlays with theme keys resolve to the correct colors
/// from the current theme at render time.
#[test]
fn test_theme_aware_overlay() {
    init_tracing_from_env();

    // 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
    let plugins_dir = project_root.join("plugins");
    fs::create_dir(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir); // Required for TypeScript type declarations

    // Create a simple plugin that adds theme-aware overlays
    let test_plugin = r###"
const editor = getEditor();

globalThis.test_theme_overlay = function(): void {
    const bufferId = editor.getActiveBufferId();
    // bufferId is a valid non-negative number (0 is the first buffer)
    if (bufferId === null || bufferId === undefined) {
        editor.setStatus("No active buffer");
        return;
    }

    // Clear any existing overlays
    editor.clearNamespace(bufferId, "test-theme");

    // Add overlay with theme key for foreground (syntax.keyword)
    editor.addOverlay(bufferId, "test-theme", 0, 4, {
        fg: "syntax.keyword",  // theme key - should resolve to keyword color
    });

    // Add overlay with theme key for background
    editor.addOverlay(bufferId, "test-theme", 5, 9, {
        fg: [255, 255, 255],  // RGB fallback white
        bg: "editor.selection_bg",  // theme key for background
    });

    editor.setStatus("Theme overlays applied");
};

editor.registerCommand(
    "Test: Apply Theme Overlay",
    "Apply overlays with theme keys",
    "test_theme_overlay",
    null  // Use null for context to ensure command is visible
);

editor.debug("Theme overlay test plugin loaded");
"###;

    let test_plugin_path = plugins_dir.join("test_theme_overlay.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    // Create test file INSIDE project_root (not a separate temp directory)
    let test_file_path = project_root.join("test_theme.txt");
    fs::write(&test_file_path, "TEST WORD here\nSecond line\n").unwrap();

    // Create harness with wide screen to avoid status message truncation
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(120, 30, Default::default(), project_root)
            .unwrap();

    // Open the test file
    harness.open_file(&test_file_path).unwrap();
    harness.render().unwrap();

    // Execute the theme overlay command
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Apply Theme Overlay").unwrap();
    // Wait for command to appear in Quick Open
    harness
        .wait_until(|h| h.screen_to_string().contains("Apply Theme Overlay"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for overlays to be applied
    harness
        .wait_until(|h| {
            if let Some(status) = h.editor().get_status_message() {
                status.contains("Theme overlays applied")
            } else {
                false
            }
        })
        .unwrap();

    harness.render().unwrap();

    // Find the screen position of "TEST" - it's not at (0,0) due to menu bar and tabs
    let screen = harness.screen_to_string();
    println!("Screen content:\n{}", screen);

    // Find the line containing "TEST" and get its y position
    let mut test_y: Option<u16> = None;
    let mut test_x: Option<u16> = None;
    for (y, line) in screen.lines().enumerate() {
        if let Some(x) = line.find("TEST") {
            test_y = Some(y as u16);
            test_x = Some(x as u16);
            println!("Found 'TEST' at screen position ({}, {})", x, y);
            break;
        }
    }

    let (x, y) = match (test_x, test_y) {
        (Some(x), Some(y)) => (x, y),
        _ => panic!("Could not find 'TEST' on screen"),
    };

    // Check that the overlay was applied with a real color
    // The first character of "TEST" should have the syntax.keyword color
    let style_at_test = harness.get_cell_style(x, y);
    println!("Style at 'T' position ({}, {}): {:?}", x, y, style_at_test);

    // Get the overlays from the buffer to verify they exist
    let overlays = harness.editor().active_state().overlays.all();
    println!("Number of overlays: {}", overlays.len());

    for (i, overlay) in overlays.iter().enumerate() {
        println!("Overlay {}: face={:?}", i, overlay.face);
    }

    // Verify overlays were created
    assert!(
        overlays.len() >= 2,
        "Expected at least 2 overlays to be created, got {}",
        overlays.len()
    );

    // Check that the foreground color at "T" is a themed color
    let style = style_at_test.expect("Should have style at TEST position");
    println!("Foreground color at T: {:?}", style.fg);

    // The color should be resolved from the theme (syntax.keyword)
    // It should NOT be the default editor foreground (White) - it should be themed
    let fg = style.fg.expect("Should have foreground color");

    // The theme's syntax.keyword color should be different from the default White
    // (high-contrast theme uses Cyan for syntax.keyword)
    assert!(
        !matches!(fg, ratatui::style::Color::White),
        "Theme key 'syntax.keyword' was not resolved - still showing default White. \
         Got {:?}, expected a themed color like Cyan or RGB.",
        fg
    );
    println!("Theme overlay resolved to: {:?}", fg);

    let screen = harness.screen_to_string();
    println!("Final screen:\n{}", screen);
}

/// Test getAllCursors with active selection: select text and uppercase it
///
/// This verifies the full round-trip of:
/// 1. getAllCursors() returns CursorInfo[] with selection ranges
/// 2. getBufferText() reads selected byte range
/// 3. deleteRange() + insertText() replaces selection with uppercased text
#[test]
fn test_get_all_cursors_uppercase_selection() {
    init_tracing_from_env();

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

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

    // Plugin: read each cursor's selection, fetch text, uppercase, replace
    let test_plugin = r#"/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();

globalThis.uppercase_selection = async function(): Promise<void> {
    const bufferId = editor.getActiveBufferId();
    if (bufferId === null || bufferId === undefined) {
        editor.setStatus("ERR: no buffer");
        return;
    }
    const cursors = editor.getAllCursors();
    if (!cursors || cursors.length === 0) {
        editor.setStatus("ERR: no cursors");
        return;
    }
    let replaced = 0;
    for (const c of cursors) {
        if (c.selection) {
            const start = c.selection.start;
            const end = c.selection.end;
            const text: string = await editor.getBufferText(bufferId, start, end);
            const upper = text.toUpperCase();
            editor.deleteRange(bufferId, start, end);
            editor.insertText(bufferId, start, upper);
            replaced++;
        }
    }
    editor.setStatus(`Uppercased ${replaced} selection(s)`);
};

editor.registerCommand(
    "Test: Uppercase Selection",
    "Uppercase the selected text via getAllCursors",
    "uppercase_selection",
    null
);
editor.setStatus("Uppercase plugin loaded");
"#;

    fs::write(plugins_dir.join("test_uppercase.ts"), test_plugin).unwrap();

    // Create test file with known content
    let test_file_path = project_root.join("test_upper.txt");
    fs::write(&test_file_path, "hello world\n").unwrap();

    // 160×30 instead of 120×30: with `{remote}` on the default
    // status bar the composed Messages line ("Opened
    // test_upper.txt | Uppercase plugin loaded") gets truncated
    // to "Uppercase plugin ..." at 120 cols. The wait_until
    // below looks for the literal "Uppercase plugin loaded".
    let mut harness =
        EditorTestHarness::with_config_and_working_dir(160, 30, Default::default(), project_root)
            .unwrap();

    harness.open_file(&test_file_path).unwrap();
    harness.render().unwrap();
    harness.assert_screen_contains("hello world");

    // Wait for plugin to load
    harness
        .wait_until(|h| h.screen_to_string().contains("Uppercase plugin loaded"))
        .unwrap();

    // Move cursor to beginning of "hello" and select 5 characters ("hello")
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    for _ in 0..5 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::SHIFT)
            .unwrap();
    }
    harness.render().unwrap();

    // Execute uppercase command via command palette
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Test: Uppercase Selection").unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Uppercase Selection"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for the async plugin to finish
    harness
        .wait_until(|h| h.screen_to_string().contains("Uppercased 1 selection(s)"))
        .unwrap();

    harness.render().unwrap();

    // Verify: "hello" should now be "HELLO", rest unchanged
    let screen = harness.screen_to_string();
    println!("Screen after uppercase:\n{}", screen);
    assert!(
        screen.contains("HELLO world"),
        "Expected 'HELLO world' after uppercasing selection. Got:\n{}",
        screen
    );
}

#[test]
fn test_plugin_has_active_search() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_root = temp_dir.path().join("project_root");
    fs::create_dir(&project_root).unwrap();

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

    // Create a simple plugin that provides a command to query hasActiveSearch()
    let test_plugin = r###"
const editor = getEditor();

globalThis.test_has_active_search = function(): void {
    let isActive = editor.hasActiveSearch();
    editor.insertAtCursor(`[SearchActive=${isActive}]`);
};

editor.registerCommand(
    "Test: Verify Active Search",
    "Check if search is active",
    "test_has_active_search",
    null
);
"###;

    let test_plugin_path = plugins_dir.join("test_search_plugin.ts");
    fs::write(&test_plugin_path, test_plugin).unwrap();

    let test_file_content = "hello world\nfoo bar\nhello again\nbaz\n";
    let test_file_path = project_root.join("test.txt");
    fs::write(&test_file_path, test_file_content).unwrap();

    let mut harness =
        EditorTestHarness::with_config_and_working_dir(80, 24, Config::default(), project_root)
            .unwrap();

    harness.open_file(&test_file_path).unwrap();
    harness.process_async_and_render().unwrap();

    // Check initial state
    // Wait until the command is registered by the plugin
    harness
        .wait_until(|h| {
            h.editor()
                .command_registry()
                .read()
                .unwrap()
                .find_by_name("Test: Verify Active Search")
                .is_some()
        })
        .expect("Wait for plugin to register command");

    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.process_async_and_render().unwrap();
    harness.type_text("Test: Verify Active Search").unwrap();
    harness.process_async_and_render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    harness
        .wait_until(|h| h.screen_to_string().contains("[SearchActive=false]"))
        .expect("Command 1");

    // Trigger search
    harness
        .send_key(KeyCode::Char('f'), KeyModifiers::CONTROL)
        .unwrap();
    harness.process_async_and_render().unwrap();
    harness.type_text("hello").unwrap();
    harness.process_async_and_render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.process_async_and_render().unwrap();

    // IMPORTANT: Wait for the search to actually take effect before
    // querying it. Observe the rendered match highlight rather than a
    // status-bar string: the status bar truncates unpredictably at narrow
    // widths (the old "Found 2 matches" → "F..." truncation depended on
    // the now-removed " | " separator), and e2e tests must assert on
    // rendered output, not model accessors (CONTRIBUTING.md). A search
    // applies the theme's `search.match_bg` to each matched "hello", so
    // the matched cells carry a background distinct from the unmatched
    // text after them on the same row — a color-independent signal that
    // only appears once the search has run.
    harness
        .wait_until(|h| {
            let buffer = h.buffer();
            let (width, height) = (buffer.area.width, buffer.area.height);
            // Rows 0-1 are the menu/tab bar; scan content rows only.
            (2..height).any(|y| {
                (0..width.saturating_sub(8)).any(|x| {
                    let spells_hello = ["h", "e", "l", "l", "o"]
                        .iter()
                        .enumerate()
                        .all(|(i, c)| h.get_cell(x + i as u16, y).as_deref() == Some(*c));
                    if !spells_hello {
                        return false;
                    }
                    // "hello" is highlighted; the 'w' of "world" (or 'a' of
                    // "again") two cells past the match's end is not.
                    let match_bg = h.get_cell_style(x, y).and_then(|s| s.bg);
                    let after_bg = h.get_cell_style(x + 6, y).and_then(|s| s.bg);
                    match_bg.is_some() && match_bg != after_bg
                })
            })
        })
        .expect("Wait for search highlights");

    // Verify search is active
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.process_async_and_render().unwrap();
    harness.type_text("Test: Verify Active Search").unwrap();
    harness.process_async_and_render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    harness
        .wait_until(|h| h.screen_to_string().contains("[SearchActive=true]"))
        .expect("Command 2");

    // Clear search
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.process_async_and_render().unwrap();
    harness.type_text("Clear Search Highlights").unwrap();
    harness.process_async_and_render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.process_async_and_render().unwrap();

    // Verify search is inactive again
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.process_async_and_render().unwrap();
    harness.type_text("Test: Verify Active Search").unwrap();
    harness.process_async_and_render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    harness
        .wait_until(|h| h.screen_to_string().contains("[SearchActive=false]"))
        .expect("Command 3");
}
