use crate::ConstraintVec;
use crate::config::ParserConfig;
use crate::constraints::{Constraint, GitStatusFilter, TextPartsBuffer};
use crate::glob_detect::has_wildcards;
use crate::location::{Location, parse_location};

#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum FuzzyQuery<'a> {
    Parts(TextPartsBuffer<'a>),
    Text(&'a str),
    Empty,
}

#[derive(Debug, Clone, PartialEq)]
pub struct FFFQuery<'a> {
    /// The original raw query string before parsing
    pub raw_query: &'a str,
    /// Parsed constraints (stack-allocated for ≤8 constraints)
    pub constraints: ConstraintVec<'a>,
    pub fuzzy_query: FuzzyQuery<'a>,
    /// Parsed location (e.g., file:12:4 -> line 12, col 4)
    pub location: Option<Location>,
}

/// Main query parser - zero-cost wrapper around configuration
#[derive(Debug)]
pub struct QueryParser<C: ParserConfig> {
    config: C,
}

impl<C: ParserConfig> QueryParser<C> {
    pub fn new(config: C) -> Self {
        Self { config }
    }

    pub fn parse<'a>(&self, query: &'a str) -> FFFQuery<'a> {
        let raw_query = query;
        let config: &C = &self.config;
        let mut constraints = ConstraintVec::new();
        let query = query.trim();

        let whitespace_count = query.chars().filter(|c| c.is_whitespace()).count();

        // Single token - check if it's a constraint or plain text
        if whitespace_count == 0 {
            // Try to parse as constraint first
            if let Some(constraint) = parse_token(query, config) {
                // Don't treat filename tokens (FilePath) as constraints in single-token
                // queries — the user is fuzzy-searching, not filtering. FilePath constraints
                // are only useful as filters in multi-token queries like "score.rs search".
                //
                // Also skip PathSegment constraints when the token looks like an absolute
                // file path with a location suffix (e.g. /Users/.../file.rs:12). Without
                // this, the leading `/` causes the entire path to be consumed as a
                // PathSegment, preventing location parsing from running.
                let has_location_suffix = matches!(constraint, Constraint::PathSegment(_))
                    && query.bytes().any(|b| b == b':')
                    && query
                        .bytes()
                        .rev()
                        .take_while(|&b| b != b':')
                        .all(|b| b.is_ascii_digit());
                if !matches!(constraint, Constraint::FilePath(_)) && !has_location_suffix {
                    constraints.push(constraint);
                    return FFFQuery {
                        raw_query,
                        constraints,
                        fuzzy_query: FuzzyQuery::Empty,
                        location: None,
                    };
                }
            }

            // Try to extract location from single token (e.g., "file:12")
            if config.enable_location() {
                let (query_without_loc, location) = parse_location(query);
                if location.is_some() {
                    return FFFQuery {
                        raw_query,
                        constraints,
                        fuzzy_query: FuzzyQuery::Text(query_without_loc),
                        location,
                    };
                }
            }

            // Plain text single token
            return FFFQuery {
                raw_query,
                constraints,
                fuzzy_query: if query.is_empty() {
                    FuzzyQuery::Empty
                } else {
                    FuzzyQuery::Text(query)
                },
                location: None,
            };
        }

        let mut text_parts = TextPartsBuffer::new();
        let tokens = query.split_whitespace();

        let mut has_file_path = false;
        for token in tokens {
            match parse_token(token, config) {
                Some(Constraint::FilePath(_)) => {
                    if has_file_path {
                        // Only one FilePath constraint allowed; treat extra path
                        // tokens as literal text (e.g. an import path the user is
                        // searching for).
                        text_parts.push(token);
                    } else {
                        constraints.push(Constraint::FilePath(token));
                        has_file_path = true;
                    }
                }
                Some(constraint) => {
                    constraints.push(constraint);
                }
                None => {
                    text_parts.push(token);
                }
            }
        }

        // Try to extract location from the last fuzzy token
        // e.g., "search file:12" -> fuzzy="search file", location=Line(12)
        let location = if config.enable_location() && !text_parts.is_empty() {
            let last_idx = text_parts.len() - 1;
            let (without_loc, loc) = parse_location(text_parts[last_idx]);
            if loc.is_some() {
                // Update the last part to be without the location suffix
                text_parts[last_idx] = without_loc;
                loc
            } else {
                None
            }
        } else {
            None
        };

        let fuzzy_query = if text_parts.is_empty() {
            FuzzyQuery::Empty
        } else if text_parts.len() == 1 {
            // If the only remaining text is empty after location extraction, treat as Empty
            if text_parts[0].is_empty() {
                FuzzyQuery::Empty
            } else {
                FuzzyQuery::Text(text_parts[0])
            }
        } else {
            // Filter out empty parts that might result from location extraction
            if text_parts.iter().all(|p| p.is_empty()) {
                FuzzyQuery::Empty
            } else {
                FuzzyQuery::Parts(text_parts)
            }
        };

        FFFQuery {
            raw_query,
            constraints,
            fuzzy_query,
            location,
        }
    }
}

impl<'a> FFFQuery<'a> {
    /// Returns the grep search text by joining all non-constraint text tokens.
    ///
    /// Backslash-escaped tokens (e.g. `\*.rs`) are included as literal text
    /// with the leading `\` stripped, since the backslash is only an escape
    /// signal to the parser and should not appear in the final pattern.
    ///
    /// `FuzzyQuery::Empty` → empty string
    /// `FuzzyQuery::Text("foo")` → `"foo"`
    /// `FuzzyQuery::Parts(["a", "\\*.rs", "b"])` → `"a *.rs b"`
    pub fn grep_text(&self) -> String {
        match &self.fuzzy_query {
            FuzzyQuery::Empty => String::new(),
            FuzzyQuery::Text(t) => strip_leading_backslash(t).to_string(),
            FuzzyQuery::Parts(parts) => parts
                .iter()
                .map(|t| strip_leading_backslash(t))
                .collect::<Vec<_>>()
                .join(" "),
        }
    }
}

/// Strip the leading `\` from a backslash-escaped constraint token only.
///
/// We strip the backslash when the next character is a constraint trigger
/// (`*`, `/`, `!`) — the user typed `\*.rs` to mean literal `*.rs`, not an
/// extension constraint. For regex escape sequences like `\w`, `\b`, `\d`,
/// `\s`, `\n` etc., the backslash is preserved so regex mode works correctly.
#[inline]
fn strip_leading_backslash(token: &str) -> &str {
    if token.len() > 1 && token.starts_with('\\') {
        let next = token.as_bytes()[1];
        // Only strip if the backslash is escaping a constraint trigger character
        if next == b'*' || next == b'/' || next == b'!' {
            return &token[1..];
        }
    }
    token
}

impl Default for QueryParser<crate::FileSearchConfig> {
    fn default() -> Self {
        Self::new(crate::FileSearchConfig)
    }
}

#[inline]
fn parse_token<'a, C: ParserConfig>(token: &'a str, config: &C) -> Option<Constraint<'a>> {
    // Backslash escape: \token → treat as literal text, skip all constraint parsing.
    // The leading \ is stripped by the caller when building the search text.
    if token.starts_with('\\') && token.len() > 1 {
        return None;
    }

    let first_byte = token.as_bytes().first()?;

    match first_byte {
        b'*' if config.enable_extension() => {
            // Ignore incomplete patterns like "*" or "*."
            if token == "*" || token == "*." {
                return None;
            }

            // Try extension first (*.rs) - simple patterns without additional wildcards
            if let Some(constraint) = parse_extension(token) {
                // Only return Extension if the rest doesn't have wildcards
                // e.g., *.rs is Extension, but *.test.* should be Glob
                let ext_part = &token[2..];
                if !has_wildcards(ext_part) {
                    return Some(constraint);
                }
            }
            // Has wildcards -> use config-specific glob detection
            if config.enable_glob() && config.is_glob_pattern(token) {
                return Some(Constraint::Glob(token));
            }
            None
        }
        b'!' if config.enable_exclude() => parse_negation(token, config),
        b'/' if config.enable_path_segments() => parse_path_segment(token),
        _ if config.enable_path_segments() && token.ends_with('/') => {
            // Handle trailing slash syntax: www/ -> PathSegment("www")
            parse_path_segment_trailing(token)
        }
        _ => {
            // Check for glob patterns using config-specific detection
            if config.enable_glob() && config.is_glob_pattern(token) {
                return Some(Constraint::Glob(token));
            }

            // Check for key:value patterns
            if let Some(colon_idx) = memchr(b':', token.as_bytes()) {
                let (key, value_with_colon) = token.split_at(colon_idx);
                let value = &value_with_colon[1..]; // Skip the colon

                match key {
                    "type" if config.enable_type_filter() => {
                        return Some(Constraint::FileType(value));
                    }
                    "status" | "st" | "g" | "git" if config.enable_git_status() => {
                        return parse_git_status(value);
                    }
                    _ => {}
                }
            }

            // Try custom parsers
            config.parse_custom(token)
        }
    }
}

/// Find first occurrence of byte in slice (fast memchr-like implementation)
#[inline]
fn memchr(needle: u8, haystack: &[u8]) -> Option<usize> {
    haystack.iter().position(|&b| b == needle)
}

/// Parse extension pattern: *.rs -> Extension("rs")
#[inline]
fn parse_extension(token: &str) -> Option<Constraint<'_>> {
    if token.len() > 2 && token.starts_with("*.") {
        Some(Constraint::Extension(&token[2..]))
    } else {
        None
    }
}

/// Parse negation pattern: !*.rs -> Not(Extension("rs")), !test -> Not(Text("test"))
/// This allows negating any constraint type
#[inline]
fn parse_negation<'a, C: ParserConfig>(token: &'a str, config: &C) -> Option<Constraint<'a>> {
    if token.len() <= 1 {
        return None;
    }

    let inner_token = &token[1..];

    // Try to parse the inner token as any constraint
    if let Some(inner_constraint) = parse_token_without_negation(inner_token, config) {
        // Wrap it in a Not constraint
        return Some(Constraint::Not(Box::new(inner_constraint)));
    }

    // If it's not a special constraint, treat it as negated text
    // For backward compatibility with !test syntax
    Some(Constraint::Not(Box::new(Constraint::Text(inner_token))))
}

/// Parse a token without checking for negation (to avoid infinite recursion)
#[inline]
fn parse_token_without_negation<'a, C: ParserConfig>(
    token: &'a str,
    config: &C,
) -> Option<Constraint<'a>> {
    // Backslash escape applies here too
    if token.starts_with('\\') && token.len() > 1 {
        return None;
    }

    let first_byte = token.as_bytes().first()?;

    match first_byte {
        b'*' if config.enable_extension() => {
            // Try extension first (*.rs) - simple patterns without additional wildcards
            if let Some(constraint) = parse_extension(token) {
                let ext_part = &token[2..];
                if !has_wildcards(ext_part) {
                    return Some(constraint);
                }
            }
            // Has wildcards -> use config-specific glob detection
            if config.enable_glob() && config.is_glob_pattern(token) {
                return Some(Constraint::Glob(token));
            }
            None
        }
        b'/' if config.enable_path_segments() => parse_path_segment(token),
        _ if config.enable_path_segments() && token.ends_with('/') => {
            // Handle trailing slash syntax: www/ -> PathSegment("www")
            parse_path_segment_trailing(token)
        }
        _ => {
            // Check for glob patterns using config-specific detection
            if config.enable_glob() && config.is_glob_pattern(token) {
                return Some(Constraint::Glob(token));
            }

            // Check for key:value patterns
            if let Some(colon_idx) = memchr(b':', token.as_bytes()) {
                let (key, value_with_colon) = token.split_at(colon_idx);
                let value = &value_with_colon[1..]; // Skip the colon

                match key {
                    "type" if config.enable_type_filter() => {
                        return Some(Constraint::FileType(value));
                    }
                    "status" | "st" | "g" | "git" if config.enable_git_status() => {
                        return parse_git_status(value);
                    }
                    _ => {}
                }
            }

            config.parse_custom(token)
        }
    }
}

/// Parse path segment: /src/ -> PathSegment("src")
#[inline]
fn parse_path_segment(token: &str) -> Option<Constraint<'_>> {
    if token.len() > 1 && token.starts_with('/') {
        let segment = token.trim_start_matches('/').trim_end_matches('/');
        if !segment.is_empty() {
            Some(Constraint::PathSegment(segment))
        } else {
            None
        }
    } else {
        None
    }
}

/// Parse path segment with trailing slash: www/ -> PathSegment("www")
/// Also supports multi-segment paths: libswscale/aarch64/ -> PathSegment("libswscale/aarch64")
#[inline]
fn parse_path_segment_trailing(token: &str) -> Option<Constraint<'_>> {
    if token.len() > 1 && token.ends_with('/') {
        let segment = token.trim_end_matches('/');
        if !segment.is_empty() {
            Some(Constraint::PathSegment(segment))
        } else {
            None
        }
    } else {
        None
    }
}

/// Parse git status filter: modified|m|untracked|u|staged|s
#[inline]
fn parse_git_status(value: &str) -> Option<Constraint<'_>> {
    if value == "*" {
        return None;
    }

    if "modified".starts_with(value) {
        return Some(Constraint::GitStatus(GitStatusFilter::Modified));
    }

    if "untracked".starts_with(value) {
        return Some(Constraint::GitStatus(GitStatusFilter::Untracked));
    }

    if "staged".starts_with(value) {
        return Some(Constraint::GitStatus(GitStatusFilter::Staged));
    }

    if "clean".starts_with(value) {
        return Some(Constraint::GitStatus(GitStatusFilter::Unmodified));
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{FileSearchConfig, GrepConfig};

    #[test]
    fn test_parse_extension() {
        assert_eq!(parse_extension("*.rs"), Some(Constraint::Extension("rs")));
        assert_eq!(
            parse_extension("*.toml"),
            Some(Constraint::Extension("toml"))
        );
        assert_eq!(parse_extension("*"), None);
        assert_eq!(parse_extension("*."), None);
    }

    #[test]
    fn test_incomplete_patterns_ignored() {
        let config = FileSearchConfig;
        // Incomplete patterns should return None and be treated as noise
        assert_eq!(parse_token("*", &config), None);
        assert_eq!(parse_token("*.", &config), None);
    }

    #[test]
    fn test_parse_path_segment() {
        assert_eq!(
            parse_path_segment("/src/"),
            Some(Constraint::PathSegment("src"))
        );
        assert_eq!(
            parse_path_segment("/lib"),
            Some(Constraint::PathSegment("lib"))
        );
        assert_eq!(parse_path_segment("/"), None);
    }

    #[test]
    fn test_parse_path_segment_trailing() {
        assert_eq!(
            parse_path_segment_trailing("www/"),
            Some(Constraint::PathSegment("www"))
        );
        assert_eq!(
            parse_path_segment_trailing("src/"),
            Some(Constraint::PathSegment("src"))
        );
        // Multi-segment paths should work
        assert_eq!(
            parse_path_segment_trailing("src/lib/"),
            Some(Constraint::PathSegment("src/lib"))
        );
        assert_eq!(
            parse_path_segment_trailing("libswscale/aarch64/"),
            Some(Constraint::PathSegment("libswscale/aarch64"))
        );
        // Should not match without trailing slash
        assert_eq!(parse_path_segment_trailing("www"), None);
    }

    #[test]
    fn test_trailing_slash_in_query() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("www/ test");
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::PathSegment("www")
        ));
        assert!(matches!(result.fuzzy_query, FuzzyQuery::Text("test")));
    }

    #[test]
    fn test_parse_git_status() {
        assert_eq!(
            parse_git_status("modified"),
            Some(Constraint::GitStatus(GitStatusFilter::Modified))
        );
        assert_eq!(
            parse_git_status("m"),
            Some(Constraint::GitStatus(GitStatusFilter::Modified))
        );
        assert_eq!(
            parse_git_status("untracked"),
            Some(Constraint::GitStatus(GitStatusFilter::Untracked))
        );
        assert_eq!(parse_git_status("invalid"), None);
    }

    #[test]
    fn test_memchr() {
        assert_eq!(memchr(b':', b"type:rust"), Some(4));
        assert_eq!(memchr(b':', b"nocolon"), None);
        assert_eq!(memchr(b':', b":start"), Some(0));
    }

    #[test]
    fn test_negation_text() {
        let parser = QueryParser::new(FileSearchConfig);
        // Need two tokens for parsing to return Some
        let result = parser.parse("!test foo");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(matches!(**inner, Constraint::Text("test")));
            }
            _ => panic!("Expected Not constraint"),
        }
    }

    #[test]
    fn test_negation_extension() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("!*.rs foo");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(matches!(**inner, Constraint::Extension("rs")));
            }
            _ => panic!("Expected Not(Extension) constraint"),
        }
    }

    #[test]
    fn test_negation_path_segment() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("!/src/ foo");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(matches!(**inner, Constraint::PathSegment("src")));
            }
            _ => panic!("Expected Not(PathSegment) constraint"),
        }
    }

    #[test]
    fn test_negation_git_status() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("!status:modified foo");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(matches!(
                    **inner,
                    Constraint::GitStatus(GitStatusFilter::Modified)
                ));
            }
            _ => panic!("Expected Not(GitStatus) constraint"),
        }
    }

    #[test]
    fn test_negation_git_status_all_key_aliases() {
        let parser = QueryParser::new(FileSearchConfig);
        for key in ["status", "st", "g", "git"] {
            let query = format!("!{key}:modified foo");
            let result = parser.parse(&query);
            assert_eq!(
                result.constraints.len(),
                1,
                "!{key}:modified should produce exactly one constraint"
            );
            match &result.constraints[0] {
                Constraint::Not(inner) => assert!(
                    matches!(**inner, Constraint::GitStatus(GitStatusFilter::Modified)),
                    "!{key}:modified expected Not(GitStatus(Modified)), got Not({inner:?})"
                ),
                other => {
                    panic!("!{key}:modified expected Not(GitStatus), got {other:?}")
                }
            }
        }
    }

    #[test]
    fn test_backslash_escape_extension() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("\\*.rs foo");
        // \*.rs should NOT be parsed as an Extension constraint
        assert_eq!(result.constraints.len(), 0);
        // Both tokens should be text
        match result.fuzzy_query {
            FuzzyQuery::Parts(parts) => {
                assert_eq!(parts.len(), 2);
                assert_eq!(parts[0], "\\*.rs");
                assert_eq!(parts[1], "foo");
            }
            _ => panic!("Expected Parts, got {:?}", result.fuzzy_query),
        }
    }

    #[test]
    fn test_backslash_escape_path_segment() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("\\/src/ foo");
        assert_eq!(result.constraints.len(), 0);
        match result.fuzzy_query {
            FuzzyQuery::Parts(parts) => {
                assert_eq!(parts[0], "\\/src/");
                assert_eq!(parts[1], "foo");
            }
            _ => panic!("Expected Parts, got {:?}", result.fuzzy_query),
        }
    }

    #[test]
    fn test_backslash_escape_negation() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("\\!test foo");
        assert_eq!(result.constraints.len(), 0);
    }

    #[test]
    fn test_grep_text_plain_text() {
        // Multi-token plain text — no constraints
        let q = QueryParser::new(GrepConfig).parse("name =");
        assert_eq!(q.grep_text(), "name =");
    }

    #[test]
    fn test_grep_text_strips_constraint() {
        let q = QueryParser::new(GrepConfig).parse("name = *.rs someth");
        assert_eq!(q.grep_text(), "name = someth");
    }

    #[test]
    fn test_grep_text_leading_constraint() {
        let q = QueryParser::new(GrepConfig).parse("*.rs name =");
        assert_eq!(q.grep_text(), "name =");
    }

    #[test]
    fn test_grep_text_only_constraints() {
        let q = QueryParser::new(GrepConfig).parse("*.rs /src/");
        assert_eq!(q.grep_text(), "");
    }

    #[test]
    fn test_grep_text_path_constraint() {
        let q = QueryParser::new(GrepConfig).parse("name /src/ value");
        assert_eq!(q.grep_text(), "name value");
    }

    #[test]
    fn test_grep_text_negation_constraint() {
        let q = QueryParser::new(GrepConfig).parse("name !*.rs value");
        assert_eq!(q.grep_text(), "name value");
    }

    #[test]
    fn test_grep_text_backslash_escape_stripped() {
        // \*.rs should be text with the leading \ removed
        let q = QueryParser::new(GrepConfig).parse("\\*.rs foo");
        assert_eq!(q.grep_text(), "*.rs foo");

        let q = QueryParser::new(GrepConfig).parse("\\/src/ foo");
        assert_eq!(q.grep_text(), "/src/ foo");

        let q = QueryParser::new(GrepConfig).parse("\\!test foo");
        assert_eq!(q.grep_text(), "!test foo");
    }

    #[test]
    fn test_grep_text_question_mark_is_text() {
        let q = QueryParser::new(GrepConfig).parse("foo? bar");
        assert_eq!(q.grep_text(), "foo? bar");
    }

    #[test]
    fn test_grep_text_bracket_is_text() {
        let q = QueryParser::new(GrepConfig).parse("arr[0] more");
        assert_eq!(q.grep_text(), "arr[0] more");
    }

    #[test]
    fn test_grep_text_path_glob_is_constraint() {
        let q = QueryParser::new(GrepConfig).parse("pattern src/**/*.rs");
        assert_eq!(q.grep_text(), "pattern");
    }

    #[test]
    fn test_grep_question_mark_is_text() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("foo?");
        assert!(result.constraints.is_empty());
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("foo?"));
    }

    #[test]
    fn test_grep_bracket_is_text() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("arr[0] something");
        // arr[0] should NOT be a glob in grep mode
        assert_eq!(result.constraints.len(), 0);
    }

    #[test]
    fn test_grep_path_glob_is_constraint() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("pattern src/**/*.rs");
        // src/**/*.rs contains / so it should be treated as a glob
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::Glob("src/**/*.rs")
        ));
    }

    #[test]
    fn test_grep_brace_is_constraint() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("pattern {src,lib}");
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::Glob("{src,lib}")
        ));
    }

    #[test]
    fn test_grep_text_preserves_backslash_escapes() {
        // Regex patterns like \w+ and \bfoo\b must survive grep_text()
        // The parser sees \w+ as a text token (not a constraint escape),
        // but strip_leading_backslash was stripping the \ anyway.
        let q = QueryParser::new(GrepConfig).parse("pub struct \\w+");
        assert_eq!(
            q.grep_text(),
            "pub struct \\w+",
            "Backslash-w in regex must be preserved"
        );

        let q = QueryParser::new(GrepConfig).parse("\\bword\\b more");
        assert_eq!(
            q.grep_text(),
            "\\bword\\b more",
            "Backslash-b word boundaries must be preserved"
        );

        // Single-token regex like "fn\\s+\\w+" returns FFFQuery with Text fuzzy query
        let result = QueryParser::new(GrepConfig).parse("fn\\s+\\w+");
        assert!(result.constraints.is_empty());
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("fn\\s+\\w+"));

        // But the escaped constraint forms SHOULD still be stripped:
        let q = QueryParser::new(GrepConfig).parse("\\*.rs foo");
        assert_eq!(
            q.grep_text(),
            "*.rs foo",
            "Escaped constraint \\*.rs should still have backslash stripped"
        );

        let q = QueryParser::new(GrepConfig).parse("\\/src/ foo");
        assert_eq!(
            q.grep_text(),
            "/src/ foo",
            "Escaped constraint \\/src/ should still have backslash stripped"
        );
    }

    #[test]
    fn test_grep_bare_star_is_text() {
        let parser = QueryParser::new(GrepConfig);
        // "a*b" contains * but no / or {} — should be text in grep mode
        let result = parser.parse("a*b something");
        assert_eq!(
            result.constraints.len(),
            0,
            "bare * without / should be text"
        );
    }

    #[test]
    fn test_grep_negated_text() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("pattern !test");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(
                    matches!(**inner, Constraint::Text("test")),
                    "Expected Not(Text(\"test\")), got Not({:?})",
                    inner
                );
            }
            other => panic!("Expected Not constraint, got {:?}", other),
        }
    }

    #[test]
    fn test_grep_negated_path_segment() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("pattern !/src/");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(
                    matches!(**inner, Constraint::PathSegment("src")),
                    "Expected Not(PathSegment(\"src\")), got Not({:?})",
                    inner
                );
            }
            other => panic!("Expected Not constraint, got {:?}", other),
        }
    }

    #[test]
    fn test_grep_negated_extension() {
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("pattern !*.rs");
        assert_eq!(result.constraints.len(), 1);
        match &result.constraints[0] {
            Constraint::Not(inner) => {
                assert!(
                    matches!(**inner, Constraint::Extension("rs")),
                    "Expected Not(Extension(\"rs\")), got Not({:?})",
                    inner
                );
            }
            other => panic!("Expected Not constraint, got {:?}", other),
        }
    }

    #[test]
    fn test_ai_grep_detects_file_path() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("libswscale/input.c rgba32ToY");
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(
                result.constraints[0],
                Constraint::FilePath("libswscale/input.c")
            ),
            "Expected FilePath, got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.grep_text(), "rgba32ToY");
    }

    #[test]
    fn test_ai_grep_detects_nested_file_path() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("src/main.rs fn main");
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::FilePath("src/main.rs")
        ));
        assert_eq!(result.grep_text(), "fn main");
    }

    #[test]
    fn test_ai_grep_no_false_positive_trailing_slash() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("src/ pattern");
        // Should be PathSegment, NOT FilePath
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::PathSegment("src")),
            "Expected PathSegment, got {:?}",
            result.constraints[0]
        );
    }

    #[test]
    fn test_ai_grep_bare_filename_is_file_path() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("main.rs pattern");
        // Bare filename with valid extension → FilePath constraint
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::FilePath("main.rs")),
            "Expected FilePath, got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.grep_text(), "pattern");
    }

    #[test]
    fn test_ai_grep_bare_filename_schema_rs() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("schema.rs part_revisions");
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::FilePath("schema.rs")),
            "Expected FilePath(schema.rs), got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.grep_text(), "part_revisions");
    }

    #[test]
    fn test_ai_grep_bare_word_no_extension_not_constraint() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("schema pattern");
        // No extension → not a file path, just text
        assert_eq!(result.constraints.len(), 0);
        assert_eq!(result.grep_text(), "schema pattern");
    }

    #[test]
    fn test_ai_grep_no_false_positive_no_extension() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("src/utils pattern");
        // No extension in last component → not a file path, just text
        assert_eq!(result.constraints.len(), 0);
        assert_eq!(result.grep_text(), "src/utils pattern");
    }

    #[test]
    fn test_ai_grep_wildcard_not_filepath() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("src/**/*.rs pattern");
        // Contains wildcards → should be a Glob, not FilePath
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::Glob("src/**/*.rs")),
            "Expected Glob, got {:?}",
            result.constraints[0]
        );
    }

    #[test]
    fn test_ai_grep_star_text_star_is_glob() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("*quote* TODO");
        // `*quote*` should be recognised as a glob constraint in AI mode
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::Glob("*quote*")),
            "Expected Glob(*quote*), got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("TODO"));
    }

    #[test]
    fn test_ai_grep_bare_star_not_glob() {
        use crate::AiGrepConfig;
        let parser = QueryParser::new(AiGrepConfig);
        let result = parser.parse("* pattern");
        // Bare `*` should NOT be treated as a glob (too broad)
        assert!(
            result.constraints.is_empty(),
            "Expected no constraints, got {:?}",
            result.constraints
        );
    }

    #[test]
    fn test_grep_no_location_parsing_single_token() {
        let parser = QueryParser::new(GrepConfig);
        // localhost:8080 should NOT be parsed as location -- it's a search pattern
        let result = parser.parse("localhost:8080");
        assert!(result.constraints.is_empty());
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("localhost:8080"));
    }

    #[test]
    fn test_grep_no_location_parsing_multi_token() {
        let q = QueryParser::new(GrepConfig).parse("*.rs localhost:8080");
        assert_eq!(
            q.grep_text(),
            "localhost:8080",
            "Colon-number suffix should be preserved in grep text"
        );
        assert!(
            q.location.is_none(),
            "Grep should not parse location from colon-number"
        );
    }

    #[test]
    fn test_grep_braces_without_comma_is_text() {
        let parser = QueryParser::new(GrepConfig);
        // Code patterns like format!("{}") should NOT be treated as brace expansion
        let result = parser.parse(r#"format!("{}\\AppData", home)"#);
        assert!(
            result.constraints.is_empty(),
            "Braces without comma should be text, got {:?}",
            result.constraints
        );
        assert_eq!(result.grep_text(), r#"format!("{}\\AppData", home)"#);
    }

    #[test]
    fn test_grep_format_braces_not_glob() {
        let parser = QueryParser::new(GrepConfig);
        // Code like format!("{}\\path", var) must not have tokens eaten as glob constraints.
        // The trailing comma on the first token means both { } and , are present,
        // but the comma is outside the braces so it should NOT trigger brace expansion.
        let input = "format!(\"{}\\\\AppData\", home)";
        let result = parser.parse(input);
        assert!(
            result.constraints.is_empty(),
            "format! pattern should have no constraints, got {:?}",
            result.constraints
        );
    }

    #[test]
    fn test_grep_config_star_text_star_not_glob() {
        use crate::GrepConfig;
        let parser = QueryParser::new(GrepConfig);
        let result = parser.parse("*quote* TODO");
        // Regular grep mode should NOT treat `*quote*` as a glob
        assert!(
            result.constraints.is_empty(),
            "Expected no constraints in GrepConfig, got {:?}",
            result.constraints
        );
    }

    #[test]
    fn test_file_picker_bare_filename_constraint() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("score.rs file_picker");
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::FilePath("score.rs")),
            "Expected FilePath(\"score.rs\"), got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("file_picker"));
    }

    #[test]
    fn test_file_picker_path_prefixed_filename_constraint() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("libswscale/slice.c lum_convert");
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(
                result.constraints[0],
                Constraint::FilePath("libswscale/slice.c")
            ),
            "Expected FilePath(\"libswscale/slice.c\"), got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("lum_convert"));
    }

    #[test]
    fn test_file_picker_single_token_filename_stays_fuzzy() {
        let parser = QueryParser::new(FileSearchConfig);
        // Single-token filename should NOT become a constraint -- it should
        // return FFFQuery with Text fuzzy query so the caller uses it for fuzzy matching.
        let result = parser.parse("score.rs");
        assert!(result.constraints.is_empty());
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("score.rs"));
    }

    #[test]
    fn test_absolute_path_with_location_not_path_segment() {
        let parser = QueryParser::new(FileSearchConfig);
        // Absolute file path with :line should parse as text + location,
        // NOT as a PathSegment constraint (which would eat the whole token).
        let result = parser.parse("/Users/neogoose/dev/fframes/src/renderer/concatenator.rs:12");
        assert!(
            result.constraints.is_empty(),
            "Absolute path with location should not become a constraint, got {:?}",
            result.constraints
        );
        assert_eq!(
            result.fuzzy_query,
            FuzzyQuery::Text("/Users/neogoose/dev/fframes/src/renderer/concatenator.rs")
        );
        assert_eq!(result.location, Some(Location::Line(12)));
    }

    #[test]
    fn test_file_picker_filename_with_multiple_fuzzy_parts() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("main.rs src components");
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::FilePath("main.rs")
        ));
        assert_eq!(
            result.fuzzy_query,
            FuzzyQuery::Parts(vec!["src", "components"])
        );
    }

    #[test]
    fn test_file_picker_version_number_not_filename() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("v2.0 release");
        // v2.0 extension starts with digit → not a filename constraint
        assert!(
            result.constraints.is_empty(),
            "v2.0 should not be a FilePath constraint, got {:?}",
            result.constraints
        );
    }

    #[test]
    fn test_file_picker_only_one_filepath_constraint() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("main.rs score.rs");
        // Only first filename becomes a constraint; second is text
        assert_eq!(result.constraints.len(), 1);
        assert!(matches!(
            result.constraints[0],
            Constraint::FilePath("main.rs")
        ));
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("score.rs"));
    }

    #[test]
    fn test_file_picker_filename_with_extension_constraint() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("main.rs *.lua");
        // main.rs → FilePath, *.lua → Extension
        assert_eq!(result.constraints.len(), 2);
        assert!(matches!(
            result.constraints[0],
            Constraint::FilePath("main.rs")
        ));
        assert!(matches!(
            result.constraints[1],
            Constraint::Extension("lua")
        ));
    }

    #[test]
    fn test_file_picker_dotfile_is_filename() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse(".gitignore src");
        assert_eq!(result.constraints.len(), 1);
        assert!(
            matches!(result.constraints[0], Constraint::FilePath(".gitignore")),
            "Expected FilePath(\".gitignore\"), got {:?}",
            result.constraints[0]
        );
        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("src"));
    }

    #[test]
    fn test_file_picker_no_extension_not_filename() {
        let parser = QueryParser::new(FileSearchConfig);
        let result = parser.parse("Makefile src");
        // No dot → not a filename constraint
        assert!(
            result.constraints.is_empty(),
            "Makefile should not be a FilePath constraint, got {:?}",
            result.constraints
        );
    }
}
