//! Fixture resolution and query methods.
//!
//! This module contains methods for finding fixture definitions,
//! references, and providing completion context.

use super::decorators;
use super::types::{
    CompletionContext, FixtureDefinition, FixtureScope, FixtureUsage, ParamInsertionInfo,
    UndeclaredFixture,
};
use super::FixtureDatabase;
use rustpython_parser::ast::{Arguments, Expr, Ranged, Stmt};
use std::collections::HashSet;
use std::path::Path;
use tracing::{debug, info};

impl FixtureDatabase {
    /// Find fixture definition for a given position in a file
    pub fn find_fixture_definition(
        &self,
        file_path: &Path,
        line: u32,
        character: u32,
    ) -> Option<FixtureDefinition> {
        debug!(
            "find_fixture_definition: file={:?}, line={}, char={}",
            file_path, line, character
        );

        let target_line = (line + 1) as usize; // Convert from 0-based to 1-based

        let content = self.get_file_content(file_path)?;
        let line_content = content.lines().nth(target_line.saturating_sub(1))?;
        debug!("Line content: {}", line_content);

        let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
        debug!("Word at cursor: {:?}", word_at_cursor);

        // Check if we're inside a fixture definition with the same name (self-referencing)
        let current_fixture_def = self.get_fixture_definition_at_line(file_path, target_line);

        // First, check if this word matches any fixture usage on this line
        if let Some(usages) = self.usages.get(file_path) {
            for usage in usages.iter() {
                if usage.line == target_line && usage.name == word_at_cursor {
                    let cursor_pos = character as usize;
                    if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
                        debug!(
                            "Cursor at {} is within usage range {}-{}: {}",
                            cursor_pos, usage.start_char, usage.end_char, usage.name
                        );
                        info!("Found fixture usage at cursor position: {}", usage.name);

                        // If we're in a fixture definition with the same name, skip it
                        if let Some(ref current_def) = current_fixture_def {
                            if current_def.name == word_at_cursor {
                                info!(
                                    "Self-referencing fixture detected, finding parent definition"
                                );
                                return self.find_closest_definition_excluding(
                                    file_path,
                                    &usage.name,
                                    Some(current_def),
                                );
                            }
                        }

                        return self.find_closest_definition(file_path, &usage.name);
                    }
                }
            }
        }

        debug!("Word at cursor '{}' is not a fixture usage", word_at_cursor);
        None
    }

    /// Get the fixture definition at a specific line (if the line is a fixture definition)
    fn get_fixture_definition_at_line(
        &self,
        file_path: &Path,
        line: usize,
    ) -> Option<FixtureDefinition> {
        for entry in self.definitions.iter() {
            for def in entry.value().iter() {
                if def.file_path == file_path && def.line == line {
                    return Some(def.clone());
                }
            }
        }
        None
    }

    /// Find fixture definition at a given position, checking both usages and definitions.
    ///
    /// This is useful for Call Hierarchy where we want to work on both fixture definition
    /// lines and fixture usage sites.
    pub fn find_fixture_or_definition_at_position(
        &self,
        file_path: &Path,
        line: u32,
        character: u32,
    ) -> Option<FixtureDefinition> {
        // First try to find a usage and resolve it to definition
        if let Some(def) = self.find_fixture_definition(file_path, line, character) {
            return Some(def);
        }

        // If not a usage, check if we're on a fixture definition line
        let target_line = (line + 1) as usize; // Convert from 0-based to 1-based
        let content = self.get_file_content(file_path)?;
        let line_content = content.lines().nth(target_line.saturating_sub(1))?;
        let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;

        // Check if this word matches a fixture definition at this line
        if let Some(definitions) = self.definitions.get(&word_at_cursor) {
            for def in definitions.iter() {
                if def.file_path == file_path && def.line == target_line {
                    // Verify cursor is within the fixture name
                    if character as usize >= def.start_char && (character as usize) < def.end_char {
                        return Some(def.clone());
                    }
                }
            }
        }

        None
    }

    /// Public method to get the fixture definition at a specific line and name
    pub fn get_definition_at_line(
        &self,
        file_path: &Path,
        line: usize,
        fixture_name: &str,
    ) -> Option<FixtureDefinition> {
        if let Some(definitions) = self.definitions.get(fixture_name) {
            for def in definitions.iter() {
                if def.file_path == file_path && def.line == line {
                    return Some(def.clone());
                }
            }
        }
        None
    }

    /// Find the closest fixture definition based on pytest priority rules.
    pub(crate) fn find_closest_definition(
        &self,
        file_path: &Path,
        fixture_name: &str,
    ) -> Option<FixtureDefinition> {
        self.find_closest_definition_with_filter(file_path, fixture_name, |_| true)
    }

    /// Find the closest definition, excluding a specific definition.
    pub(crate) fn find_closest_definition_excluding(
        &self,
        file_path: &Path,
        fixture_name: &str,
        exclude: Option<&FixtureDefinition>,
    ) -> Option<FixtureDefinition> {
        self.find_closest_definition_with_filter(file_path, fixture_name, |def| {
            if let Some(excluded) = exclude {
                def != excluded
            } else {
                true
            }
        })
    }

    /// Internal helper that implements pytest priority rules with a custom filter.
    /// Priority order:
    /// 1. Same file (highest priority, last definition wins)
    /// 2. Closest conftest.py in parent directories (including imported fixtures)
    /// 3. Third-party fixtures from site-packages
    fn find_closest_definition_with_filter<F>(
        &self,
        file_path: &Path,
        fixture_name: &str,
        filter: F,
    ) -> Option<FixtureDefinition>
    where
        F: Fn(&FixtureDefinition) -> bool,
    {
        let definitions = self.definitions.get(fixture_name)?;

        // Priority 1: Same file (highest priority)
        debug!(
            "Checking for fixture {} in same file: {:?}",
            fixture_name, file_path
        );

        if let Some(last_def) = definitions
            .iter()
            .filter(|def| def.file_path == file_path && filter(def))
            .max_by_key(|def| def.line)
        {
            info!(
                "Found fixture {} in same file at line {}",
                fixture_name, last_def.line
            );
            return Some(last_def.clone());
        }

        // Priority 2: Search upward through conftest.py files
        let mut current_dir = file_path.parent()?;

        debug!(
            "Searching for fixture {} in conftest.py files starting from {:?}",
            fixture_name, current_dir
        );
        loop {
            let conftest_path = current_dir.join("conftest.py");
            debug!("  Checking conftest.py at: {:?}", conftest_path);

            // First check if the fixture is defined directly in this conftest
            for def in definitions.iter() {
                if def.file_path == conftest_path && filter(def) {
                    info!(
                        "Found fixture {} in conftest.py: {:?}",
                        fixture_name, conftest_path
                    );
                    return Some(def.clone());
                }
            }

            // Then check if the conftest imports this fixture
            // Check both filesystem and file cache for conftest existence
            let conftest_in_cache = self.file_cache.contains_key(&conftest_path);
            if (conftest_path.exists() || conftest_in_cache)
                && self.is_fixture_imported_in_file(fixture_name, &conftest_path)
            {
                // The fixture is imported in this conftest, so it's available here
                // Return the original definition (pytest makes it available at conftest scope)
                debug!(
                    "Fixture {} is imported in conftest.py: {:?}",
                    fixture_name, conftest_path
                );
                // Get any matching definition that passes the filter
                if let Some(def) = definitions.iter().find(|def| filter(def)) {
                    info!(
                        "Found imported fixture {} via conftest.py: {:?} (original: {:?})",
                        fixture_name, conftest_path, def.file_path
                    );
                    return Some(def.clone());
                }
            }

            match current_dir.parent() {
                Some(parent) => current_dir = parent,
                None => break,
            }
        }

        // Priority 3: Plugin fixtures (discovered via pytest11 entry points)
        // These are globally available like third-party fixtures, but from workspace-local
        // editable installs that aren't in site-packages or conftest.py.
        debug!(
            "No fixture {} found in conftest hierarchy, checking plugins",
            fixture_name
        );
        for def in definitions.iter() {
            if def.is_plugin && !def.is_third_party && filter(def) {
                info!(
                    "Found plugin fixture {} via pytest11 entry point: {:?}",
                    fixture_name, def.file_path
                );
                return Some(def.clone());
            }
        }

        // Priority 4: Third-party fixtures (site-packages)
        debug!(
            "No fixture {} found in plugins, checking third-party",
            fixture_name
        );
        for def in definitions.iter() {
            if def.is_third_party && filter(def) {
                info!(
                    "Found third-party fixture {} in site-packages: {:?}",
                    fixture_name, def.file_path
                );
                return Some(def.clone());
            }
        }

        debug!(
            "No fixture {} found in scope for {:?}",
            fixture_name, file_path
        );
        None
    }

    /// Find the fixture name at a given position (either definition or usage)
    pub fn find_fixture_at_position(
        &self,
        file_path: &Path,
        line: u32,
        character: u32,
    ) -> Option<String> {
        let target_line = (line + 1) as usize;

        debug!(
            "find_fixture_at_position: file={:?}, line={}, char={}",
            file_path, target_line, character
        );

        let content = self.get_file_content(file_path)?;
        let line_content = content.lines().nth(target_line.saturating_sub(1))?;
        debug!("Line content: {}", line_content);

        let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
        debug!("Word at cursor: {:?}", word_at_cursor);

        // Check if this word matches any fixture usage on this line
        if let Some(usages) = self.usages.get(file_path) {
            for usage in usages.iter() {
                if usage.line == target_line {
                    let cursor_pos = character as usize;
                    if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
                        debug!(
                            "Cursor at {} is within usage range {}-{}: {}",
                            cursor_pos, usage.start_char, usage.end_char, usage.name
                        );
                        info!("Found fixture usage at cursor position: {}", usage.name);
                        return Some(usage.name.clone());
                    }
                }
            }
        }

        // Check if we're on a fixture definition line
        for entry in self.definitions.iter() {
            for def in entry.value().iter() {
                if def.file_path == file_path && def.line == target_line {
                    if let Some(ref word) = word_at_cursor {
                        if word == &def.name {
                            info!(
                                "Found fixture definition name at cursor position: {}",
                                def.name
                            );
                            return Some(def.name.clone());
                        }
                    }
                }
            }
        }

        debug!("No fixture found at cursor position");
        None
    }

    /// Extract the word at a given character position in a line
    pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
        super::string_utils::extract_word_at_position(line, character)
    }

    /// Find all references (usages) of a fixture by name
    pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
        info!("Finding all references for fixture: {}", fixture_name);

        let mut all_references = Vec::new();

        for entry in self.usages.iter() {
            let file_path = entry.key();
            let usages = entry.value();

            for usage in usages.iter() {
                if usage.name == fixture_name {
                    debug!(
                        "Found reference to {} in {:?} at line {}",
                        fixture_name, file_path, usage.line
                    );
                    all_references.push(usage.clone());
                }
            }
        }

        info!(
            "Found {} total references for fixture: {}",
            all_references.len(),
            fixture_name
        );
        all_references
    }

    /// Find all references that resolve to a specific fixture definition.
    /// Uses the usage_by_fixture reverse index for O(m) lookup where m = usages of this fixture,
    /// instead of O(n) iteration over all usages.
    pub fn find_references_for_definition(
        &self,
        definition: &FixtureDefinition,
    ) -> Vec<FixtureUsage> {
        info!(
            "Finding references for specific definition: {} at {:?}:{}",
            definition.name, definition.file_path, definition.line
        );

        let mut matching_references = Vec::new();

        // Use reverse index for O(m) lookup instead of O(n) iteration over all usages
        let Some(usages_for_fixture) = self.usage_by_fixture.get(&definition.name) else {
            info!("No references found for fixture: {}", definition.name);
            return matching_references;
        };

        for (file_path, usage) in usages_for_fixture.iter() {
            let fixture_def_at_line = self.get_fixture_definition_at_line(file_path, usage.line);

            let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
                if current_def.name == usage.name {
                    debug!(
                        "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
                        file_path, usage.line, current_def.line
                    );
                    self.find_closest_definition_excluding(
                        file_path,
                        &usage.name,
                        Some(current_def),
                    )
                } else {
                    self.find_closest_definition(file_path, &usage.name)
                }
            } else {
                self.find_closest_definition(file_path, &usage.name)
            };

            if let Some(resolved_def) = resolved_def {
                if resolved_def == *definition {
                    debug!(
                        "Usage at {:?}:{} resolves to our definition",
                        file_path, usage.line
                    );
                    matching_references.push(usage.clone());
                } else {
                    debug!(
                        "Usage at {:?}:{} resolves to different definition at {:?}:{}",
                        file_path, usage.line, resolved_def.file_path, resolved_def.line
                    );
                }
            }
        }

        info!(
            "Found {} references that resolve to this specific definition",
            matching_references.len()
        );
        matching_references
    }

    /// Get all undeclared fixture usages for a file
    pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
        self.undeclared_fixtures
            .get(file_path)
            .map(|entry| entry.value().clone())
            .unwrap_or_default()
    }

    /// Get all available fixtures for a given file.
    /// Results are cached with version-based invalidation for performance.
    /// Returns Arc to avoid cloning the potentially large Vec on cache hits.
    pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
        use std::sync::Arc;

        // Canonicalize path for consistent cache keys
        let file_path = self.get_canonical_path(file_path.to_path_buf());

        // Check cache first
        let current_version = self
            .definitions_version
            .load(std::sync::atomic::Ordering::SeqCst);

        if let Some(cached) = self.available_fixtures_cache.get(&file_path) {
            let (cached_version, cached_fixtures) = cached.value();
            if *cached_version == current_version {
                // Return cloned Vec from Arc (cheap reference count increment)
                return cached_fixtures.as_ref().clone();
            }
        }

        // Compute available fixtures
        let available_fixtures = self.compute_available_fixtures(&file_path);

        // Store in cache
        self.available_fixtures_cache.insert(
            file_path,
            (current_version, Arc::new(available_fixtures.clone())),
        );

        available_fixtures
    }

    /// Internal method to compute available fixtures without caching.
    fn compute_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
        let mut available_fixtures = Vec::new();
        let mut seen_names = HashSet::new();

        // Priority 1: Fixtures in the same file
        for entry in self.definitions.iter() {
            let fixture_name = entry.key();
            for def in entry.value().iter() {
                if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
                    available_fixtures.push(def.clone());
                    seen_names.insert(fixture_name.clone());
                }
            }
        }

        // Priority 2: Fixtures in conftest.py files (including imported fixtures)
        if let Some(mut current_dir) = file_path.parent() {
            loop {
                let conftest_path = current_dir.join("conftest.py");

                // First add fixtures defined directly in the conftest
                for entry in self.definitions.iter() {
                    let fixture_name = entry.key();
                    for def in entry.value().iter() {
                        if def.file_path == conftest_path
                            && !seen_names.contains(fixture_name.as_str())
                        {
                            available_fixtures.push(def.clone());
                            seen_names.insert(fixture_name.clone());
                        }
                    }
                }

                // Then add fixtures imported into the conftest
                if self.file_cache.contains_key(&conftest_path) {
                    let mut visited = HashSet::new();
                    let imported_fixtures =
                        self.get_imported_fixtures(&conftest_path, &mut visited);
                    for fixture_name in imported_fixtures {
                        if !seen_names.contains(&fixture_name) {
                            // Get the original definition for this imported fixture
                            if let Some(definitions) = self.definitions.get(&fixture_name) {
                                if let Some(def) = definitions.first() {
                                    available_fixtures.push(def.clone());
                                    seen_names.insert(fixture_name);
                                }
                            }
                        }
                    }
                }

                match current_dir.parent() {
                    Some(parent) => current_dir = parent,
                    None => break,
                }
            }
        }

        // Priority 3: Plugin fixtures (pytest11 entry points, e.g. workspace editable installs)
        for entry in self.definitions.iter() {
            let fixture_name = entry.key();
            for def in entry.value().iter() {
                if def.is_plugin
                    && !def.is_third_party
                    && !seen_names.contains(fixture_name.as_str())
                {
                    available_fixtures.push(def.clone());
                    seen_names.insert(fixture_name.clone());
                }
            }
        }

        // Priority 4: Third-party fixtures from site-packages
        for entry in self.definitions.iter() {
            let fixture_name = entry.key();
            for def in entry.value().iter() {
                if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
                    available_fixtures.push(def.clone());
                    seen_names.insert(fixture_name.clone());
                }
            }
        }

        available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
        available_fixtures
    }

    /// Get the completion context for a given position
    pub fn get_completion_context(
        &self,
        file_path: &Path,
        line: u32,
        character: u32,
    ) -> Option<CompletionContext> {
        let content = self.get_file_content(file_path)?;
        let target_line = (line + 1) as usize;

        // Try AST-based analysis first
        let parsed = self.get_parsed_ast(file_path, &content);

        if let Some(parsed) = parsed {
            let line_index = self.get_line_index(file_path, &content);

            if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
                // First check if we're inside a decorator
                if let Some(ctx) =
                    self.check_decorator_context(&module.body, &content, target_line, &line_index)
                {
                    return Some(ctx);
                }

                // Then check for function context
                if let Some(ctx) = self.get_function_completion_context(
                    &module.body,
                    &content,
                    target_line,
                    character as usize,
                    &line_index,
                ) {
                    return Some(ctx);
                }
            }
        }

        // Fallback: text-based analysis for incomplete/invalid Python
        self.get_completion_context_from_text(&content, target_line)
    }

    /// Check whether a `@pytest.fixture` decorator appears in the lines immediately
    /// above `def_line_idx` (0-based index into `lines`).
    ///
    /// Scans upward through decorator lines (lines starting with `@` after stripping
    /// whitespace) and blank lines, stopping at the first non-decorator, non-blank line.
    fn has_fixture_decorator_above(lines: &[&str], def_line_idx: usize) -> bool {
        if def_line_idx == 0 {
            return false;
        }
        let mut i = def_line_idx - 1;
        loop {
            let trimmed = lines[i].trim();
            if trimmed.is_empty() {
                // Skip blank lines between decorators and def
                if i == 0 {
                    break;
                }
                i -= 1;
                continue;
            }
            if trimmed.starts_with('@') {
                // Check for @pytest.fixture or @fixture (with optional parens/args)
                if trimmed.contains("pytest.fixture") || trimmed.starts_with("@fixture") {
                    return true;
                }
                // Another decorator — keep scanning upward
                if i == 0 {
                    break;
                }
                i -= 1;
                continue;
            }
            // Hit a non-decorator, non-blank line — stop
            break;
        }
        false
    }

    /// Extract the fixture scope from decorator text above a function definition.
    ///
    /// Scans decorator lines above `def_line_idx` for `@pytest.fixture(scope="...")`.
    /// Searches each decorator line individually to avoid quadratic string building.
    /// Returns `None` if no scope keyword is found (caller should default to `Function`).
    fn extract_fixture_scope_from_text(
        lines: &[&str],
        def_line_idx: usize,
    ) -> Option<FixtureScope> {
        if def_line_idx == 0 {
            return None;
        }

        // Scan decorator lines above the def and search each one for scope=
        let mut i = def_line_idx - 1;
        loop {
            let trimmed = lines[i].trim();
            if trimmed.is_empty() {
                if i == 0 {
                    break;
                }
                i -= 1;
                continue;
            }
            if trimmed.starts_with('@') {
                // Check this decorator line for scope="..." or scope='...'
                for pattern in &["scope=\"", "scope='"] {
                    if let Some(pos) = trimmed.find(pattern) {
                        let start = pos + pattern.len();
                        let quote_char = if pattern.ends_with('"') { '"' } else { '\'' };
                        if let Some(end) = trimmed[start..].find(quote_char) {
                            let scope_str = &trimmed[start..start + end];
                            return FixtureScope::parse(scope_str);
                        }
                    }
                }
                if i == 0 {
                    break;
                }
                i -= 1;
                continue;
            }
            break;
        }

        None
    }

    /// Text-based fallback for detecting usefixtures decorator and pytestmark contexts.
    ///
    /// Handles cases like:
    ///   `@pytest.mark.usefixtures(`
    ///   `pytestmark = [pytest.mark.usefixtures(`
    ///
    /// Returns `Some(UsefixturesDecorator)` if the cursor appears to be inside
    /// an unclosed `pytest.mark.usefixtures(` call.
    fn get_usefixtures_context_from_text(
        lines: &[&str],
        cursor_idx: usize,
    ) -> Option<CompletionContext> {
        // Scan backward from cursor line looking for usefixtures pattern
        let scan_limit = cursor_idx.saturating_sub(10);

        let mut i = cursor_idx;
        loop {
            let line = lines[i];
            if let Some(pos) = line.find("usefixtures(") {
                // Found the pattern — check if cursor is inside the unclosed call
                // Count parens from the usefixtures( position to the cursor
                let mut depth: i32 = 0;

                // Count from the opening paren on this line
                for ch in line[pos..].chars() {
                    if ch == '(' {
                        depth += 1;
                    }
                    if ch == ')' {
                        depth -= 1;
                    }
                }

                // Continue counting on subsequent lines up to cursor.
                // Skip when i == cursor_idx since (i + 1)..=cursor_idx would panic.
                if i < cursor_idx {
                    for line in &lines[(i + 1)..=cursor_idx] {
                        for ch in line.chars() {
                            if ch == '(' {
                                depth += 1;
                            }
                            if ch == ')' {
                                depth -= 1;
                            }
                        }
                    }
                }

                // If depth > 0, we're inside the unclosed usefixtures call
                if depth > 0 {
                    return Some(CompletionContext::UsefixturesDecorator);
                }

                // depth == 0 and cursor is on the same line as the opening —
                // Only offer completions if cursor is positioned between the parens,
                // not after a fully closed usefixtures() call.
                if i == cursor_idx && depth == 0 {
                    // Find the closing paren position; if cursor is before it,
                    // we're still inside the call.
                    if let Some(close_pos) = line[pos..].rfind(')') {
                        let abs_close = pos + close_pos;
                        // Cursor column is approximated by line length at this point;
                        // but since we don't have the cursor column here, we check
                        // whether the opening and closing paren are adjacent (empty call)
                        // — in that case the user likely wants completions inside "()".
                        let open_pos = pos + line[pos..].find('(').unwrap_or(0);
                        if abs_close == open_pos + 1 {
                            // Empty parens like usefixtures() — offer completions
                            return Some(CompletionContext::UsefixturesDecorator);
                        }
                        // Parens are balanced with content — user may be done
                        return None;
                    }
                    // No closing paren found on this line — unclosed call
                    return Some(CompletionContext::UsefixturesDecorator);
                }
            }

            if i == 0 || i <= scan_limit {
                break;
            }
            i -= 1;
        }

        None
    }

    /// Text-based fallback for completion context when the AST parser fails.
    ///
    /// Checks for two kinds of contexts:
    /// 1. Usefixtures/pytestmark decorator contexts (checked first, like the AST path)
    /// 2. Function signature contexts (def/async def lines)
    fn get_completion_context_from_text(
        &self,
        content: &str,
        target_line: usize,
    ) -> Option<CompletionContext> {
        let mut lines: Vec<&str> = content.lines().collect();

        // Feature #2: handle trailing newline — str::lines() omits the trailing
        // empty line when content ends with '\n'
        if content.ends_with('\n') {
            lines.push("");
        }

        if target_line == 0 || target_line > lines.len() {
            return None;
        }

        let cursor_idx = target_line - 1; // 0-based

        // Check usefixtures/pytestmark context first (mirrors AST path priority)
        if let Some(ctx) = Self::get_usefixtures_context_from_text(&lines, cursor_idx) {
            return Some(ctx);
        }

        // Scan backward for def/async def.
        // Known limitation: only scans up to 50 lines backward. If the cursor is
        // deep inside a very long incomplete function body (>50 lines), the text
        // fallback won't find the enclosing `def`. This is acceptable because the
        // AST-based path handles complete functions of any length; this fallback
        // only runs for syntactically invalid (incomplete) Python.
        let mut def_line_idx = None;
        let scan_limit = cursor_idx.saturating_sub(50);

        let mut i = cursor_idx;
        loop {
            let trimmed = lines[i].trim();
            if trimmed.starts_with("def ") || trimmed.starts_with("async def ") {
                def_line_idx = Some(i);
                break;
            }
            if i == 0 || i <= scan_limit {
                break;
            }
            i -= 1;
        }

        let def_line_idx = def_line_idx?;
        let def_line = lines[def_line_idx].trim();

        // Extract function name
        let name_start = if def_line.starts_with("async def ") {
            "async def ".len()
        } else {
            "def ".len()
        };
        let remaining = &def_line[name_start..];
        let func_name: String = remaining
            .chars()
            .take_while(|c| c.is_alphanumeric() || *c == '_')
            .collect();

        if func_name.is_empty() {
            return None;
        }

        // Determine is_test / is_fixture
        let is_test = func_name.starts_with("test_");
        let is_fixture = Self::has_fixture_decorator_above(&lines, def_line_idx);

        // No completions for regular functions
        if !is_test && !is_fixture {
            return None;
        }

        // Check parenthesis state from def line to cursor, tracking whether
        // the cursor is inside the parentheses (between '(' and ')').
        // We need to determine if the cursor is within the signature parens,
        // not just whether both parens exist in the scanned range.
        let mut paren_depth: i32 = 0;
        let mut cursor_inside_parens = false;
        let mut found_open = false;
        let mut signature_closed = false; // True when ')' found AND cursor is after it

        for (line_idx_offset, line) in lines[def_line_idx..=cursor_idx].iter().enumerate() {
            let current_line_idx = def_line_idx + line_idx_offset;
            let is_cursor_line = current_line_idx == cursor_idx;

            for ch in line.chars() {
                if ch == '(' {
                    paren_depth += 1;
                    if paren_depth == 1 {
                        found_open = true;
                    }
                } else if ch == ')' {
                    paren_depth -= 1;
                    if paren_depth == 0 && found_open {
                        // Closing paren of the signature found
                        if !is_cursor_line {
                            // Cursor is on a line after the closing paren
                            signature_closed = true;
                        }
                        // If on cursor line, cursor might be before or after ')'
                        // but since this is a text fallback for broken syntax,
                        // we treat cursor on the same line as still in-signature
                    }
                }
            }

            // After processing the cursor line, check if we're inside parens
            if is_cursor_line && found_open && paren_depth > 0 {
                cursor_inside_parens = true;
            }
        }

        // Reject only if the signature is fully closed AND the cursor is on a
        // subsequent line (i.e. in the body area). Since this fallback only runs
        // when the AST parse failed (incomplete/invalid Python), having both
        // parens on the def line does NOT mean the function is complete — there
        // may be no body yet (e.g. "def test_bla():").
        //
        // When the cursor is on the same line as the def (between or after the
        // parens), we still offer completions because the user is likely still
        // editing the signature of an incomplete function.
        if signature_closed && !cursor_inside_parens {
            return None;
        }

        // If both parens are present on the def line but cursor is on that same
        // line or inside parens, we still want to provide completions.
        // Also handle the case where the cursor is on the def line after '):'
        // — this is still useful since the function has no body.

        // Extract existing parameters
        let mut declared_params = Vec::new();
        if found_open {
            let mut param_text = String::new();
            let mut past_open = false;
            let mut past_close = false;
            for line in &lines[def_line_idx..=cursor_idx] {
                for ch in line.chars() {
                    if past_close {
                        // Stop collecting after closing paren
                        continue;
                    } else if past_open {
                        if ch == ')' {
                            past_close = true;
                        } else {
                            param_text.push(ch);
                        }
                    } else if ch == '(' {
                        past_open = true;
                    }
                }
                if past_open && !past_close {
                    param_text.push(' ');
                }
            }
            for param in param_text.split(',') {
                let name: String = param
                    .trim()
                    .chars()
                    .take_while(|c| c.is_alphanumeric() || *c == '_')
                    .collect();
                if !name.is_empty() {
                    declared_params.push(name);
                }
            }
        }

        // Determine fixture scope
        let fixture_scope = if is_fixture {
            let scope = Self::extract_fixture_scope_from_text(&lines, def_line_idx)
                .unwrap_or(FixtureScope::Function);
            Some(scope)
        } else {
            None
        };

        Some(CompletionContext::FunctionSignature {
            function_name: func_name,
            function_line: def_line_idx + 1, // 1-based
            is_fixture,
            declared_params,
            fixture_scope,
        })
    }

    /// Check if the cursor is inside a decorator that needs fixture completions,
    /// or inside a pytestmark assignment's usefixtures call.
    fn check_decorator_context(
        &self,
        stmts: &[Stmt],
        _content: &str,
        target_line: usize,
        line_index: &[usize],
    ) -> Option<CompletionContext> {
        for stmt in stmts {
            // Check decorators on functions and classes
            let decorator_list = match stmt {
                Stmt::FunctionDef(f) => Some(f.decorator_list.as_slice()),
                Stmt::AsyncFunctionDef(f) => Some(f.decorator_list.as_slice()),
                Stmt::ClassDef(c) => Some(c.decorator_list.as_slice()),
                _ => None,
            };

            if let Some(decorator_list) = decorator_list {
                for decorator in decorator_list {
                    let dec_start_line =
                        self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
                    let dec_end_line =
                        self.get_line_from_offset(decorator.range().end().to_usize(), line_index);

                    if target_line >= dec_start_line && target_line <= dec_end_line {
                        if decorators::is_usefixtures_decorator(decorator) {
                            return Some(CompletionContext::UsefixturesDecorator);
                        }
                        if decorators::is_parametrize_decorator(decorator) {
                            return Some(CompletionContext::ParametrizeIndirect);
                        }
                    }
                }
            }

            // Check pytestmark = ... and pytestmark: T = ... assignments
            let pytestmark_value: Option<&Expr> = match stmt {
                Stmt::Assign(assign) => {
                    let is_pytestmark = assign
                        .targets
                        .iter()
                        .any(|t| matches!(t, Expr::Name(n) if n.id.as_str() == "pytestmark"));
                    if is_pytestmark {
                        Some(assign.value.as_ref())
                    } else {
                        None
                    }
                }
                Stmt::AnnAssign(ann_assign) => {
                    let is_pytestmark = matches!(
                        ann_assign.target.as_ref(),
                        Expr::Name(n) if n.id.as_str() == "pytestmark"
                    );
                    if is_pytestmark {
                        ann_assign.value.as_ref().map(|v| v.as_ref())
                    } else {
                        None
                    }
                }
                _ => None,
            };

            if let Some(value) = pytestmark_value {
                let stmt_start =
                    self.get_line_from_offset(stmt.range().start().to_usize(), line_index);
                let stmt_end = self.get_line_from_offset(stmt.range().end().to_usize(), line_index);

                if target_line >= stmt_start
                    && target_line <= stmt_end
                    && self.cursor_inside_usefixtures_call(value, target_line, line_index)
                {
                    return Some(CompletionContext::UsefixturesDecorator);
                }
            }

            // Recursively check class bodies
            if let Stmt::ClassDef(class_def) = stmt {
                if let Some(ctx) =
                    self.check_decorator_context(&class_def.body, _content, target_line, line_index)
                {
                    return Some(ctx);
                }
            }
        }

        None
    }

    /// Returns true if `target_line` falls within any `pytest.mark.usefixtures(...)` call
    /// anywhere inside `expr` (including nested in lists/tuples).
    fn cursor_inside_usefixtures_call(
        &self,
        expr: &Expr,
        target_line: usize,
        line_index: &[usize],
    ) -> bool {
        match expr {
            Expr::Call(call) => {
                if decorators::is_usefixtures_decorator(&call.func) {
                    let call_start =
                        self.get_line_from_offset(expr.range().start().to_usize(), line_index);
                    let call_end =
                        self.get_line_from_offset(expr.range().end().to_usize(), line_index);
                    return target_line >= call_start && target_line <= call_end;
                }
                false
            }
            Expr::List(list) => list
                .elts
                .iter()
                .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
            Expr::Tuple(tuple) => tuple
                .elts
                .iter()
                .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
            _ => false,
        }
    }

    /// Get completion context when cursor is inside a function
    fn get_function_completion_context(
        &self,
        stmts: &[Stmt],
        content: &str,
        target_line: usize,
        target_char: usize,
        line_index: &[usize],
    ) -> Option<CompletionContext> {
        for stmt in stmts {
            match stmt {
                Stmt::FunctionDef(func_def) => {
                    if let Some(ctx) = self.get_func_context(
                        &func_def.name,
                        &func_def.decorator_list,
                        &func_def.args,
                        &func_def.returns,
                        &func_def.body,
                        func_def.range,
                        content,
                        target_line,
                        target_char,
                        line_index,
                    ) {
                        return Some(ctx);
                    }
                }
                Stmt::AsyncFunctionDef(func_def) => {
                    if let Some(ctx) = self.get_func_context(
                        &func_def.name,
                        &func_def.decorator_list,
                        &func_def.args,
                        &func_def.returns,
                        &func_def.body,
                        func_def.range,
                        content,
                        target_line,
                        target_char,
                        line_index,
                    ) {
                        return Some(ctx);
                    }
                }
                Stmt::ClassDef(class_def) => {
                    if let Some(ctx) = self.get_function_completion_context(
                        &class_def.body,
                        content,
                        target_line,
                        target_char,
                        line_index,
                    ) {
                        return Some(ctx);
                    }
                }
                _ => {}
            }
        }

        None
    }

    /// Find the line where the function signature ends (the line containing the trailing `:`).
    ///
    /// Uses AST range information from arguments, return annotation, and body statements
    /// to locate the signature boundary. Falls back to scanning for `:` after the last
    /// known signature element.
    fn find_signature_end_line(
        &self,
        func_start_line: usize,
        args: &rustpython_parser::ast::Arguments,
        returns: &Option<Box<Expr>>,
        body: &[Stmt],
        content: &str,
        line_index: &[usize],
    ) -> usize {
        // Find the last AST element in the signature
        let mut last_sig_offset: Option<usize> = None;

        // Check return annotation
        if let Some(ret) = returns {
            last_sig_offset = Some(ret.range().end().to_usize());
        }

        // Check all argument categories for the one ending latest
        let all_arg_ends = args
            .args
            .iter()
            .chain(args.posonlyargs.iter())
            .chain(args.kwonlyargs.iter())
            .map(|a| a.def.range.end().to_usize())
            .chain(args.vararg.as_ref().map(|a| a.range.end().to_usize()))
            .chain(args.kwarg.as_ref().map(|a| a.range.end().to_usize()));

        if let Some(max_arg_end) = all_arg_ends.max() {
            last_sig_offset =
                Some(last_sig_offset.map_or(max_arg_end, |prev| prev.max(max_arg_end)));
        }

        // Convert to line number
        let last_sig_line = last_sig_offset
            .map(|offset| self.get_line_from_offset(offset, line_index))
            .unwrap_or(func_start_line);

        // Upper bound: line before first body statement
        let first_body_line = body
            .first()
            .map(|stmt| self.get_line_from_offset(stmt.range().start().to_usize(), line_index));

        // Scan forward from last_sig_line looking for trailing ":"
        let lines: Vec<&str> = content.lines().collect();
        let scan_end = first_body_line
            .unwrap_or(last_sig_line + 10)
            .min(last_sig_line + 10)
            .min(lines.len());
        let scan_start = last_sig_line.saturating_sub(1);

        for (i, line) in lines
            .iter()
            .enumerate()
            .skip(scan_start)
            .take(scan_end.saturating_sub(scan_start))
        {
            let trimmed = line.trim();
            if trimmed.ends_with(':') {
                return i + 1; // Convert to 1-based
            }
        }

        // Fallback: if body exists, signature ends on the line before the body
        if let Some(body_line) = first_body_line {
            return body_line.saturating_sub(1).max(func_start_line);
        }

        // Last resort: function start line
        func_start_line
    }

    /// Helper to get function completion context
    #[allow(clippy::too_many_arguments)]
    fn get_func_context(
        &self,
        func_name: &rustpython_parser::ast::Identifier,
        decorator_list: &[Expr],
        args: &rustpython_parser::ast::Arguments,
        returns: &Option<Box<Expr>>,
        body: &[Stmt],
        range: rustpython_parser::text_size::TextRange,
        content: &str,
        target_line: usize,
        _target_char: usize,
        line_index: &[usize],
    ) -> Option<CompletionContext> {
        let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
        let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);

        if target_line < func_start_line || target_line > func_end_line {
            return None;
        }

        let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
        let is_test = func_name.as_str().starts_with("test_");

        if !is_test && !is_fixture {
            return None;
        }

        // Determine fixture scope for scope-aware completion filtering
        let fixture_scope = if is_fixture {
            let scope = decorator_list
                .iter()
                .find_map(decorators::extract_fixture_scope)
                .unwrap_or(super::types::FixtureScope::Function);
            Some(scope)
        } else {
            None
        };

        // Collect all parameters
        let params: Vec<String> = FixtureDatabase::all_args(args)
            .map(|arg| arg.def.arg.to_string())
            .collect();

        // Find the line where the function signature ends using AST information
        let sig_end_line =
            self.find_signature_end_line(func_start_line, args, returns, body, content, line_index);

        let in_signature = target_line <= sig_end_line;

        let context = if in_signature {
            CompletionContext::FunctionSignature {
                function_name: func_name.to_string(),
                function_line: func_start_line,
                is_fixture,
                declared_params: params,
                fixture_scope,
            }
        } else {
            CompletionContext::FunctionBody {
                function_name: func_name.to_string(),
                function_line: func_start_line,
                is_fixture,
                declared_params: params,
                fixture_scope,
            }
        };

        Some(context)
    }

    /// Get information about where to insert a new parameter in a function signature.
    ///
    /// Uses the cached AST as the primary path: finds the function definition at
    /// `function_line`, determines `needs_comma` from the AST argument list, and
    /// locates the closing `)` via paren-depth scanning of the raw source bytes.
    /// This correctly handles multi-line signatures and return-type annotations
    /// (`-> T:`), which the old `"):"`  string-matching approach could not.
    ///
    /// Falls back to a pure byte-scan when the AST is unavailable (e.g. the file
    /// has syntax errors), using the same `scan_for_signature_close_paren` helper.
    pub fn get_function_param_insertion_info(
        &self,
        file_path: &Path,
        function_line: usize,
    ) -> Option<ParamInsertionInfo> {
        let content = self.get_file_content(file_path)?;
        let line_index = self.get_line_index(file_path, &content);
        let bytes = content.as_bytes();

        // ── AST path ─────────────────────────────────────────────────────────
        // Preferred: the AST gives accurate `needs_comma` (from the arg list)
        // and lets us scan from the exact `def` byte offset.
        if let Some(ast) = self.get_parsed_ast(file_path, &content) {
            if let rustpython_parser::ast::Mod::Module(module) = ast.as_ref() {
                if let Some(info) =
                    find_insertion_in_stmts(&module.body, function_line, bytes, &line_index)
                {
                    return Some(info);
                }
            }
        }

        // ── String fallback ───────────────────────────────────────────────────
        // Used when the AST is unavailable (syntax errors) or the function was
        // not found in the module-level AST walk (should be rare).
        let def_line_start = *line_index
            .get(function_line.saturating_sub(1))
            .unwrap_or(&0);
        let close_paren = scan_for_signature_close_paren(bytes, def_line_start)?;

        // Find the opening `(` to determine whether there are existing params.
        let open_paren = bytes[def_line_start..close_paren]
            .iter()
            .position(|&b| b == b'(')
            .map(|pos| def_line_start + pos)?;

        // has_params: any non-whitespace, non-comment content between `(` and `)`.
        let between = &bytes[open_paren + 1..close_paren];
        let has_params = {
            let mut in_comment = false;
            between.iter().any(|&b| {
                if b == b'\n' {
                    in_comment = false;
                    return false;
                }
                if in_comment {
                    return false;
                }
                if b == b'#' {
                    in_comment = true;
                    return false;
                }
                !b.is_ascii_whitespace()
            })
        };

        // Check whether `)` is on its own line (only whitespace before it on
        // that line).  If so, and there are existing params, use the multiline
        // insertion strategy: insert right after the last argument content
        // instead of at `)` itself.
        let close_paren_line = byte_offset_to_line_1based(close_paren, &line_index);
        if has_params {
            if let Some(ml) =
                try_multiline_insertion(close_paren, close_paren_line, bytes, &line_index)
            {
                return Some(ml);
            }
        }

        Some(ParamInsertionInfo {
            line: close_paren_line,
            char_pos: byte_offset_to_col(close_paren, &line_index),
            needs_comma: has_params,
            multiline_indent: None,
        })
    }

    /// Check if a position is inside a test or fixture function (parameter or body)
    /// Returns Some((function_name, is_fixture, declared_params)) if inside a function
    #[allow(dead_code)] // Used in tests
    pub fn is_inside_function(
        &self,
        file_path: &Path,
        line: u32,
        character: u32,
    ) -> Option<(String, bool, Vec<String>)> {
        // Try cache first, then file system
        let content = self.get_file_content(file_path)?;

        let target_line = (line + 1) as usize; // Convert to 1-based

        // Parse the file (using cached AST)
        let parsed = self.get_parsed_ast(file_path, &content)?;

        if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
            return self.find_enclosing_function(
                &module.body,
                &content,
                target_line,
                character as usize,
            );
        }

        None
    }

    #[allow(dead_code)]
    fn find_enclosing_function(
        &self,
        stmts: &[Stmt],
        content: &str,
        target_line: usize,
        _target_char: usize,
    ) -> Option<(String, bool, Vec<String>)> {
        let line_index = Self::build_line_index(content);

        for stmt in stmts {
            match stmt {
                Stmt::FunctionDef(func_def) => {
                    let func_start_line =
                        self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
                    let func_end_line =
                        self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);

                    // Check if target is within this function's range
                    if target_line >= func_start_line && target_line <= func_end_line {
                        let is_fixture = func_def
                            .decorator_list
                            .iter()
                            .any(decorators::is_fixture_decorator);
                        let is_test = func_def.name.starts_with("test_");

                        // Only return if it's a test or fixture
                        if is_test || is_fixture {
                            let params: Vec<String> = func_def
                                .args
                                .args
                                .iter()
                                .map(|arg| arg.def.arg.to_string())
                                .collect();

                            return Some((func_def.name.to_string(), is_fixture, params));
                        }
                    }
                }
                Stmt::AsyncFunctionDef(func_def) => {
                    let func_start_line =
                        self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
                    let func_end_line =
                        self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);

                    if target_line >= func_start_line && target_line <= func_end_line {
                        let is_fixture = func_def
                            .decorator_list
                            .iter()
                            .any(decorators::is_fixture_decorator);
                        let is_test = func_def.name.starts_with("test_");

                        if is_test || is_fixture {
                            let params: Vec<String> = func_def
                                .args
                                .args
                                .iter()
                                .map(|arg| arg.def.arg.to_string())
                                .collect();

                            return Some((func_def.name.to_string(), is_fixture, params));
                        }
                    }
                }
                _ => {}
            }
        }

        None
    }

    // ============ Cycle Detection ============

    /// Detect circular dependencies in fixtures with caching.
    /// Results are cached and only recomputed when definitions change.
    /// Returns Arc to avoid cloning the potentially large Vec.
    pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
        use std::sync::Arc;

        let current_version = self
            .definitions_version
            .load(std::sync::atomic::Ordering::SeqCst);

        // Check cache first
        if let Some(cached) = self.cycle_cache.get(&()) {
            let (cached_version, cached_cycles) = cached.value();
            if *cached_version == current_version {
                return Arc::clone(cached_cycles);
            }
        }

        // Compute cycles
        let cycles = Arc::new(self.compute_fixture_cycles());

        // Store in cache
        self.cycle_cache
            .insert((), (current_version, Arc::clone(&cycles)));

        cycles
    }

    /// Actually compute fixture cycles using iterative DFS (Tarjan-like approach).
    /// Uses iterative algorithm to avoid stack overflow on deep dependency graphs.
    fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
        use super::types::FixtureCycle;
        use std::collections::HashMap;

        // Build dependency graph: fixture_name -> dependencies (only known fixtures)
        let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
        let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();

        for entry in self.definitions.iter() {
            let fixture_name = entry.key().clone();
            if let Some(def) = entry.value().first() {
                fixture_defs.insert(fixture_name.clone(), def.clone());
                // Only include dependencies that are known fixtures
                let valid_deps: Vec<String> = def
                    .dependencies
                    .iter()
                    .filter(|d| self.definitions.contains_key(*d))
                    .cloned()
                    .collect();
                dep_graph.insert(fixture_name, valid_deps);
            }
        }

        let mut cycles = Vec::new();
        let mut visited: HashSet<String> = HashSet::new();
        let mut seen_cycles: HashSet<String> = HashSet::new(); // Deduplicate cycles

        // Iterative DFS using explicit stack
        for start_fixture in dep_graph.keys() {
            if visited.contains(start_fixture) {
                continue;
            }

            // Stack entries: (fixture_name, iterator_index, path_to_here)
            let mut stack: Vec<(String, usize, Vec<String>)> =
                vec![(start_fixture.clone(), 0, vec![])];
            let mut rec_stack: HashSet<String> = HashSet::new();

            while let Some((current, idx, mut path)) = stack.pop() {
                if idx == 0 {
                    // First time visiting this node
                    if rec_stack.contains(&current) {
                        // Found a cycle
                        let cycle_start_idx = path.iter().position(|f| f == &current).unwrap_or(0);
                        let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
                        cycle_path.push(current.clone());

                        // Create a canonical key for deduplication (sorted cycle representation)
                        let mut cycle_key: Vec<String> =
                            cycle_path[..cycle_path.len() - 1].to_vec();
                        cycle_key.sort();
                        let cycle_key_str = cycle_key.join(",");

                        if !seen_cycles.contains(&cycle_key_str) {
                            seen_cycles.insert(cycle_key_str);
                            if let Some(fixture_def) = fixture_defs.get(&current) {
                                cycles.push(FixtureCycle {
                                    cycle_path,
                                    fixture: fixture_def.clone(),
                                });
                            }
                        }
                        continue;
                    }

                    rec_stack.insert(current.clone());
                    path.push(current.clone());
                }

                // Get dependencies for current node
                let deps = match dep_graph.get(&current) {
                    Some(d) => d,
                    None => {
                        rec_stack.remove(&current);
                        continue;
                    }
                };

                if idx < deps.len() {
                    // Push current back with next index
                    stack.push((current.clone(), idx + 1, path.clone()));

                    let dep = &deps[idx];
                    if rec_stack.contains(dep) {
                        // Found a cycle through this dependency
                        let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
                        let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
                        cycle_path.push(dep.clone());

                        let mut cycle_key: Vec<String> =
                            cycle_path[..cycle_path.len() - 1].to_vec();
                        cycle_key.sort();
                        let cycle_key_str = cycle_key.join(",");

                        if !seen_cycles.contains(&cycle_key_str) {
                            seen_cycles.insert(cycle_key_str);
                            if let Some(fixture_def) = fixture_defs.get(dep) {
                                cycles.push(FixtureCycle {
                                    cycle_path,
                                    fixture: fixture_def.clone(),
                                });
                            }
                        }
                    } else if !visited.contains(dep) {
                        // Explore this dependency
                        stack.push((dep.clone(), 0, path.clone()));
                    }
                } else {
                    // Done with this node
                    visited.insert(current.clone());
                    rec_stack.remove(&current);
                }
            }
        }

        cycles
    }

    /// Detect cycles for fixtures in a specific file.
    /// Returns cycles where the first fixture in the cycle is defined in the given file.
    /// Uses cached cycle detection results for efficiency.
    pub fn detect_fixture_cycles_in_file(
        &self,
        file_path: &Path,
    ) -> Vec<super::types::FixtureCycle> {
        let all_cycles = self.detect_fixture_cycles();
        all_cycles
            .iter()
            .filter(|cycle| cycle.fixture.file_path == file_path)
            .cloned()
            .collect()
    }

    // ============ Scope Validation ============

    /// Detect scope mismatches where a broader-scoped fixture depends on a narrower-scoped fixture.
    /// For example, a session-scoped fixture depending on a function-scoped fixture.
    /// Returns mismatches for fixtures defined in the given file.
    pub fn detect_scope_mismatches_in_file(
        &self,
        file_path: &Path,
    ) -> Vec<super::types::ScopeMismatch> {
        use super::types::ScopeMismatch;

        let mut mismatches = Vec::new();

        // Get fixtures defined in this file
        let Some(fixture_names) = self.file_definitions.get(file_path) else {
            return mismatches;
        };

        for fixture_name in fixture_names.iter() {
            // Get the fixture definition
            let Some(definitions) = self.definitions.get(fixture_name) else {
                continue;
            };

            // Find the definition in this file
            let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
                continue;
            };

            // Check each dependency
            for dep_name in &fixture_def.dependencies {
                // Find the dependency's definition (use resolution logic to get correct one)
                if let Some(dep_definitions) = self.definitions.get(dep_name) {
                    // Find best matching definition for the dependency
                    // Use the first one (most local) - matches cycle detection behavior
                    if let Some(dep_def) = dep_definitions.first() {
                        // Check if scope mismatch: fixture has broader scope than dependency
                        // FixtureScope is ordered: Function < Class < Module < Package < Session
                        if fixture_def.scope > dep_def.scope {
                            mismatches.push(ScopeMismatch {
                                fixture: fixture_def.clone(),
                                dependency: dep_def.clone(),
                            });
                        }
                    }
                }
            }
        }

        mismatches
    }

    /// Resolve a fixture by name for a given file using priority rules.
    ///
    /// Returns the best matching FixtureDefinition based on pytest's
    /// fixture shadowing rules: same file > conftest hierarchy > third-party.
    pub fn resolve_fixture_for_file(
        &self,
        file_path: &Path,
        fixture_name: &str,
    ) -> Option<FixtureDefinition> {
        let definitions = self.definitions.get(fixture_name)?;

        // Priority 1: Same file
        if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
            return Some(def.clone());
        }

        // Priority 2: conftest.py in parent directories (closest first)
        let file_path = self.get_canonical_path(file_path.to_path_buf());
        let mut best_conftest: Option<&FixtureDefinition> = None;
        let mut best_depth = usize::MAX;

        for def in definitions.iter() {
            if def.is_third_party {
                continue;
            }
            if def.file_path.ends_with("conftest.py") {
                if let Some(parent) = def.file_path.parent() {
                    if file_path.starts_with(parent) {
                        let depth = parent.components().count();
                        if depth > best_depth {
                            // Deeper = closer conftest
                            best_conftest = Some(def);
                            best_depth = depth;
                        } else if best_conftest.is_none() {
                            best_conftest = Some(def);
                            best_depth = depth;
                        }
                    }
                }
            }
        }

        if let Some(def) = best_conftest {
            return Some(def.clone());
        }

        // Priority 3: Plugin fixtures (pytest11 entry points)
        if let Some(def) = definitions
            .iter()
            .find(|d| d.is_plugin && !d.is_third_party)
        {
            return Some(def.clone());
        }

        // Priority 4: Third-party (site-packages)
        if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
            return Some(def.clone());
        }

        // Fallback: first definition
        definitions.first().cloned()
    }

    /// Find the name of the function/fixture containing a given line.
    ///
    /// Used for call hierarchy to identify callers.
    pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
        let content = self.get_file_content(file_path)?;

        // Use cached AST to avoid re-parsing
        let parsed = self.get_parsed_ast(file_path, &content)?;

        if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
            // Use cached line index for position calculations
            let line_index = self.get_line_index(file_path, &content);

            for stmt in &module.body {
                if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
                    return Some(name);
                }
            }
        }

        None
    }

    /// Recursively search for a function containing the given line.
    fn find_function_containing_line(
        &self,
        stmt: &Stmt,
        target_line: usize,
        line_index: &[usize],
    ) -> Option<String> {
        match stmt {
            Stmt::FunctionDef(func_def) => {
                let start_line =
                    self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
                let end_line =
                    self.get_line_from_offset(func_def.range.end().to_usize(), line_index);

                if target_line >= start_line && target_line <= end_line {
                    return Some(func_def.name.to_string());
                }
            }
            Stmt::AsyncFunctionDef(func_def) => {
                let start_line =
                    self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
                let end_line =
                    self.get_line_from_offset(func_def.range.end().to_usize(), line_index);

                if target_line >= start_line && target_line <= end_line {
                    return Some(func_def.name.to_string());
                }
            }
            Stmt::ClassDef(class_def) => {
                // Check methods inside the class
                for class_stmt in &class_def.body {
                    if let Some(name) =
                        self.find_function_containing_line(class_stmt, target_line, line_index)
                    {
                        return Some(name);
                    }
                }
            }
            _ => {}
        }
        None
    }
}

// ── Free helpers for get_function_param_insertion_info ───────────────────────

/// Scan `bytes` starting from `start`, tracking paren depth, to find the byte
/// offset of the closing `)` that matches the first `(` encountered.
///
/// Properly skips:
/// - String literals (single, double, and triple-quoted) so that `)` inside a
///   default value like `def f(x=")")` is never counted.
/// - Inline comments (`#` to end-of-line).
///
/// Returns `None` if no matching `)` is found within `bytes`.
fn scan_for_signature_close_paren(bytes: &[u8], start: usize) -> Option<usize> {
    let mut i = start;
    let mut depth: i32 = 0;
    let mut found_open = false;

    while i < bytes.len() {
        match bytes[i] {
            b'#' => {
                // Inline comment: skip to end of line.
                while i < bytes.len() && bytes[i] != b'\n' {
                    i += 1;
                }
            }
            b'"' | b'\'' => {
                let q = bytes[i];
                // Triple-quoted string?
                if i + 2 < bytes.len() && bytes[i + 1] == q && bytes[i + 2] == q {
                    i += 3;
                    while i < bytes.len() {
                        if i + 2 < bytes.len()
                            && bytes[i] == q
                            && bytes[i + 1] == q
                            && bytes[i + 2] == q
                        {
                            i += 3;
                            break;
                        }
                        i += 1;
                    }
                } else {
                    // Single-quoted string.
                    i += 1;
                    while i < bytes.len() {
                        if bytes[i] == b'\\' {
                            i += 2; // skip escaped char
                        } else if bytes[i] == q {
                            i += 1; // consume closing quote
                            break;
                        } else {
                            i += 1;
                        }
                    }
                }
            }
            b'(' => {
                depth += 1;
                found_open = true;
                i += 1;
            }
            b')' if found_open => {
                depth -= 1;
                if depth == 0 {
                    return Some(i);
                }
                i += 1;
            }
            _ => {
                i += 1;
            }
        }
    }

    None
}

/// Convert a byte offset to a 1-based line number using `line_index`.
///
/// `line_index` is built by `FixtureDatabase::build_line_index`: it starts with
/// `0` (byte 0 = start of line 1) and then stores the byte offset of the first
/// character of each subsequent line.
fn byte_offset_to_line_1based(offset: usize, line_index: &[usize]) -> usize {
    match line_index.binary_search(&offset) {
        Ok(line) => line + 1,
        Err(line) => line,
    }
}

/// Convert a byte offset to a 0-based column within its line.
fn byte_offset_to_col(offset: usize, line_index: &[usize]) -> usize {
    let line = byte_offset_to_line_1based(offset, line_index);
    // line_index[line - 1] is the byte start of that 1-based line.
    offset - line_index[line.saturating_sub(1)]
}

/// Recursively walk `stmts` looking for a function definition whose `def`
/// keyword is on `function_line` (1-based).  Returns `ParamInsertionInfo`
/// when the function is found.
fn find_insertion_in_stmts(
    stmts: &[Stmt],
    function_line: usize,
    bytes: &[u8],
    line_index: &[usize],
) -> Option<ParamInsertionInfo> {
    for stmt in stmts {
        if let Some(info) = find_insertion_in_stmt(stmt, function_line, bytes, line_index) {
            return Some(info);
        }
    }
    None
}

/// Match a single statement, recursing into function/class bodies as needed.
fn find_insertion_in_stmt(
    stmt: &Stmt,
    function_line: usize,
    bytes: &[u8],
    line_index: &[usize],
) -> Option<ParamInsertionInfo> {
    match stmt {
        Stmt::FunctionDef(f) => {
            let def_start = f.range.start().to_usize();
            if byte_offset_to_line_1based(def_start, line_index) == function_line {
                return param_insertion_from_args(def_start, &f.args, bytes, line_index);
            }
            // Recurse into the function body (handles nested functions).
            find_insertion_in_stmts(&f.body, function_line, bytes, line_index)
        }
        Stmt::AsyncFunctionDef(f) => {
            let def_start = f.range.start().to_usize();
            if byte_offset_to_line_1based(def_start, line_index) == function_line {
                return param_insertion_from_args(def_start, &f.args, bytes, line_index);
            }
            find_insertion_in_stmts(&f.body, function_line, bytes, line_index)
        }
        Stmt::ClassDef(c) => {
            // Recurse into the class body to find test methods.
            find_insertion_in_stmts(&c.body, function_line, bytes, line_index)
        }
        _ => None,
    }
}

/// Given the byte offset of a `def` keyword and the function's AST `Arguments`,
/// scan the raw source bytes from `def_start` to find the closing `)` and build
/// a `ParamInsertionInfo`.
///
/// `has_params` (`needs_comma`) comes from the AST arg list, which correctly
/// handles all argument forms (`*args`, `**kwargs`, keyword-only, etc.).
fn param_insertion_from_args(
    def_start: usize,
    args: &Arguments,
    bytes: &[u8],
    line_index: &[usize],
) -> Option<ParamInsertionInfo> {
    let has_params = !args.posonlyargs.is_empty()
        || !args.args.is_empty()
        || !args.kwonlyargs.is_empty()
        || args.vararg.is_some()
        || args.kwarg.is_some();

    let close_paren = scan_for_signature_close_paren(bytes, def_start)?;
    let close_paren_line = byte_offset_to_line_1based(close_paren, line_index);

    // When `)` sits on its own line and there are existing params, use the
    // multiline-paren insertion strategy so that the comma ends up after the
    // last argument rather than before `)`.
    if has_params {
        if let Some(ml) = try_multiline_insertion(close_paren, close_paren_line, bytes, line_index)
        {
            return Some(ml);
        }
    }

    Some(ParamInsertionInfo {
        line: close_paren_line,
        char_pos: byte_offset_to_col(close_paren, line_index),
        needs_comma: has_params,
        multiline_indent: None,
    })
}

/// If `close_paren` is on a line that contains only whitespace before it
/// (i.e. `)` is on its own line), return a `ParamInsertionInfo` whose
/// insertion point is right after the last non-whitespace byte before `)`.
///
/// This ensures that for multiline signatures like
/// ```python
/// def test_foo(
///     fixture_a: TypeA,
///     fixture_b: TypeB,   ← last content; trailing comma present
/// ):
/// ```
/// the new parameter is appended as `\n    new_fixture,` rather than
/// `, new_fixture` being injected in front of the lone `)`.
///
/// Returns `None` when `)` is NOT on its own line (single-line or inline-paren
/// signature), telling callers to fall back to the classic strategy.
fn try_multiline_insertion(
    close_paren: usize,
    close_paren_line: usize,
    bytes: &[u8],
    line_index: &[usize],
) -> Option<ParamInsertionInfo> {
    // `)` must be on its own line — only whitespace may precede it on that line.
    let line_start = line_index[close_paren_line - 1];
    let only_ws = bytes[line_start..close_paren]
        .iter()
        .all(|&b| b == b' ' || b == b'\t');
    if !only_ws {
        return None;
    }

    // Scan backwards from the byte just before `)` to find the last
    // non-whitespace byte.  That is either a `,` (trailing comma) or the
    // last character of the final argument.
    let last_content_pos = find_last_content_before(bytes, close_paren)?;
    let has_trailing_comma = bytes[last_content_pos] == b',';

    // Determine the indentation for the new parameter by looking at the
    // leading whitespace of the line that contains `last_content_pos`.
    let indent = indent_of_line_at(bytes, last_content_pos, line_index);

    // Insert point: the byte immediately after `last_content_pos`.
    // For `    fixture_b: TypeB,\n` that's right after the `,`, before `\n`.
    let insert_offset = last_content_pos + 1;
    let insert_line = byte_offset_to_line_1based(insert_offset, line_index);
    let insert_col = byte_offset_to_col(insert_offset, line_index);

    Some(ParamInsertionInfo {
        line: insert_line,
        char_pos: insert_col,
        // If there is already a trailing comma we only need to emit the
        // newline + indent + new param (+ trailing comma to mirror style).
        // If there is no trailing comma we must first add `,` after the last
        // argument and then emit the newline + indent + new param.
        needs_comma: !has_trailing_comma,
        multiline_indent: Some(indent),
    })
}

/// Scan backwards from `before` (exclusive) through `bytes`, skipping ASCII
/// whitespace (`' '`, `'\t'`, `'\n'`, `'\r'`), and return the byte offset of
/// the first non-whitespace byte found, or `None` if only whitespace precedes
/// the position.
fn find_last_content_before(bytes: &[u8], before: usize) -> Option<usize> {
    let mut pos = before;
    while pos > 0 {
        pos -= 1;
        match bytes[pos] {
            b' ' | b'\t' | b'\n' | b'\r' => continue,
            _ => return Some(pos),
        }
    }
    None
}

/// Return the leading-whitespace string of the line that contains `byte_pos`.
fn indent_of_line_at(bytes: &[u8], byte_pos: usize, line_index: &[usize]) -> String {
    let line_1based = byte_offset_to_line_1based(byte_pos, line_index);
    let line_start = line_index[line_1based - 1];
    let indent_len = bytes[line_start..]
        .iter()
        .take_while(|&&b| b == b' ' || b == b'\t')
        .count();
    String::from_utf8_lossy(&bytes[line_start..line_start + indent_len]).into_owned()
}
