use crate::help::HelpPopupState;
use crate::input::InputState;
use crate::query::{Debouncer, QueryState};
use crate::autocomplete::{self, AutocompleteState, insertion};
use crate::config::{ClipboardBackend, Config};
use crate::history::HistoryState;
use crate::notification::NotificationState;
use crate::scroll::ScrollState;
use crate::search::SearchState;
use crate::stats::StatsState;
use crate::tooltip::{self, TooltipState};

/// Which pane has focus
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
    InputField,
    ResultsPane,
}

/// What to output when exiting
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
    Results, // Output filtered JSON results (Enter)
    Query,   // Output query string only (Shift+Enter)
}

/// Application state
pub struct App {
    pub input: InputState,
    pub query: QueryState,
    pub focus: Focus,
    pub results_scroll: ScrollState,
    pub output_mode: Option<OutputMode>,
    pub should_quit: bool,
    pub autocomplete: AutocompleteState,
    pub error_overlay_visible: bool,
    pub history: HistoryState,
    pub help: HelpPopupState,
    pub notification: NotificationState,
    pub clipboard_backend: ClipboardBackend,
    pub tooltip: TooltipState,
    pub stats: StatsState,
    pub debouncer: Debouncer,
    pub search: SearchState,
}

impl App {
    /// Create a new App instance with JSON input and configuration
    ///
    /// # Arguments
    /// * `json_input` - The JSON data to explore
    /// * `config` - Application configuration
    pub fn new(json_input: String, config: &Config) -> Self {
        Self {
            input: InputState::new(),
            query: QueryState::new(json_input),
            focus: Focus::InputField,
            results_scroll: ScrollState::new(),
            output_mode: None,
            should_quit: false,
            autocomplete: AutocompleteState::new(),
            error_overlay_visible: false,
            history: HistoryState::new(),
            help: HelpPopupState::new(),
            notification: NotificationState::new(),
            clipboard_backend: config.clipboard.backend,
            tooltip: TooltipState::new(config.tooltip.auto_show),
            stats: StatsState::default(),
            debouncer: Debouncer::new(),
            search: SearchState::new(),
        }
    }

    /// Check if the application should quit
    pub fn should_quit(&self) -> bool {
        self.should_quit
    }

    /// Get the output mode (if set)
    pub fn output_mode(&self) -> Option<OutputMode> {
        self.output_mode
    }

    /// Get the current query text
    pub fn query(&self) -> &str {
        self.input.query()
    }

    /// Get the total number of lines in the current results
    /// Note: Returns u32 to handle large files (>65K lines) correctly
    /// When there's an error, uses last_successful_result since that's what gets rendered
    pub fn results_line_count_u32(&self) -> u32 {
        self.query.line_count()
    }


    /// Update autocomplete suggestions based on current query and cursor position
    /// Delegates to the autocomplete module for the actual logic
    pub fn update_autocomplete(&mut self) {
        // Clone values to avoid borrow conflicts
        let query = self.input.query().to_string();
        let cursor_pos = self.input.textarea.cursor().1; // Column position
        let result = self.query.last_successful_result_unformatted.clone();
        let result_type = self.query.base_type_for_suggestions.clone();

        autocomplete::update_suggestions(
            &mut self.autocomplete,
            &query,
            cursor_pos,
            result.as_deref(),
            result_type,
        );
    }

    /// Update tooltip state based on current cursor position
    /// Detects if cursor is on or inside a jq function or operator and updates tooltip accordingly
    /// Functions take priority over operators when both could apply
    pub fn update_tooltip(&mut self) {
        let query = self.input.query();
        let cursor_pos = self.input.textarea.cursor().1; // Column position
        
        // Detect function (takes priority)
        let detected_function = tooltip::detect_function_at_cursor(query, cursor_pos);
        self.tooltip.set_current_function(detected_function.map(|s| s.to_string()));
        
        // Detect operator only if no function detected (function takes priority)
        let detected_operator = if detected_function.is_none() {
            tooltip::detect_operator_at_cursor(query, cursor_pos)
        } else {
            None
        };
        self.tooltip.set_current_operator(detected_operator.map(|s| s.to_string()));
    }

    /// Update stats based on the last successful result
    /// Uses the unformatted result (no ANSI codes) for accurate parsing
    pub fn update_stats(&mut self) {
        if let Some(result) = &self.query.last_successful_result_unformatted {
            self.stats.compute(result);
        }
    }

    /// Insert an autocomplete suggestion at the current cursor position
    /// Delegates to the autocomplete insertion module
    pub fn insert_autocomplete_suggestion(&mut self, suggestion: &autocomplete::autocomplete_state::Suggestion) {
        // Delegate to autocomplete module
        insertion::insert_suggestion(&mut self.input.textarea, &mut self.query, suggestion);

        // Hide autocomplete and reset scroll/error state
        self.autocomplete.hide();
        self.results_scroll.reset();
        self.error_overlay_visible = false; // Auto-hide error overlay on query change
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::query::ResultType;
    use crate::autocomplete::autocomplete_state::{Suggestion, SuggestionType};

    /// Helper to create App with default config for tests
    fn test_app(json: &str) -> App {
        App::new(json.to_string(), &Config::default())
    }

    /// Helper to create a test suggestion from text (for backward compatibility with existing tests)
    fn test_suggestion(text: &str) -> Suggestion {
        Suggestion::new(text, SuggestionType::Field)
    }

    #[test]
    fn test_app_initialization() {
        let json = r#"{"name": "Alice", "age": 30}"#;
        let app = test_app(json);

        // Check default state
        assert_eq!(app.focus, Focus::InputField);
        assert_eq!(app.results_scroll.offset, 0);
        assert_eq!(app.output_mode, None);
        assert!(!app.should_quit);
        assert_eq!(app.query(), "");
    }

    #[test]
    fn test_initial_query_result() {
        let json = r#"{"name": "Bob"}"#;
        let app = test_app(json);

        // Initial query should execute identity filter "."
        assert!(app.query.result.is_ok());
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("Bob"));
    }

    #[test]
    fn test_focus_enum() {
        assert_eq!(Focus::InputField, Focus::InputField);
        assert_eq!(Focus::ResultsPane, Focus::ResultsPane);
        assert_ne!(Focus::InputField, Focus::ResultsPane);
    }

    #[test]
    fn test_output_mode_enum() {
        assert_eq!(OutputMode::Results, OutputMode::Results);
        assert_eq!(OutputMode::Query, OutputMode::Query);
        assert_ne!(OutputMode::Results, OutputMode::Query);
    }

    #[test]
    fn test_should_quit_getter() {
        let json = r#"{}"#;
        let mut app = test_app(json);

        assert!(!app.should_quit());

        app.should_quit = true;
        assert!(app.should_quit());
    }

    #[test]
    fn test_output_mode_getter() {
        let json = r#"{}"#;
        let mut app = test_app(json);

        assert_eq!(app.output_mode(), None);

        app.output_mode = Some(OutputMode::Results);
        assert_eq!(app.output_mode(), Some(OutputMode::Results));

        app.output_mode = Some(OutputMode::Query);
        assert_eq!(app.output_mode(), Some(OutputMode::Query));
    }

    #[test]
    fn test_query_getter_empty() {
        let json = r#"{"test": true}"#;
        let app = test_app(json);

        assert_eq!(app.query(), "");
    }

    #[test]
    fn test_app_with_empty_json_object() {
        let json = "{}";
        let app = test_app(json);

        assert!(app.query.result.is_ok());
    }

    #[test]
    fn test_app_with_json_array() {
        let json = r#"[1, 2, 3]"#;
        let app = test_app(json);

        assert!(app.query.result.is_ok());
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("1"));
        assert!(result.contains("2"));
        assert!(result.contains("3"));
    }

    // Tests for large file handling (>65K lines)
    #[test]
    fn test_max_scroll_large_content() {
        let json = r#"{"test": true}"#;
        let mut app = test_app(json);

        // Simulate large content result
        let large_result: String = (0..70000).map(|i| format!("line {}\n", i)).collect();
        app.query.result = Ok(large_result);

        // Should handle >65K lines without overflow
        let line_count = app.results_line_count_u32();
        assert!(line_count > 65535);

        // Update scroll bounds
        app.results_scroll.update_bounds(line_count, 20);

        // max_offset should be clamped to u16::MAX
        assert_eq!(app.results_scroll.max_offset, u16::MAX);
    }

    #[test]
    fn test_results_line_count_large_file() {
        let json = r#"{"test": true}"#;
        let mut app = test_app(json);

        // Simulate result with exactly u16::MAX lines
        let result: String = (0..65535).map(|_| "x\n").collect();
        app.query.result = Ok(result);

        // Verify line count is correct (using internal method)
        assert_eq!(app.results_line_count_u32(), 65535);

        // Update scroll bounds
        app.results_scroll.update_bounds(65535, 10);

        // Verify max_offset handles it correctly
        assert_eq!(app.results_scroll.max_offset, 65525); // 65535 - 10
    }

    #[test]
    fn test_line_count_uses_last_result_on_error() {
        let json = r#"{"test": true}"#;
        let mut app = test_app(json);

        // Execute a valid query first to cache result
        let valid_result: String = (0..50).map(|i| format!("line{}\n", i)).collect();
        app.query.result = Ok(valid_result.clone());
        app.query.last_successful_result = Some(valid_result);

        // Verify line count with valid result
        assert_eq!(app.results_line_count_u32(), 50);

        // Now simulate an error (short error message)
        app.query.result = Err("syntax error\nline 2\nline 3".to_string());

        // Line count should use last_successful_result (50 lines), not error (3 lines)
        assert_eq!(app.results_line_count_u32(), 50);

        // Update scroll bounds and verify max_offset is calculated correctly
        app.results_scroll.update_bounds(50, 10);
        assert_eq!(app.results_scroll.max_offset, 40); // 50 - 10 = 40
    }

    #[test]
    fn test_line_count_with_error_no_cached_result() {
        let json = r#"{"test": true}"#;
        let mut app = test_app(json);

        // Set error without any cached result
        app.query.last_successful_result = None;
        app.query.result = Err("error message".to_string());

        // Should return 0 when no cached result available
        assert_eq!(app.results_line_count_u32(), 0);

        // Update scroll bounds
        app.results_scroll.update_bounds(0, 10);
        assert_eq!(app.results_scroll.max_offset, 0);
    }

    #[test]
    fn test_array_suggestion_appends_to_path() {
        // When accepting [].field suggestion for .services, should produce .services[].field
        let json = r#"{"services": [{"name": "alice"}, {"name": "bob"}, {"name": "charlie"}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute ".services" to cache base
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        // Validate cached state after ".services"
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()),
                   "base_query should be '.services'");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects),
                   "base_type should be ArrayOfObjects");

        // Step 2: Accept autocomplete suggestion "[].name" (no leading dot since after NoOp)
        app.insert_autocomplete_suggestion(&test_suggestion("[].name"));

        // Should produce .services[].name (append, not replace)
        assert_eq!(app.query(), ".services[].name");

        // CRITICAL: Verify the query EXECUTES correctly and returns ALL array elements
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("alice"), "Should contain first element");
        assert!(result.contains("bob"), "Should contain second element");
        assert!(result.contains("charlie"), "Should contain third element");

        // Verify it does NOT just return nulls or single value
        let line_count = result.lines().count();
        assert!(line_count >= 3, "Should return at least 3 lines for 3 array elements");
    }

    #[test]
    fn test_simple_path_continuation_with_dot() {
        // Test simple path continuation: .object.field
        // This is the bug: .services[0].deploymentConfiguration.alarms becomes deploymentConfigurationalarms
        let json = r#"{"user": {"name": "Alice", "age": 30, "address": {"city": "NYC"}}}"#;
        let mut app = test_app(json);

        // Step 1: Execute base query
        app.input.textarea.insert_str(".user");
        app.query.execute(".user");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".user".to_string()));
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::Object));

        // Step 2: Type ".na" (partial field access)
        app.input.textarea.insert_str(".na");

        // Step 3: Accept suggestion "name" (no leading dot since continuing path)
        app.insert_autocomplete_suggestion(&test_suggestion("name"));

        // Should produce: .user.name
        // NOT: .username (missing dot)
        assert_eq!(app.query(), ".user.name");
        
        // Verify execution
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("Alice"));
    }

    #[test]
    fn test_array_suggestion_replaces_partial_field() {
        // When user types partial field after array name, accepting [] suggestion should replace partial
        let json = r#"{"services": [{"serviceArn": "arn1"}, {"serviceArn": "arn2"}, {"serviceArn": "arn3"}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute ".services" to cache base
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()));
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects));

        // Step 2: Type ".s" (partial)
        app.input.textarea.insert_char('.');
        app.input.textarea.insert_char('s');

        // Step 3: Accept autocomplete suggestion "[].serviceArn"
        app.insert_autocomplete_suggestion(&test_suggestion("[].serviceArn"));

        // Should produce .services[].serviceArn (replace ".s" with "[].serviceArn")
        assert_eq!(app.query(), ".services[].serviceArn");

        // CRITICAL: Verify execution returns ALL serviceArns
        let result = app.query.result.as_ref().unwrap();
        eprintln!("Query result:\n{}", result);

        assert!(result.contains("arn1"), "Should contain first serviceArn");
        assert!(result.contains("arn2"), "Should contain second serviceArn");
        assert!(result.contains("arn3"), "Should contain third serviceArn");

        // Should NOT have nulls (would indicate query failed to iterate array)
        let null_count = result.matches("null").count();
        assert_eq!(null_count, 0, "Should not have any null values - query should iterate all array elements");
    }

    #[test]
    fn test_array_suggestion_replaces_trailing_dot() {
        // When user types ".services." (trailing dot, no partial), array suggestion should replace the dot
        let json = r#"{"services": [{"deploymentConfiguration": {"x": 1}}, {"deploymentConfiguration": {"x": 2}}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute ".services" to cache base query and type
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()),
                   "base_query should be '.services'");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects),
                   "base_type should be ArrayOfObjects");

        // Step 2: Type a dot (syntax error, doesn't update base)
        app.input.textarea.insert_char('.');

        // Step 3: Accept autocomplete suggestion "[].deploymentConfiguration"
        app.insert_autocomplete_suggestion(&test_suggestion("[].deploymentConfiguration"));

        // Should produce .services[].deploymentConfiguration (NOT .services.[].deploymentConfiguration)
        assert_eq!(app.query(), ".services[].deploymentConfiguration");

        // Verify the query executes correctly
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("x"));
        assert!(result.contains("1"));
        assert!(result.contains("2"));
    }

    #[test]
    fn test_nested_array_suggestion_replaces_trailing_dot() {
        // Test deeply nested arrays: .services[].capacityProviderStrategy[].
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0, "weight": 1}]}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute base query to cache state
        app.input.textarea.insert_str(".services[].capacityProviderStrategy[]");
        app.query.execute(".services[].capacityProviderStrategy[]");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".services[].capacityProviderStrategy[]".to_string()));
        // With only 1 service, this returns a single object, not destructured
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::Object));

        // Step 2: Type trailing dot
        app.input.textarea.insert_char('.');

        // Step 3: Accept autocomplete suggestion "base"
        // Note: suggestion is "base" (no prefix) since Object after CloseBracket
        app.insert_autocomplete_suggestion(&test_suggestion("base"));

        // Should produce .services[].capacityProviderStrategy[].base
        assert_eq!(app.query(), ".services[].capacityProviderStrategy[].base");

        // Verify the query executes and returns the base values
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("0"));
    }

    #[test]
    fn test_array_suggestion_after_pipe() {
        // After pipe, array suggestions should include leading dot
        let json = r#"{"services": [{"name": "svc1"}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute base query
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()));
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects));

        // Step 2: Type " | ."
        app.input.textarea.insert_str(" | .");

        // Step 3: Accept autocomplete suggestion ".[].name" (WITH leading dot after pipe)
        app.insert_autocomplete_suggestion(&test_suggestion(".[].name"));

        // Should produce .services | .[].name (NOT .services | . | .[].name)
        assert_eq!(app.query(), ".services | .[].name");

        // Verify execution
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("svc1"));
    }

    #[test]
    fn test_array_suggestion_after_pipe_exact_user_flow() {
        // Replicate exact user flow: type partial, select, then pipe
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0}]}]}"#;
        let mut app = test_app(json);

        // Step 1: Type ".ser" (partial)
        app.input.textarea.insert_str(".ser");
        // Note: .ser returns null, base stays at "."

        // Step 2: Select ".services" from autocomplete
        // In real app, user would Tab here with suggestion ".services"
        app.insert_autocomplete_suggestion(&test_suggestion(".services"));

        // Validate: should now be ".services"
        assert_eq!(app.query(), ".services");

        // Step 3: Verify base is now cached after successful execution
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()),
                   "base should be '.services' after insertion executed it");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects));

        // Step 4: Type " | ."
        app.input.textarea.insert_str(" | .");

        // Step 5: Select ".[].capacityProviderStrategy"
        app.insert_autocomplete_suggestion(&test_suggestion(".[].capacityProviderStrategy"));

        // Should produce: .services | .[].capacityProviderStrategy
        // NOT: .services | . | .[].capacityProviderStrategy
        assert_eq!(app.query(), ".services | .[].capacityProviderStrategy");
    }

    #[test]
    fn test_pipe_after_typing_space() {
        // Test typing space then pipe character by character
        let json = r#"{"services": [{"name": "svc1"}]}"#;
        let mut app = test_app(json);

        // Step 1: Type and execute ".services"
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()));

        // Step 2: Type space (executes ".services ")
        app.input.textarea.insert_char(' ');
        app.query.execute(".services ");

        // Step 3: Type | (executes ".services |" - syntax error, base stays at ".services")
        app.input.textarea.insert_char('|');
        app.query.execute(".services |");

        // Step 4: Type space then dot
        app.input.textarea.insert_str(" .");

        // Step 5: Accept suggestion
        app.insert_autocomplete_suggestion(&test_suggestion(".[].name"));

        // Should be: base + " | " + suggestion
        // Base is trimmed, so: ".services" + " | " + ".[].name" = ".services | .[].name" ✅
        assert_eq!(app.query(), ".services | .[].name");
    }

    #[test]
    fn test_suggestions_persist_when_typing_partial_after_array() {
        // Critical: When typing partial field after [], suggestions should persist
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0, "weight": 1, "capacityProvider": "x"}]}]}"#;
        let mut app = test_app(json);

        // Step 1: Type the full path up to the last array
        app.input.textarea.insert_str(".services[].capacityProviderStrategy[]");
        app.query.execute(".services[].capacityProviderStrategy[]");
        app.update_autocomplete();

        // Cache should have the array element objects with fields: base, weight, capacityProvider
        assert!(app.query.last_successful_result_unformatted.is_some());
        let cached = app.query.last_successful_result_unformatted.clone();

        // Step 2: Type a dot - should still have cached result
        app.input.textarea.insert_char('.');
        // Query is now ".services[].capacityProviderStrategy[]." which is syntax error
        app.query.execute(".services[].capacityProviderStrategy[].");

        // Cache should NOT be cleared (syntax error doesn't update cache)
        assert_eq!(app.query.last_successful_result_unformatted, cached);

        // Step 3: Type a partial "b" - query returns multiple nulls
        app.input.textarea.insert_char('b');
        // Query is now ".services[].capacityProviderStrategy[].b" which returns multiple nulls
        app.query.execute(".services[].capacityProviderStrategy[].b");

        // CRITICAL: Cache should STILL not be cleared (multiple nulls shouldn't overwrite)
        assert_eq!(app.query.last_successful_result_unformatted, cached);

        // Step 4: Update autocomplete - should still show suggestions based on cached result
        app.update_autocomplete();

        // Should have suggestions for the cached object fields
        let suggestions = app.autocomplete.suggestions();
        assert!(!suggestions.is_empty(), "Suggestions should persist when typing partial that returns null");

        // Should have "base" suggestion (filtered by partial "b")
        assert!(suggestions.iter().any(|s| s.text.contains("base")),
                "Should suggest 'base' field when filtering by 'b'");
    }

    #[test]
    fn test_suggestions_persist_with_optional_chaining_and_partial() {
        // Critical: When typing partial after []?, suggestions should persist
        // Realistic scenario: some services have capacityProviderStrategy, some don't
        let json = r#"{
            "services": [
                {
                    "serviceName": "service1",
                    "capacityProviderStrategy": [
                        {"base": 0, "weight": 1, "capacityProvider": "FARGATE"},
                        {"base": 0, "weight": 2, "capacityProvider": "FARGATE_SPOT"}
                    ]
                },
                {
                    "serviceName": "service2"
                },
                {
                    "serviceName": "service3",
                    "capacityProviderStrategy": [
                        {"base": 1, "weight": 3, "capacityProvider": "EC2"}
                    ]
                }
            ]
        }"#;
        let mut app = test_app(json);

        // Step 1: Execute query with optional chaining up to the array
        app.input.textarea.insert_str(".services[].capacityProviderStrategy[]?");
        app.query.execute(".services[].capacityProviderStrategy[]?");

        // This should return the object with base, weight, capacityProvider fields
        let cached_before_partial = app.query.last_successful_result_unformatted.clone();
        assert!(cached_before_partial.is_some());
        assert!(cached_before_partial.as_ref().unwrap().contains("base"));

        // Step 2: Type a dot
        app.input.textarea.insert_char('.');
        app.query.execute(".services[].capacityProviderStrategy[]?.");
        // Syntax error - cache should remain
        assert_eq!(app.query.last_successful_result_unformatted, cached_before_partial);

        // Step 3: Type partial "b"
        app.input.textarea.insert_char('b');
        app.query.execute(".services[].capacityProviderStrategy[]?.b");

        // This returns single "null" (not multiple) due to optional chaining
        // Cache should NOT be updated
        assert_eq!(app.query.last_successful_result_unformatted, cached_before_partial,
                   "Cache should not be overwritten by null result from partial field");

        // Step 4: Update autocomplete
        app.update_autocomplete();

        // Should have suggestions based on the cached object
        let suggestions = app.autocomplete.suggestions();
        assert!(!suggestions.is_empty(), "Suggestions should persist when typing partial after []?");

        // Should suggest "base" (filtered by partial "b")
        assert!(suggestions.iter().any(|s| s.text.contains("base")),
                "Should suggest 'base' field when filtering by 'b' after []?");
    }

    #[test]
    fn test_jq_keyword_autocomplete_no_dot_prefix() {
        // Test that jq keywords like "then", "else", "end" don't get a dot prefix
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0}]}]}"#;
        let mut app = test_app(json);

        // Step 1: Type the beginning of an if statement
        app.input.textarea.insert_str(".services | if has(\"capacityProviderStrategy\")");
        app.query.execute(".services | if has(\"capacityProviderStrategy\")");

        // Step 2: Type partial "the" to trigger autocomplete for "then"
        app.input.textarea.insert_str(" the");

        // Step 3: Accept "then" from autocomplete
        // This should NOT add a dot before "then"
        app.insert_autocomplete_suggestion(&test_suggestion("then"));

        // Should produce: .services | if has("capacityProviderStrategy") then
        // NOT: .services | if has("capacityProviderStrategy") .then
        assert_eq!(app.query(), ".services | if has(\"capacityProviderStrategy\") then");
        
        // Verify no extra dot was added
        assert!(!app.query().contains(" .then"), "Should not have dot before 'then' keyword");
    }

    #[test]
    fn test_jq_keyword_else_autocomplete() {
        // Test "else" keyword autocomplete
        let json = r#"{"value": 42}"#;
        let mut app = test_app(json);

        // Type an if-then statement
        app.input.textarea.insert_str("if .value > 10 then \"high\" el");
        
        // Accept "else" from autocomplete
        app.insert_autocomplete_suggestion(&test_suggestion("else"));

        // Should produce: if .value > 10 then "high" else
        // NOT: if .value > 10 then "high" .else
        assert_eq!(app.query(), "if .value > 10 then \"high\" else");
        assert!(!app.query().contains(".else"), "Should not have dot before 'else' keyword");
    }

    #[test]
    fn test_jq_keyword_end_autocomplete() {
        // Test "end" keyword autocomplete
        let json = r#"{"value": 42}"#;
        let mut app = test_app(json);

        // Type a complete if-then-else statement
        app.input.textarea.insert_str("if .value > 10 then \"high\" else \"low\" en");
        
        // Accept "end" from autocomplete
        app.insert_autocomplete_suggestion(&test_suggestion("end"));

        // Should produce: if .value > 10 then "high" else "low" end
        // NOT: if .value > 10 then "high" else "low" .end
        assert_eq!(app.query(), "if .value > 10 then \"high\" else \"low\" end");
        assert!(!app.query().contains(".end"), "Should not have dot before 'end' keyword");
    }

    #[test]
    fn test_field_access_after_jq_keyword_preserves_space() {
        // Test that field access after "then" preserves the space
        // Bug: ".services[] | if has(\"x\") then .field" becomes "then.field" (no space)
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0}]}]}"#;
        let mut app = test_app(json);

        // Step 1: Execute base query
        app.input.textarea.insert_str(".services[]");
        app.query.execute(".services[]");

        // Step 2: Type if-then with field access
        app.input.textarea.insert_str(" | if has(\"capacityProviderStrategy\") then .ca");

        // Step 3: Accept field suggestion (with leading dot as it would come from get_suggestions)
        app.insert_autocomplete_suggestion(&test_suggestion(".capacityProviderStrategy"));

        // Should produce: .services[] | if has("capacityProviderStrategy") then .capacityProviderStrategy
        // NOT: .services[] | if has("capacityProviderStrategy") thencapacityProviderStrategy
        assert_eq!(
            app.query(),
            ".services[] | if has(\"capacityProviderStrategy\") then .capacityProviderStrategy"
        );
        
        // Verify there's a space before the field name
        assert!(app.query().contains("then .capacityProviderStrategy"), 
                "Should have space between 'then' and field name");
        assert!(!app.query().contains("thencapacityProviderStrategy"), 
                "Should NOT concatenate 'then' with field name");
    }

    #[test]
    fn test_field_access_after_else_preserves_space() {
        // Test that field access after "else" preserves the space
        let json = r#"{"services": [{"name": "test"}]}"#;
        let mut app = test_app(json);

        // Execute base query
        app.input.textarea.insert_str(".services[]");
        app.query.execute(".services[]");

        // Type if-then-else with field access
        app.input.textarea.insert_str(" | if has(\"name\") then .name else .na");

        // Accept field suggestion (with leading dot as it would come from get_suggestions)
        app.insert_autocomplete_suggestion(&test_suggestion(".name"));

        // Should have space between "else" and field
        assert!(app.query().contains("else .name"), 
                "Should have space between 'else' and field name");
        assert!(!app.query().contains("elsename"), 
                "Should NOT concatenate 'else' with field name");
    }

    #[test]
    fn test_autocomplete_inside_if_statement() {
        // Autocomplete inside complex query should only replace the local part
        let json = r#"{"services": [{"capacityProviderStrategy": [{"base": 0}]}]}"#;
        let mut app = test_app(json);

        // User types complex query with if/then
        app.input.textarea.insert_str(".services | if has(\"capacityProviderStrategy\") then .ca");

        // Execute to cache state (this will likely error due to incomplete query)
        app.query.execute(".services | if has(\"capacityProviderStrategy\") then .ca");

        // The issue: when Tab is pressed, entire query gets replaced with base + suggestion
        // Expected: only ".ca" should be replaced
        // Actual: entire query replaced with ".services[].capacityProviderStrategy"

        // TODO: This test documents the bug - we need smarter insertion
        // For now, this is a known limitation when using autocomplete inside complex expressions
    }

    #[test]
    fn test_root_field_suggestion() {
        // At root, typing "." and selecting field should replace "." with ".field"
        let json = r#"{"services": [{"name": "test"}], "status": "active"}"#;
        let mut app = test_app(json);

        // Validate initial state
        assert_eq!(app.query.base_query_for_suggestions, Some(".".to_string()),
                   "base_query should be '.' initially");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::Object),
                   "base_type should be Object");

        // User types "."
        app.input.textarea.insert_str(".");

        // Accept suggestion ".services" (with leading dot since at root after NoOp)
        app.insert_autocomplete_suggestion(&test_suggestion(".services"));

        // Should produce ".services" NOT "..services"
        assert_eq!(app.query(), ".services");

        // Verify query executes correctly
        let result = app.query.result.as_ref().unwrap();
        assert!(result.contains("name"));
    }

    #[test]
    fn test_field_suggestion_replaces_from_dot() {
        // When accepting .field suggestion at root, should replace from last dot
        let json = r#"{"name": "test", "age": 30}"#;
        let mut app = test_app(json);

        // Initial state: "." was executed during App::new()
        // Validate initial state
        assert_eq!(app.query.base_query_for_suggestions, Some(".".to_string()),
                   "base_query should be '.' initially");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::Object),
                   "base_type should be Object for root");

        // Simulate: user typed ".na" and cursor is at end
        app.input.textarea.insert_str(".na");

        // Accept autocomplete suggestion "name" (no leading dot since after Dot)
        app.insert_autocomplete_suggestion(&test_suggestion("name"));

        // Should produce .name (replace from the dot)
        assert_eq!(app.query(), ".name");
    }

    #[test]
    fn test_autocomplete_with_real_ecs_like_data() {
        // Test with data structure similar to AWS ECS services
        let json = r#"{
            "services": [
                {"serviceArn": "arn:aws:ecs:region:account:service/cluster/svc1", "serviceName": "service1"},
                {"serviceArn": "arn:aws:ecs:region:account:service/cluster/svc2", "serviceName": "service2"},
                {"serviceArn": "arn:aws:ecs:region:account:service/cluster/svc3", "serviceName": "service3"},
                {"serviceArn": "arn:aws:ecs:region:account:service/cluster/svc4", "serviceName": "service4"},
                {"serviceArn": "arn:aws:ecs:region:account:service/cluster/svc5", "serviceName": "service5"}
            ]
        }"#;
        let mut app = test_app(json);

        // Step 1: Execute ".services" to cache base
        app.input.textarea.insert_str(".services");
        app.query.execute(".services");

        // Validate cached state
        assert_eq!(app.query.base_query_for_suggestions, Some(".services".to_string()),
                   "base_query should be '.services'");
        assert_eq!(app.query.base_type_for_suggestions, Some(ResultType::ArrayOfObjects),
                   "base_type should be ArrayOfObjects");

        // Step 2: Type ".s" (partial)
        app.input.textarea.insert_str(".s");

        // Step 3: Accept "[].serviceArn" (no leading dot since after NoOp)
        app.insert_autocomplete_suggestion(&test_suggestion("[].serviceArn"));

        let query_text = app.query();
        assert_eq!(query_text, ".services[].serviceArn");

        // Verify execution returns ALL 5 serviceArns
        let result = app.query.result.as_ref().unwrap();

        // Check for all service ARNs
        assert!(result.contains("svc1"));
        assert!(result.contains("svc2"));
        assert!(result.contains("svc3"));
        assert!(result.contains("svc4"));
        assert!(result.contains("svc5"));

        // Count non-null values
        let lines: Vec<&str> = result.lines().collect();
        let non_null_lines: Vec<&str> = lines.iter()
            .filter(|line| !line.trim().contains("null"))
            .copied()
            .collect();

        assert!(non_null_lines.len() >= 5, "Should have at least 5 non-null results, got {}", non_null_lines.len());
    }

    // ========== Tooltip Integration Tests ==========

    #[test]
    fn test_tooltip_initialized_enabled() {
        let json = r#"{"name": "test"}"#;
        let app = test_app(json);
        
        // Tooltip should be enabled by default
        assert!(app.tooltip.enabled);
        assert!(app.tooltip.current_function.is_none());
    }

    #[test]
    fn test_update_tooltip_detects_function() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with a function
        app.input.textarea.insert_str("select(.name)");
        
        // Move cursor to be on "select"
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward); // Position 1
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_function, Some("select".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_inside_function_parens() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with a function
        app.input.textarea.insert_str("select(.name)");
        
        // Move cursor inside the parentheses (on ".name")
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        for _ in 0..8 { // Position 8 is inside parens
            app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
        }
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_function, Some("select".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_no_function() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query without a function
        app.input.textarea.insert_str(".name");
        
        app.update_tooltip();
        
        assert!(app.tooltip.current_function.is_none());
        assert!(!app.tooltip.should_show());
    }

    #[test]
    fn test_tooltip_disabled_does_not_show() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with a function
        app.input.textarea.insert_str("select(.name)");
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        
        // Disable tooltip
        app.tooltip.toggle();
        assert!(!app.tooltip.enabled);
        
        app.update_tooltip();
        
        // Function should be detected but should_show returns false
        assert_eq!(app.tooltip.current_function, Some("select".to_string()));
        assert!(!app.tooltip.should_show());
    }

    // ========== Operator Tooltip Integration Tests ==========

    #[test]
    fn test_update_tooltip_detects_operator_double_slash() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with the // operator
        app.input.textarea.insert_str(".name // \"default\"");
        
        // Move cursor to be on the first /
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        for _ in 0..6 { // Position 6 is on first /
            app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
        }
        
        app.update_tooltip();
        
        assert!(app.tooltip.current_function.is_none());
        assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_detects_operator_pipe_equals() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with the |= operator
        app.input.textarea.insert_str(".name |= ascii_upcase");
        
        // Move cursor to be on the |
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        for _ in 0..6 { // Position 6 is on |
            app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
        }
        
        app.update_tooltip();
        
        assert!(app.tooltip.current_function.is_none());
        assert_eq!(app.tooltip.current_operator, Some("|=".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_detects_operator_alternative_assignment() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with the //= operator
        app.input.textarea.insert_str(".count //= 0");
        
        // Move cursor to be on the first /
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        for _ in 0..7 { // Position 7 is on first /
            app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
        }
        
        app.update_tooltip();
        
        assert!(app.tooltip.current_function.is_none());
        assert_eq!(app.tooltip.current_operator, Some("//=".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_detects_operator_recursive_descent() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with the .. operator
        app.input.textarea.insert_str(".. | numbers");
        
        // Move cursor to be on the first .
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        
        app.update_tooltip();
        
        assert!(app.tooltip.current_function.is_none());
        assert_eq!(app.tooltip.current_operator, Some("..".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_no_operator_on_single_pipe() {
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type a query with a single pipe (not |=)
        app.input.textarea.insert_str(".name | length");
        
        // Move cursor to be on the |
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
        for _ in 0..6 { // Position 6 is on |
            app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
        }
        
        app.update_tooltip();
        
        // Single pipe should NOT trigger operator tooltip
        assert!(app.tooltip.current_function.is_none());
        assert!(app.tooltip.current_operator.is_none());
        assert!(!app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_cursor_after_operator_detects_it() {
        // When user types "//", cursor ends up at position 2 (after the operator)
        // The tooltip should still show because we detect operators immediately before cursor
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type just "//" - cursor will be at position 2 (after the operator)
        app.input.textarea.insert_str("//");
        
        // Cursor is now at position 2 (after the //)
        let cursor_pos = app.input.textarea.cursor().1;
        assert_eq!(cursor_pos, 2, "Cursor should be at position 2 after typing //");
        
        app.update_tooltip();
        
        // Cursor AFTER operator should now detect it (extended behavior)
        assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_cursor_on_operator_detects_it() {
        // When cursor is ON the operator (e.g., user moved cursor back), it should detect
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        // Type "//" then move cursor back onto it
        app.input.textarea.insert_str("//");
        app.input.textarea.move_cursor(tui_textarea::CursorMove::Back); // Now on second /
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_cursor_after_pipe_equals() {
        // Test |= operator detection when cursor is after it
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        app.input.textarea.insert_str("|=");
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_operator, Some("|=".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_cursor_after_alternative_assignment() {
        // Test //= operator detection when cursor is after it
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        app.input.textarea.insert_str("//=");
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_operator, Some("//=".to_string()));
        assert!(app.tooltip.should_show());
    }

    #[test]
    fn test_update_tooltip_cursor_after_double_dot() {
        // Test .. operator detection when cursor is after it
        let json = r#"{"name": "test"}"#;
        let mut app = test_app(json);
        
        app.input.textarea.insert_str("..");
        
        app.update_tooltip();
        
        assert_eq!(app.tooltip.current_operator, Some("..".to_string()));
        assert!(app.tooltip.should_show());
    }

    // **Feature: function-tooltip, Property 1: Tooltip visibility follows cursor on functions**
    // *For any* query string containing jq functions and any cursor position, when tooltip is enabled:
    // - If cursor is inside a function's parentheses, `should_show()` returns true and `current_function` matches the innermost enclosing function
    // - If cursor is on a recognized function name, `should_show()` returns true and `current_function` matches that function
    // - If cursor is not within any function context, `should_show()` returns false
    // **Validates: Requirements 1.1, 1.2, 1.3**
    mod property_tests {
        use super::*;
        use proptest::prelude::*;
        use crate::autocomplete::jq_functions::JQ_FUNCTION_METADATA;

        proptest! {
            #![proptest_config(ProptestConfig::with_cases(100))]

            #[test]
            fn prop_tooltip_visibility_on_function_name(
                func_index in 0usize..JQ_FUNCTION_METADATA.len(),
                prefix in "[.| ]{0,3}",
                suffix in "[()| ]{0,5}"
            ) {
                let func = &JQ_FUNCTION_METADATA[func_index];
                let func_name = func.name;
                
                let query = format!("{}{}{}", prefix, func_name, suffix);
                let func_start = prefix.len();
                
                let json = r#"{"test": true}"#;
                let mut app = test_app(json);
                
                // Insert the query
                app.input.textarea.insert_str(&query);
                
                // Move cursor to be on the function name
                app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
                for _ in 0..func_start {
                    app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
                }
                
                // Update tooltip
                app.update_tooltip();
                
                // When cursor is on a function name and tooltip is enabled, should_show should be true
                prop_assert!(
                    app.tooltip.should_show(),
                    "Tooltip should show when cursor is on function '{}' in query '{}'",
                    func_name,
                    query
                );
                prop_assert_eq!(
                    app.tooltip.current_function.as_deref(),
                    Some(func_name),
                    "Current function should be '{}' when cursor is on it",
                    func_name
                );
            }

            #[test]
            fn prop_tooltip_visibility_inside_function_parens(
                func_index in 0usize..JQ_FUNCTION_METADATA.len(),
                inner_content in "[.a-z0-9]{1,8}"
            ) {
                let func = &JQ_FUNCTION_METADATA[func_index];
                
                // Only test functions that take arguments
                if !func.needs_parens {
                    return Ok(());
                }
                
                let func_name = func.name;
                let query = format!("{}({})", func_name, inner_content);
                let content_start = func_name.len() + 1; // Position after opening paren
                
                let json = r#"{"test": true}"#;
                let mut app = test_app(json);
                
                // Insert the query
                app.input.textarea.insert_str(&query);
                
                // Move cursor inside the parentheses
                app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
                for _ in 0..content_start {
                    app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
                }
                
                // Update tooltip
                app.update_tooltip();
                
                // When cursor is inside function parens and tooltip is enabled, should_show should be true
                prop_assert!(
                    app.tooltip.should_show(),
                    "Tooltip should show when cursor is inside '{}' parens in query '{}'",
                    func_name,
                    query
                );
                prop_assert_eq!(
                    app.tooltip.current_function.as_deref(),
                    Some(func_name),
                    "Current function should be '{}' when cursor is inside its parens",
                    func_name
                );
            }

            #[test]
            fn prop_tooltip_not_visible_outside_function_context(
                field_name in "[a-z]{1,8}"
            ) {
                // Skip field names that happen to also be jq function names
                // (e.g., "env", "add", "map", "min", "max", "not", "type", etc.)
                if JQ_FUNCTION_METADATA.iter().any(|f| f.name == field_name) {
                    return Ok(());
                }

                // Query with just field access, no function
                let query = format!(".{}", field_name);

                let json = r#"{"test": true}"#;
                let mut app = test_app(json);

                // Insert the query
                app.input.textarea.insert_str(&query);

                // Cursor is at end of query (after field name)
                app.update_tooltip();

                // When cursor is not in any function context, should_show should be false
                prop_assert!(
                    !app.tooltip.should_show(),
                    "Tooltip should not show when cursor is outside function context in query '{}'",
                    query
                );
                prop_assert!(
                    app.tooltip.current_function.is_none(),
                    "Current function should be None when cursor is outside function context"
                );
            }

            #[test]
            fn prop_tooltip_disabled_never_shows(
                func_index in 0usize..JQ_FUNCTION_METADATA.len()
            ) {
                let func = &JQ_FUNCTION_METADATA[func_index];
                let func_name = func.name;
                let query = format!("{}(.x)", func_name);
                
                let json = r#"{"test": true}"#;
                let mut app = test_app(json);
                
                // Insert the query
                app.input.textarea.insert_str(&query);
                app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
                
                // Disable tooltip
                app.tooltip.toggle();
                
                // Update tooltip
                app.update_tooltip();
                
                // When tooltip is disabled, should_show should always be false
                prop_assert!(
                    !app.tooltip.should_show(),
                    "Tooltip should not show when disabled, even on function '{}'",
                    func_name
                );
                // But current_function should still be detected
                prop_assert_eq!(
                    app.tooltip.current_function.as_deref(),
                    Some(func_name),
                    "Current function should still be detected when disabled"
                );
            }

            // **Feature: operator-tooltips, Property 4: Function priority over operator**
            // *For any* query where cursor position could match both a function context
            // (inside function parens) and an operator, the system SHALL show the function
            // tooltip, not the operator tooltip.
            // **Validates: Requirements 5.3**
            #[test]
            fn prop_function_priority_over_operator(
                func_index in 0usize..JQ_FUNCTION_METADATA.len(),
                op_index in 0usize..4usize,
                left_operand in "[.a-z]{1,5}",
                right_operand in "[a-z0-9\"]{1,5}"
            ) {
                let func = &JQ_FUNCTION_METADATA[func_index];
                
                // Only test functions that take arguments (have parens)
                if !func.needs_parens {
                    return Ok(());
                }
                
                let func_name = func.name;
                let operators = ["//", "|=", "//=", ".."];
                let op = operators[op_index];
                
                // Build query like "select(. // x)" or "map(.x |= . + 1)"
                let query = format!("{}({} {} {})", func_name, left_operand, op, right_operand);
                
                // Calculate position of operator inside the parens
                let op_start = func_name.len() + 1 + left_operand.len() + 1; // func( + left + space
                
                let json = r#"{"test": true}"#;
                let mut app = test_app(json);
                
                // Insert the query
                app.input.textarea.insert_str(&query);
                
                // Move cursor to be on the operator
                app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
                for _ in 0..op_start {
                    app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
                }
                
                // Update tooltip
                app.update_tooltip();
                
                // Function should take priority - current_function should be set
                prop_assert_eq!(
                    app.tooltip.current_function.as_deref(),
                    Some(func_name),
                    "Function '{}' should be detected (priority over operator '{}') in query '{}'",
                    func_name,
                    op,
                    query
                );
                
                // Operator should NOT be detected when inside function parens
                prop_assert!(
                    app.tooltip.current_operator.is_none(),
                    "Operator '{}' should NOT be detected when inside function '{}' parens in query '{}'",
                    op,
                    func_name,
                    query
                );
                
                // Tooltip should show (function detected)
                prop_assert!(
                    app.tooltip.should_show(),
                    "Tooltip should show for function '{}' in query '{}'",
                    func_name,
                    query
                );
            }
        }
    }
}
