use super::*;
use smallvec::SmallVec;

pub fn collect_array_expansion_part_spans(word: &Word, spans: &mut Vec<Span>) {
    collect_array_expansion_spans(&word.parts, false, false, spans);
}

pub fn all_elements_array_expansion_part_spans(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_all_elements_array_expansion_part_spans(word, locator, shell_dialect, &mut spans);
    spans
}

pub fn collect_all_elements_array_expansion_part_spans(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    collect_all_elements_array_expansion_spans(&word.parts, locator, shell_dialect, spans);
}

pub fn word_has_all_elements_array_expansion_syntax(
    word: &Word,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    parts_have_all_elements_array_expansion_syntax(&word.parts, source, shell_dialect)
}

pub fn direct_all_elements_array_expansion_part_spans(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_direct_all_elements_array_expansion_part_spans(
        word,
        locator,
        shell_dialect,
        &mut spans,
    );
    spans
}

pub fn collect_direct_all_elements_array_expansion_part_spans(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    let escaped_templates = escaped_parameter_template_bodies(word.span, locator.source());
    collect_direct_all_elements_array_expansion_part_spans_with_escaped_templates(
        word,
        locator,
        shell_dialect,
        escaped_templates.as_slice(),
        spans,
    );
}

pub fn collect_unquoted_all_elements_array_expansion_part_spans(
    word: &Word,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    collect_unquoted_all_elements_array_expansion_spans(
        &word.parts,
        false,
        source,
        shell_dialect,
        spans,
    );
}

pub fn word_quoted_all_elements_array_slice_spans(word: &Word) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_all_elements_array_slice_spans(&word.parts, false, true, &mut spans);
    spans
}

#[cfg(test)]
pub fn word_has_quoted_all_elements_array_slice(word: &Word) -> bool {
    !word_quoted_all_elements_array_slice_spans(word).is_empty()
}

pub fn word_has_direct_all_elements_array_expansion_in_source(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    !direct_all_elements_array_expansion_part_spans(word, locator, shell_dialect).is_empty()
}

#[cfg(test)]
pub fn word_quoted_unindexed_bash_source_span_in_source(word: &Word, source: &str) -> Option<Span> {
    let mut spans = Vec::new();
    collect_quoted_unindexed_bash_source_spans(&word.parts, false, source, &mut spans);
    spans.into_iter().next()
}

pub fn collect_unquoted_array_expansion_part_spans(word: &Word, spans: &mut Vec<Span>) {
    collect_array_expansion_spans(&word.parts, false, true, spans);
}

pub fn word_unquoted_star_parameter_spans(word: &Word, unquoted_array_spans: &[Span]) -> Vec<Span> {
    word.parts_with_spans()
        .filter_map(|(part, span)| {
            (unquoted_array_spans.contains(&span) && part_uses_star_splat(part)).then_some(span)
        })
        .collect()
}

pub fn word_unquoted_star_splat_spans(word: &Word) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_unquoted_star_splat_spans(&word.parts, false, &mut spans);
    spans
}

pub fn word_quoted_star_splat_spans(word: &Word) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_quoted_star_splat_spans(&word.parts, false, &mut spans);
    spans
}

pub fn word_positional_at_splat_spans(word: &Word) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_positional_at_splat_spans(&word.parts, &mut spans);
    spans
}

pub fn word_is_pure_positional_at_splat(word: &Word) -> bool {
    parts_are_pure_positional_at_splat(&word.parts)
}

pub fn word_positional_at_splat_spans_in_source(word: &Word, source: &str) -> Vec<Span> {
    word_positional_at_splat_spans(word)
        .into_iter()
        .filter(|span| !span_is_escaped(*span, source))
        .collect()
}

#[cfg(test)]
pub fn word_folded_positional_at_splat_span_in_source(word: &Word, source: &str) -> Option<Span> {
    let spans = word_positional_at_splat_spans(word)
        .into_iter()
        .filter(|span| !span_is_escaped(*span, source))
        .collect::<Vec<_>>();
    let first = spans.first().copied()?;

    if spans.len() == 1
        && (word_has_single_positional_at_splat_part(word)
            || positional_at_splat_is_standalone_expansion(word, source))
    {
        return None;
    }

    Some(first)
}

pub fn word_folded_all_elements_array_span_in_source(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    let spans = folded_all_elements_array_candidate_spans(word, locator, shell_dialect)
        .into_iter()
        .filter(|span| !span_is_escaped(*span, source))
        .collect::<Vec<_>>();
    let first = spans.first().copied()?;

    if spans.len() == 1
        && (word_has_single_folded_all_elements_array_part_in_dialect(word, shell_dialect)
            || all_elements_array_expansion_is_standalone(word, locator, shell_dialect))
    {
        return None;
    }

    Some(first)
}

pub(crate) fn folded_all_elements_array_candidate_spans(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_folded_all_elements_array_candidate_spans(
        &word.parts,
        locator,
        shell_dialect,
        &mut spans,
    );
    spans
}

pub(crate) fn collect_folded_all_elements_array_candidate_spans(
    parts: &[WordPartNode],
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    let source = locator.source();
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_folded_all_elements_array_candidate_spans(
                    parts,
                    locator,
                    shell_dialect,
                    spans,
                )
            }
            WordPart::Parameter(parameter)
                if parameter_uses_replacement_all_elements_array_expansion(parameter) =>
            {
                spans.push(part.span);
            }
            _ if part_uses_direct_all_elements_array_expansion(&part.kind, shell_dialect) => {
                if let Some(span) = normalize_direct_all_elements_array_expansion_span(
                    part.span,
                    locator,
                    shell_dialect,
                ) {
                    spans.push(span);
                }
            }
            WordPart::Parameter(parameter)
                if parameter_might_use_all_elements_array_expansion(
                    parameter,
                    part.span,
                    source,
                    shell_dialect,
                ) =>
            {
                if let Some(span) = normalize_nested_direct_all_elements_array_expansion_span(
                    part.span,
                    locator,
                    shell_dialect,
                ) {
                    spans.push(span);
                }
            }
            _ => {}
        }
    }
}

pub(crate) fn collect_array_expansion_spans(
    parts: &[WordPartNode],
    quoted: bool,
    only_unquoted: bool,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_array_expansion_spans(parts, true, only_unquoted, spans)
            }
            WordPart::Variable(name)
                if matches!(name.as_str(), "@" | "*") && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::ArrayAccess(reference)
                if reference.has_array_selector() && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::Parameter(parameter)
                if parameter_is_array_like(parameter) && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::ParameterExpansion {
                reference,
                operator,
                ..
            } if !matches!(operator.as_ref(), ParameterOp::UseReplacement)
                && reference.has_array_selector()
                && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::IndirectExpansion {
                reference,
                operator,
                ..
            } if !matches!(operator.as_deref(), Some(ParameterOp::UseReplacement))
                && reference.has_array_selector()
                && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::Transformation { reference, .. }
                if reference.has_array_selector() && (!quoted || !only_unquoted) =>
            {
                spans.push(part.span);
            }
            WordPart::ArraySlice { .. } | WordPart::ArrayIndices(_)
                if !quoted || !only_unquoted =>
            {
                spans.push(part.span);
            }
            _ => {}
        }
    }
}

pub(crate) fn collect_all_elements_array_expansion_spans(
    parts: &[WordPartNode],
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    let source = locator.source();
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_all_elements_array_expansion_spans(parts, locator, shell_dialect, spans)
            }
            WordPart::Variable(name) if name.as_str() == "@" => {
                if let Some(span) =
                    normalize_all_elements_array_expansion_span(part.span, locator, shell_dialect)
                {
                    spans.push(span);
                }
            }
            WordPart::ArrayAccess(reference)
                if var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect) =>
            {
                if let Some(span) =
                    normalize_all_elements_array_expansion_span(part.span, locator, shell_dialect)
                {
                    spans.push(span);
                }
            }
            WordPart::ArrayIndices(reference)
                if var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect) =>
            {
                if let Some(span) =
                    normalize_all_elements_array_expansion_span(part.span, locator, shell_dialect)
                {
                    spans.push(span);
                }
            }
            WordPart::PrefixMatch {
                kind: PrefixMatchKind::At,
                ..
            } => {
                if let Some(span) =
                    normalize_all_elements_array_expansion_span(part.span, locator, shell_dialect)
                {
                    spans.push(span);
                }
            }
            WordPart::Parameter(parameter)
                if parameter_might_use_all_elements_array_expansion(
                    parameter,
                    part.span,
                    source,
                    shell_dialect,
                ) =>
            {
                if let Some(span) =
                    normalize_all_elements_array_expansion_span(part.span, locator, shell_dialect)
                {
                    spans.push(span);
                }
            }
            WordPart::Variable(name) if name.as_str() == "*" => {}
            _ => {}
        }
    }
}

pub(crate) fn parts_have_all_elements_array_expansion_syntax(
    parts: &[WordPartNode],
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    for (index, part) in parts.iter().enumerate() {
        let has_match = match &part.kind {
            WordPart::SingleQuoted { .. } => false,
            WordPart::DoubleQuoted { parts, .. } => {
                parts_have_all_elements_array_expansion_syntax(parts, source, shell_dialect)
            }
            WordPart::Variable(name) => {
                name.as_str() == "@"
                    && !part_has_zsh_short_positional_subscript(parts, index, source, shell_dialect)
            }
            WordPart::ArrayAccess(reference) | WordPart::ArrayIndices(reference) => {
                var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
            }
            WordPart::ArraySlice { reference, .. } => {
                var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
            }
            WordPart::PrefixMatch {
                kind: PrefixMatchKind::At,
                ..
            } => true,
            WordPart::PrefixMatch {
                kind: PrefixMatchKind::Star,
                ..
            } => false,
            WordPart::Parameter(parameter) => {
                parameter_uses_unquoted_all_elements_array_expansion(parameter, shell_dialect)
            }
            WordPart::Literal(_)
            | WordPart::CommandSubstitution { .. }
            | WordPart::ArithmeticExpansion { .. }
            | WordPart::Length(_)
            | WordPart::ParameterExpansion { .. }
            | WordPart::IndirectExpansion { .. }
            | WordPart::ProcessSubstitution { .. }
            | WordPart::Transformation { .. }
            | WordPart::Substring { .. }
            | WordPart::ArrayLength(_)
            | WordPart::ZshQualifiedGlob(_) => false,
        };

        if has_match {
            return true;
        }
    }

    false
}

pub(crate) fn collect_unquoted_all_elements_array_expansion_spans(
    parts: &[WordPartNode],
    quoted: bool,
    _source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_unquoted_all_elements_array_expansion_spans(
                    parts,
                    true,
                    _source,
                    shell_dialect,
                    spans,
                )
            }
            _ if !quoted
                && part_uses_unquoted_all_elements_array_expansion_in_dialect(
                    &part.kind,
                    shell_dialect,
                ) =>
            {
                spans.push(part.span)
            }
            _ => {}
        }
    }
}

pub(crate) fn collect_all_elements_array_slice_spans(
    parts: &[WordPartNode],
    quoted: bool,
    only_quoted: bool,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_all_elements_array_slice_spans(parts, true, only_quoted, spans)
            }
            _ if (!only_quoted || quoted) && part_uses_all_elements_array_slice(&part.kind) => {
                spans.push(part.span)
            }
            _ => {}
        }
    }
}

pub(crate) fn collect_direct_all_elements_array_expansion_part_spans_with_escaped_templates(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    escaped_templates: &[EscapedParameterTemplateBody],
    spans: &mut Vec<Span>,
) {
    collect_direct_all_elements_array_expansion_spans_with_escaped_templates(
        &word.parts,
        locator,
        shell_dialect,
        escaped_templates,
        spans,
    );
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct EscapedParameterTemplateBody {
    pub span: Span,
    pub contains_nested_parameter: bool,
}

pub(crate) fn escaped_parameter_template_bodies(
    word_span: Span,
    source: &str,
) -> SmallVec<[EscapedParameterTemplateBody; 2]> {
    let mut bodies = SmallVec::new();
    if word_span.start.offset() >= word_span.end.offset() || word_span.end.offset() > source.len() {
        return bodies;
    }

    let text = word_span.slice(source);
    if !text.contains("\\${") {
        return bodies;
    }

    let mut index = 0usize;
    while index < text.len() {
        if text[index..].starts_with("\\${") {
            let dollar_offset = index + '\\'.len_utf8();
            if offset_is_backslash_escaped(word_span.start.offset() + dollar_offset, source)
                && let Some(end_offset) = escaped_parameter_template_end(text, dollar_offset)
            {
                let body_start = dollar_offset + "${".len();
                let body_end = end_offset.saturating_sub('}'.len_utf8());
                if body_start < body_end {
                    let span = Span::from_positions(
                        word_span.start.advanced_by(&text[..body_start]),
                        word_span.start.advanced_by(&text[..body_end]),
                    );
                    bodies.push(EscapedParameterTemplateBody {
                        span,
                        contains_nested_parameter: text[body_start..body_end].contains("${"),
                    });
                }
                index = end_offset;
                continue;
            }
        }

        let Some(ch) = text[index..].chars().next() else {
            break;
        };
        index += ch.len_utf8();
    }

    bodies
}

pub(crate) fn span_start_inside_escaped_parameter_template(
    span: Span,
    bodies: &[EscapedParameterTemplateBody],
) -> bool {
    bodies
        .iter()
        .copied()
        .any(|body| body.contains(span.start.offset()))
}

fn collect_direct_all_elements_array_expansion_spans_with_escaped_templates(
    parts: &[WordPartNode],
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
    escaped_templates: &[EscapedParameterTemplateBody],
    spans: &mut Vec<Span>,
) {
    let source = locator.source();
    for part in parts {
        if span_start_inside_escaped_parameter_template(part.span, escaped_templates) {
            continue;
        }
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_direct_all_elements_array_expansion_spans_with_escaped_templates(
                    parts,
                    locator,
                    shell_dialect,
                    escaped_templates,
                    spans,
                )
            }
            _ if part_uses_direct_all_elements_array_expansion(&part.kind, shell_dialect) => {
                if let Some(span) = normalize_direct_all_elements_array_expansion_span(
                    part.span,
                    locator,
                    shell_dialect,
                ) {
                    spans.push(span);
                }
            }
            WordPart::Parameter(parameter)
                if parameter_might_use_all_elements_array_expansion(
                    parameter,
                    part.span,
                    source,
                    shell_dialect,
                ) =>
            {
                if let Some(span) = normalize_nested_direct_all_elements_array_expansion_span(
                    part.span,
                    locator,
                    shell_dialect,
                ) {
                    spans.push(span);
                }
            }
            _ => {}
        }
    }
}

impl EscapedParameterTemplateBody {
    fn contains(self, offset: usize) -> bool {
        self.span.start.offset() <= offset && offset < self.span.end.offset()
    }
}

pub(crate) fn escaped_parameter_template_end(text: &str, dollar_offset: usize) -> Option<usize> {
    if dollar_offset >= text.len() || !text[dollar_offset..].starts_with("${") {
        return None;
    }

    let bytes = text.as_bytes();
    let mut index = dollar_offset + "${".len();
    let mut depth = 1usize;
    let mut quote_state = EscapedTemplateQuote::None;

    while index < bytes.len() {
        let byte = bytes[index];
        match quote_state {
            EscapedTemplateQuote::Single => {
                if byte == b'\'' {
                    quote_state = EscapedTemplateQuote::None;
                }
                index += 1;
                continue;
            }
            EscapedTemplateQuote::Double => {
                if byte == b'\\' {
                    index += usize::from(index + 1 < bytes.len()) + 1;
                    continue;
                }
                if byte == b'"' {
                    quote_state = EscapedTemplateQuote::None;
                }
                index += 1;
                continue;
            }
            EscapedTemplateQuote::None => {}
        }

        match byte {
            b'\\' => {
                index += usize::from(index + 1 < bytes.len()) + 1;
            }
            b'\'' => {
                quote_state = EscapedTemplateQuote::Single;
                index += 1;
            }
            b'"' => {
                quote_state = EscapedTemplateQuote::Double;
                index += 1;
            }
            b'$' if bytes.get(index + 1) == Some(&b'{') => {
                depth += 1;
                index += "${".len();
            }
            b'}' => {
                depth -= 1;
                index += '}'.len_utf8();
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => index = advance_shell_char(text, index),
        }
    }

    None
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum EscapedTemplateQuote {
    None,
    Single,
    Double,
}

#[cfg(test)]
pub(crate) fn collect_quoted_unindexed_bash_source_spans(
    parts: &[WordPartNode],
    quoted: bool,
    source: &str,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_quoted_unindexed_bash_source_spans(parts, true, source, spans)
            }
            WordPart::Variable(name)
                if quoted
                    && name.as_str() == "BASH_SOURCE"
                    && !span_is_escaped(part.span, source) =>
            {
                spans.push(part.span);
            }
            WordPart::Parameter(parameter)
                if quoted
                    && parameter_is_unindexed_bash_source(parameter)
                    && !span_is_escaped(part.span, source) =>
            {
                spans.push(part.span);
            }
            _ => {}
        }
    }
}

pub(crate) fn normalize_all_elements_array_expansion_span(
    span: Span,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    let text = span.slice(source);
    if text == "$@"
        && source_starts_with_non_at_selector_subscript(&source[span.end.offset()..], shell_dialect)
    {
        return None;
    }
    if !span_is_escaped(span, source)
        && (text == "$@" || candidate_is_all_elements_array_expansion(text, shell_dialect))
    {
        return Some(span);
    }

    let base_offset = span.start.offset();
    let mut search_from = 0usize;

    while let Some(found) = text[search_from..].find('$') {
        let relative_start = search_from + found;
        let absolute_start = base_offset + relative_start;
        if offset_is_backslash_escaped(absolute_start, source) {
            search_from = relative_start + 1;
            continue;
        }

        let start = locator.position_at_offset(absolute_start)?;
        let remainder = &source[absolute_start..];

        if let Some(after_at) = remainder.strip_prefix("$@") {
            if source_starts_with_non_at_selector_subscript(after_at, shell_dialect) {
                search_from = relative_start + 1;
                continue;
            }
            let end = locator.position_at_offset(absolute_start + "$@".len())?;
            return Some(Span::from_positions(start, end));
        }

        if remainder.starts_with("${")
            && let Some(relative_end) = remainder.find('}')
        {
            let candidate = &remainder[..=relative_end];
            if candidate_is_all_elements_array_expansion(candidate, shell_dialect) {
                let end = locator.position_at_offset(absolute_start + candidate.len())?;
                return Some(Span::from_positions(start, end));
            }
        }

        search_from = relative_start + 1;
    }

    widen_all_elements_array_expansion_span(span, locator, shell_dialect)
}

pub(crate) fn normalize_direct_all_elements_array_expansion_span(
    span: Span,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    let text = span.slice(source);
    if text == "$@"
        && source_starts_with_non_at_selector_subscript(&source[span.end.offset()..], shell_dialect)
    {
        return None;
    }
    if !span_is_escaped(span, source)
        && (text == "$@" || candidate_is_direct_all_elements_array_expansion(text, shell_dialect))
    {
        return Some(span);
    }

    let base_offset = span.start.offset();
    let mut search_from = 0usize;

    while let Some(found) = text[search_from..].find('$') {
        let relative_start = search_from + found;
        let absolute_start = base_offset + relative_start;
        if offset_is_backslash_escaped(absolute_start, source) {
            search_from = relative_start + 1;
            continue;
        }

        let start = locator.position_at_offset(absolute_start)?;
        let remainder = &source[absolute_start..];

        if let Some(after_at) = remainder.strip_prefix("$@") {
            if source_starts_with_non_at_selector_subscript(after_at, shell_dialect) {
                search_from = relative_start + 1;
                continue;
            }
            let end = locator.position_at_offset(absolute_start + "$@".len())?;
            return Some(Span::from_positions(start, end));
        }

        if remainder.starts_with("${")
            && let Some(relative_end) = remainder.find('}')
        {
            let candidate = &remainder[..=relative_end];
            if candidate_is_direct_all_elements_array_expansion(candidate, shell_dialect) {
                let end = locator.position_at_offset(absolute_start + candidate.len())?;
                return Some(Span::from_positions(start, end));
            }
        }

        search_from = relative_start + 1;
    }

    widen_direct_all_elements_array_expansion_span(span, locator, shell_dialect)
}

pub(crate) fn normalize_nested_direct_all_elements_array_expansion_span(
    span: Span,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    #[derive(Clone, Copy, PartialEq, Eq)]
    enum QuoteState {
        None,
        Single,
        Double,
    }

    let text = span.slice(source);
    if !text.contains('$') {
        return None;
    }

    let base_offset = span.start.offset();
    let bytes = text.as_bytes();
    let mut index = 0usize;
    let mut nested_braced_depth = 0usize;
    let mut quote_state = QuoteState::None;

    while index < bytes.len() {
        let absolute_start = base_offset + index;
        let byte = bytes[index];

        match quote_state {
            QuoteState::Single if nested_braced_depth > 0 => {
                if byte == b'\'' {
                    quote_state = QuoteState::None;
                }
                index += 1;
                continue;
            }
            QuoteState::Double if nested_braced_depth > 0 => {
                if byte == b'\\' {
                    index += usize::from(index + 1 < bytes.len()) + 1;
                    continue;
                }
                if byte == b'"' {
                    quote_state = QuoteState::None;
                }
                index += 1;
                continue;
            }
            QuoteState::None if nested_braced_depth > 0 && byte == b'\'' => {
                quote_state = QuoteState::Single;
                index += 1;
                continue;
            }
            QuoteState::None if nested_braced_depth > 0 && byte == b'"' => {
                quote_state = QuoteState::Double;
                index += 1;
                continue;
            }
            QuoteState::None => {}
            QuoteState::Single | QuoteState::Double => {}
        }

        if byte == b'\\' {
            if index + 2 < bytes.len() && bytes[index + 1] == b'$' && bytes[index + 2] == b'{' {
                nested_braced_depth += 1;
                index += 3;
                continue;
            }

            index += usize::from(index + 1 < bytes.len()) + 1;
            continue;
        }

        if byte == b'}' && nested_braced_depth > 0 {
            nested_braced_depth -= 1;
            index += 1;
            continue;
        }

        if byte != b'$' {
            if byte == b'{' && nested_braced_depth > 0 {
                nested_braced_depth += 1;
            }
            index += 1;
            continue;
        }

        if offset_is_backslash_escaped(absolute_start, source) {
            index += 1;
            continue;
        }

        let remainder = &source[absolute_start..];
        if nested_braced_depth == 0
            && let Some(after_at) = remainder.strip_prefix("$@")
        {
            if source_starts_with_non_at_selector_subscript(after_at, shell_dialect) {
                index += 1;
                continue;
            }
            let start = locator.position_at_offset(absolute_start)?;
            let end = locator.position_at_offset(absolute_start + "$@".len())?;
            return Some(Span::from_positions(start, end));
        }

        if remainder.starts_with("${") {
            if nested_braced_depth == 0
                && let Some(relative_end) = remainder.find('}')
            {
                let candidate = &remainder[..=relative_end];
                if candidate_is_direct_all_elements_array_expansion(candidate, shell_dialect) {
                    let start = locator.position_at_offset(absolute_start)?;
                    let end = locator.position_at_offset(absolute_start + candidate.len())?;
                    return Some(Span::from_positions(start, end));
                }
            }

            nested_braced_depth += 1;
            index += 2;
            continue;
        }

        index += 1;
    }

    None
}

pub(crate) fn widen_all_elements_array_expansion_span(
    span: Span,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    let text = span.slice(source);
    if !text.contains("[@]") {
        return None;
    }

    let start_offset = span.start.offset().checked_sub(2)?;
    if source.as_bytes().get(start_offset..span.start.offset())? != b"${" {
        return None;
    }
    if offset_is_backslash_escaped(start_offset, source) {
        return None;
    }

    let start = locator.position_at_offset(start_offset)?;
    let remainder = &source[start_offset..];
    let relative_end = remainder.find('}')?;
    let candidate = &remainder[..=relative_end];
    if !candidate_is_all_elements_array_expansion(candidate, shell_dialect) {
        return None;
    }

    let end = locator.position_at_offset(start_offset + candidate.len())?;
    Some(Span::from_positions(start, end))
}

pub(crate) fn widen_direct_all_elements_array_expansion_span(
    span: Span,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<Span> {
    let source = locator.source();
    let text = span.slice(source);
    if !text.contains("[@]") {
        return None;
    }

    let start_offset = span.start.offset().checked_sub(2)?;
    if source.as_bytes().get(start_offset..span.start.offset())? != b"${" {
        return None;
    }
    if offset_is_backslash_escaped(start_offset, source) {
        return None;
    }

    let start = locator.position_at_offset(start_offset)?;
    let remainder = &source[start_offset..];
    let relative_end = remainder.find('}')?;
    let candidate = &remainder[..=relative_end];
    if !candidate_is_direct_all_elements_array_expansion(candidate, shell_dialect) {
        return None;
    }

    let end = locator.position_at_offset(start_offset + candidate.len())?;
    Some(Span::from_positions(start, end))
}

pub(crate) fn candidate_is_all_elements_array_expansion(
    candidate: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let Some(inner) = candidate
        .strip_prefix("${")
        .and_then(|text| text.strip_suffix('}'))
    else {
        return false;
    };

    let (inner, indirect_like) = inner
        .strip_prefix('!')
        .map_or((inner, false), |stripped| (stripped, true));

    let Some(first) = inner.as_bytes().first().copied() else {
        return false;
    };

    if first == b'@' {
        return !indirect_like && strip_special_at_splat_suffix(inner, shell_dialect).is_some();
    }

    if !is_name_start(first) {
        return false;
    }

    let bytes = inner.as_bytes();
    let mut index = 1usize;
    while index < bytes.len() && is_name_continue(bytes[index]) {
        index += 1;
    }

    if inner[index..].starts_with("[@]") {
        return true;
    }

    indirect_like && inner[index..].starts_with('@')
}

pub(crate) fn candidate_is_direct_all_elements_array_expansion(
    candidate: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let Some(mut inner) = candidate
        .strip_prefix("${")
        .and_then(|text| text.strip_suffix('}'))
    else {
        return false;
    };

    if let Some(stripped) = inner.strip_prefix('!') {
        inner = stripped;
    }

    let suffix = if inner.starts_with('@') {
        let Some(stripped) = strip_special_at_splat_suffix(inner, shell_dialect) else {
            return false;
        };
        stripped
    } else {
        let Some(first) = inner.as_bytes().first().copied() else {
            return false;
        };
        if !is_name_start(first) {
            return false;
        }

        let bytes = inner.as_bytes();
        let mut index = 1usize;
        while index < bytes.len() && is_name_continue(bytes[index]) {
            index += 1;
        }

        let Some(stripped) = inner[index..].strip_prefix("[@]") else {
            return false;
        };
        stripped
    };

    if suffix.starts_with('+') || suffix.starts_with(":+") {
        return false;
    }

    true
}

fn strip_special_at_splat_suffix(
    inner: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Option<&str> {
    let suffix = inner.strip_prefix('@')?;

    if let Some(stripped) = strip_zsh_positional_selector_suffix(suffix) {
        return Some(stripped);
    }

    if shell_dialect == shuck_semantic::ShellDialect::Zsh && suffix.starts_with('[') {
        return None;
    }

    Some(suffix)
}

fn strip_zsh_positional_selector_suffix(suffix: &str) -> Option<&str> {
    let rest = suffix.strip_prefix('[')?;
    let close = rest.find(']')?;
    match &rest[..close] {
        "@" | "*" => Some(&rest[close + 1..]),
        _ => None,
    }
}

fn source_starts_with_non_at_selector_subscript(
    text: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    if shell_dialect != shuck_semantic::ShellDialect::Zsh {
        return false;
    }

    let Some(rest) = text.strip_prefix('[') else {
        return false;
    };
    let Some(close) = rest.find(']') else {
        return false;
    };

    !matches!(&rest[..close], "@" | "*")
}

fn part_has_zsh_short_positional_subscript(
    parts: &[WordPartNode],
    index: usize,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    shell_dialect == shuck_semantic::ShellDialect::Zsh
        && parts.get(index).is_some_and(
            |part| matches!(&part.kind, WordPart::Variable(name) if name.as_str() == "@"),
        )
        && parts.get(index + 1).is_some_and(|next| {
            matches!(
                &next.kind,
                WordPart::Literal(text)
                    if literal_starts_with_zsh_subscript(text.as_str(source, next.span))
            )
        })
}

fn literal_starts_with_zsh_subscript(text: &str) -> bool {
    let Some(rest) = text.strip_prefix('[') else {
        return false;
    };
    rest.contains(']')
}

fn var_ref_is_zsh_indexed_positional_at(
    reference: &VarRef,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    shell_dialect == shuck_semantic::ShellDialect::Zsh
        && reference.name.as_str() == "@"
        && reference
            .subscript
            .as_deref()
            .is_some_and(|subscript| subscript.selector().is_none())
}

fn var_ref_is_zsh_ranged_positional_at(
    reference: &VarRef,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    shell_dialect == shuck_semantic::ShellDialect::Zsh
        && reference.name.as_str() == "@"
        && reference.subscript.as_deref().is_some_and(|subscript| {
            subscript.selector().is_none() && subscript.syntax_text(source).contains(',')
        })
}

fn parameter_uses_zsh_ranged_positional_at(
    parameter: &ParameterExpansion,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Indices { reference }
        | BourneParameterExpansion::Slice { reference, .. } => {
            var_ref_is_zsh_ranged_positional_at(reference, source, shell_dialect)
        }
        BourneParameterExpansion::Operation {
            reference,
            operator,
            ..
        } => {
            !matches!(operator.as_ref(), ParameterOp::UseReplacement)
                && var_ref_is_zsh_ranged_positional_at(reference, source, shell_dialect)
        }
        BourneParameterExpansion::Transformation { reference, .. } => {
            var_ref_is_zsh_ranged_positional_at(reference, source, shell_dialect)
        }
        _ => false,
    }
}

fn part_has_zsh_short_positional_range(
    parts: &[WordPartNode],
    index: usize,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    shell_dialect == shuck_semantic::ShellDialect::Zsh
        && parts.get(index).is_some_and(
            |part| matches!(&part.kind, WordPart::Variable(name) if name.as_str() == "@"),
        )
        && parts.get(index + 1).is_some_and(|next| {
            matches!(
                &next.kind,
                WordPart::Literal(text)
                    if literal_starts_with_zsh_positional_range(text.as_str(source, next.span))
            )
        })
}

fn literal_starts_with_zsh_positional_range(text: &str) -> bool {
    let Some(rest) = text.strip_prefix('[') else {
        return false;
    };
    let Some(close) = rest.find(']') else {
        return false;
    };

    rest[..close].contains(',')
}

pub fn word_zsh_positional_parameter_range_spans(
    word: &Word,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> Vec<Span> {
    let mut spans = Vec::new();
    collect_zsh_positional_parameter_range_spans(&word.parts, source, shell_dialect, &mut spans);
    spans
}

fn collect_zsh_positional_parameter_range_spans(
    parts: &[WordPartNode],
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
    spans: &mut Vec<Span>,
) {
    if shell_dialect != shuck_semantic::ShellDialect::Zsh {
        return;
    }

    for (index, part) in parts.iter().enumerate() {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_zsh_positional_parameter_range_spans(parts, source, shell_dialect, spans);
            }
            WordPart::Parameter(parameter)
                if parameter_uses_zsh_ranged_positional_at(parameter, source, shell_dialect) =>
            {
                spans.push(part.span);
            }
            WordPart::Variable(_)
                if part_has_zsh_short_positional_range(parts, index, source, shell_dialect) =>
            {
                spans.push(part.span);
            }
            _ => {}
        }
    }
}

pub(crate) fn parameter_is_array_like(parameter: &ParameterExpansion) -> bool {
    match &parameter.syntax {
        ParameterExpansionSyntax::Bourne(syntax) => match syntax {
            BourneParameterExpansion::Access { reference } => reference.has_array_selector(),
            BourneParameterExpansion::Indices { .. } => true,
            BourneParameterExpansion::Slice { reference, .. } => reference.has_array_selector(),
            BourneParameterExpansion::Operation {
                reference,
                operator,
                ..
            } => {
                !matches!(operator.as_ref(), ParameterOp::UseReplacement)
                    && reference.has_array_selector()
            }
            BourneParameterExpansion::Transformation { reference, .. } => {
                reference.has_array_selector()
            }
            _ => false,
        },
        ParameterExpansionSyntax::Zsh(_) => false,
    }
}

pub(crate) fn parameter_might_use_all_elements_array_expansion(
    parameter: &ParameterExpansion,
    span: Span,
    source: &str,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    if !span.slice(source).contains('@') {
        return false;
    }

    match &parameter.syntax {
        ParameterExpansionSyntax::Bourne(syntax) => match syntax {
            BourneParameterExpansion::Access { reference }
            | BourneParameterExpansion::Indices { reference }
            | BourneParameterExpansion::Slice { reference, .. }
            | BourneParameterExpansion::Operation { reference, .. }
            | BourneParameterExpansion::Transformation { reference, .. } => {
                !var_ref_is_zsh_indexed_positional_at(reference, shell_dialect)
            }
            BourneParameterExpansion::Length { .. } | BourneParameterExpansion::Indirect { .. } => {
                false
            }
            BourneParameterExpansion::PrefixMatch { kind, .. } => {
                matches!(kind, PrefixMatchKind::At)
            }
        },
        ParameterExpansionSyntax::Zsh(syntax) => match &syntax.target {
            ZshExpansionTarget::Reference(reference) => {
                !var_ref_is_zsh_indexed_positional_at(reference, shell_dialect)
            }
            ZshExpansionTarget::Nested(_)
            | ZshExpansionTarget::Word(_)
            | ZshExpansionTarget::Empty => true,
        },
    }
}

pub(crate) fn parameter_is_scalar_like(parameter: &ParameterExpansion) -> bool {
    match &parameter.syntax {
        ParameterExpansionSyntax::Bourne(syntax) => match syntax {
            BourneParameterExpansion::Access { reference } => !reference.has_array_selector(),
            BourneParameterExpansion::Length { .. }
            | BourneParameterExpansion::PrefixMatch { .. } => true,
            BourneParameterExpansion::Indirect { reference, .. }
            | BourneParameterExpansion::Operation { reference, .. }
            | BourneParameterExpansion::Transformation { reference, .. } => {
                !reference.has_array_selector()
            }
            BourneParameterExpansion::Indices { .. } => false,
            BourneParameterExpansion::Slice { reference, .. } => !reference.has_array_selector(),
        },
        ParameterExpansionSyntax::Zsh(_) => true,
    }
}

pub(crate) fn parameter_uses_replacement_operator(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Indirect {
            operator: Some(operator),
            ..
        }
        | BourneParameterExpansion::Operation { operator, .. } => {
            matches!(operator.as_ref(), ParameterOp::UseReplacement)
        }
        BourneParameterExpansion::Access { .. }
        | BourneParameterExpansion::Length { .. }
        | BourneParameterExpansion::Indices { .. }
        | BourneParameterExpansion::PrefixMatch { .. }
        | BourneParameterExpansion::Slice { .. }
        | BourneParameterExpansion::Transformation { .. }
        | BourneParameterExpansion::Indirect { operator: None, .. } => false,
    }
}

pub(crate) fn part_uses_star_splat(part: &WordPart) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == "*",
        WordPart::ArrayAccess(reference) => var_ref_uses_star_splat(reference),
        WordPart::Parameter(parameter) => parameter_uses_star_splat(parameter),
        WordPart::ParameterExpansion { reference, .. }
        | WordPart::IndirectExpansion { reference, .. }
        | WordPart::Transformation { reference, .. } => var_ref_uses_star_splat(reference),
        _ => false,
    }
}

pub(crate) fn part_uses_all_elements_array_slice(part: &WordPart) -> bool {
    match part {
        WordPart::ArraySlice { reference, .. } => var_ref_uses_all_elements_at_splat(reference),
        WordPart::Parameter(parameter) => parameter_uses_all_elements_array_slice(parameter),
        _ => false,
    }
}

pub(crate) fn part_uses_positional_at_splat(part: &WordPart) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == "@",
        WordPart::ArrayAccess(reference) => var_ref_uses_positional_at_splat(reference),
        WordPart::Parameter(parameter) => parameter_uses_positional_at_splat(parameter),
        _ => false,
    }
}

pub(crate) fn part_uses_unquoted_all_elements_array_expansion_in_dialect(
    part: &WordPart,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == "@",
        WordPart::ArrayAccess(reference) | WordPart::ArrayIndices(reference) => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        WordPart::ArraySlice { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        WordPart::Parameter(parameter) => {
            parameter_uses_unquoted_all_elements_array_expansion(parameter, shell_dialect)
        }
        _ => false,
    }
}

pub(crate) fn part_uses_direct_all_elements_array_expansion(
    part: &WordPart,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == "@",
        WordPart::ArrayAccess(reference) | WordPart::ArrayIndices(reference) => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        WordPart::ArraySlice { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        WordPart::Parameter(parameter) => {
            parameter_uses_direct_all_elements_array_expansion(parameter, shell_dialect)
        }
        _ => false,
    }
}

pub(crate) fn part_is_pure_positional_at_splat(part: &WordPart) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == "@",
        WordPart::ArrayAccess(reference) => var_ref_uses_positional_at_splat(reference),
        WordPart::Parameter(parameter) => parameter_is_pure_positional_at_splat(parameter),
        _ => false,
    }
}

pub(crate) fn part_uses_assign_default_operator(part: &WordPart) -> bool {
    match part {
        WordPart::Parameter(parameter) => parameter_uses_assign_default_operator(parameter),
        WordPart::ParameterExpansion { operator, .. }
        | WordPart::IndirectExpansion {
            operator: Some(operator),
            ..
        } => matches!(operator.as_ref(), ParameterOp::AssignDefault),
        _ => false,
    }
}

pub(crate) fn var_ref_uses_star_splat(reference: &VarRef) -> bool {
    reference.name.as_str() == "*"
        || matches!(
            reference
                .subscript
                .as_ref()
                .and_then(|subscript| subscript.selector()),
            Some(SubscriptSelector::Star)
        )
}

pub(crate) fn var_ref_uses_all_elements_at_splat(reference: &VarRef) -> bool {
    reference.name.as_str() == "@"
        || matches!(
            reference
                .subscript
                .as_ref()
                .and_then(|subscript| subscript.selector()),
            Some(SubscriptSelector::At)
        )
}

pub(crate) fn var_ref_uses_all_elements_at_splat_in_dialect(
    reference: &VarRef,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    (reference.name.as_str() == "@"
        && !var_ref_is_zsh_indexed_positional_at(reference, shell_dialect))
        || matches!(
            reference
                .subscript
                .as_ref()
                .and_then(|subscript| subscript.selector()),
            Some(SubscriptSelector::At)
        )
}

pub(crate) fn parameter_uses_all_elements_array_slice(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    matches!(
        syntax,
        BourneParameterExpansion::Slice { reference, .. }
            if var_ref_uses_all_elements_at_splat(reference)
    )
}

pub(crate) fn parameter_uses_unquoted_all_elements_array_expansion(
    parameter: &ParameterExpansion,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Indices { reference }
        | BourneParameterExpansion::Slice { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        BourneParameterExpansion::Operation {
            reference,
            operator,
            ..
        } => {
            !matches!(operator.as_ref(), ParameterOp::UseReplacement)
                && var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        BourneParameterExpansion::Transformation { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        _ => false,
    }
}

pub(crate) fn parameter_uses_direct_all_elements_array_expansion(
    parameter: &ParameterExpansion,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Indices { reference }
        | BourneParameterExpansion::Slice { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        BourneParameterExpansion::Operation {
            reference,
            operator,
            ..
        } => {
            !matches!(operator.as_ref(), ParameterOp::UseReplacement)
                && var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        BourneParameterExpansion::Transformation { reference, .. } => {
            var_ref_uses_all_elements_at_splat_in_dialect(reference, shell_dialect)
        }
        _ => false,
    }
}

pub(crate) fn parameter_uses_replacement_all_elements_array_expansion(
    parameter: &ParameterExpansion,
) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    matches!(
        syntax,
        BourneParameterExpansion::Operation {
            reference,
            operator,
            ..
        } if matches!(operator.as_ref(), ParameterOp::UseReplacement)
            && var_ref_uses_all_elements_at_splat(reference)
    )
}

#[cfg(test)]
pub(crate) fn parameter_is_unindexed_bash_source(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    matches!(
        syntax,
        BourneParameterExpansion::Access { reference }
            if reference.name.as_str() == "BASH_SOURCE" && reference.subscript.is_none()
    )
}

pub(crate) fn parameter_uses_star_splat(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Slice { reference, .. }
        | BourneParameterExpansion::Operation { reference, .. }
        | BourneParameterExpansion::Transformation { reference, .. } => {
            var_ref_uses_star_splat(reference)
        }
        _ => false,
    }
}

pub(crate) fn var_ref_uses_positional_at_splat(reference: &VarRef) -> bool {
    reference.name.as_str() == "@"
}

pub(crate) fn parameter_uses_positional_at_splat(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Slice { reference, .. }
        | BourneParameterExpansion::Operation { reference, .. } => {
            var_ref_uses_positional_at_splat(reference)
        }
        _ => false,
    }
}

pub(crate) fn parameter_is_pure_positional_at_splat(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Slice { reference, .. } => {
            var_ref_uses_positional_at_splat(reference)
        }
        _ => false,
    }
}

pub(crate) fn parameter_uses_assign_default_operator(parameter: &ParameterExpansion) -> bool {
    let ParameterExpansionSyntax::Bourne(syntax) = &parameter.syntax else {
        return false;
    };

    match syntax {
        BourneParameterExpansion::Operation { operator, .. } => {
            matches!(operator.as_ref(), ParameterOp::AssignDefault)
        }
        BourneParameterExpansion::Indirect {
            operator: Some(operator),
            ..
        } => matches!(operator.as_ref(), ParameterOp::AssignDefault),
        _ => false,
    }
}

pub(crate) fn collect_unquoted_star_splat_spans(
    parts: &[WordPartNode],
    quoted: bool,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_unquoted_star_splat_spans(parts, true, spans);
            }
            _ if !quoted && part_uses_star_splat(&part.kind) => spans.push(part.span),
            _ => {}
        }
    }
}

pub(crate) fn collect_quoted_star_splat_spans(
    parts: &[WordPartNode],
    quoted: bool,
    spans: &mut Vec<Span>,
) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => {
                collect_quoted_star_splat_spans(parts, true, spans);
            }
            _ if quoted && part_uses_star_splat(&part.kind) => spans.push(part.span),
            _ => {}
        }
    }
}

pub(crate) fn collect_positional_at_splat_spans(parts: &[WordPartNode], spans: &mut Vec<Span>) {
    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => {}
            WordPart::DoubleQuoted { parts, .. } => collect_positional_at_splat_spans(parts, spans),
            _ if part_uses_positional_at_splat(&part.kind) => spans.push(part.span),
            _ => {}
        }
    }
}

pub(crate) fn parts_are_pure_positional_at_splat(parts: &[WordPartNode]) -> bool {
    let mut saw_splat = false;

    for part in parts {
        match &part.kind {
            WordPart::SingleQuoted { .. } => return false,
            WordPart::DoubleQuoted { parts, .. } => {
                if !parts_are_pure_positional_at_splat(parts) {
                    return false;
                }
                saw_splat = true;
            }
            _ if part_is_pure_positional_at_splat(&part.kind) => saw_splat = true,
            _ => return false,
        }
    }

    saw_splat
}

#[cfg(test)]
pub(crate) fn word_has_single_positional_at_splat_part(word: &Word) -> bool {
    parts_have_single_positional_at_splat(&word.parts)
}

#[cfg(test)]
pub(crate) fn parts_have_single_positional_at_splat(parts: &[WordPartNode]) -> bool {
    let [part] = parts else {
        return false;
    };

    match &part.kind {
        WordPart::DoubleQuoted { parts, .. } => parts_have_single_positional_at_splat(parts),
        WordPart::SingleQuoted { .. } => false,
        _ => part_uses_positional_at_splat(&part.kind),
    }
}

pub(crate) fn word_has_single_folded_all_elements_array_part_in_dialect(
    word: &Word,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    parts_have_single_folded_all_elements_array_part(&word.parts, shell_dialect)
}

pub(crate) fn parts_have_single_folded_all_elements_array_part(
    parts: &[WordPartNode],
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    let [part] = parts else {
        return false;
    };

    match &part.kind {
        WordPart::DoubleQuoted { parts, .. } => {
            parts_have_single_folded_all_elements_array_part(parts, shell_dialect)
        }
        WordPart::SingleQuoted { .. } => false,
        WordPart::Parameter(parameter) => {
            part_uses_direct_all_elements_array_expansion(&part.kind, shell_dialect)
                || parameter_uses_replacement_all_elements_array_expansion(parameter)
        }
        _ => part_uses_direct_all_elements_array_expansion(&part.kind, shell_dialect),
    }
}

#[cfg(test)]
pub(crate) fn positional_at_splat_is_standalone_expansion(word: &Word, source: &str) -> bool {
    let text = word.span.slice(source);
    let body = if word.is_fully_double_quoted() {
        let Some(unquoted) = text
            .strip_prefix('"')
            .and_then(|value| value.strip_suffix('"'))
        else {
            return false;
        };
        unquoted
    } else {
        text
    };

    if body == "$@" || body == "${@}" {
        return true;
    }

    if !body.starts_with("${@") || !body.ends_with('}') {
        return false;
    }
    true
}

pub(crate) fn all_elements_array_expansion_is_standalone(
    word: &Word,
    locator: Locator<'_>,
    shell_dialect: shuck_semantic::ShellDialect,
) -> bool {
    if word.parts.len() != 1 {
        return false;
    }

    let source = locator.source();
    let text = word.span.slice(source);
    let body = if word.is_fully_double_quoted() {
        let Some(unquoted) = text
            .strip_prefix('"')
            .and_then(|value| value.strip_suffix('"'))
        else {
            return false;
        };
        unquoted
    } else {
        text
    };

    folded_all_elements_array_candidate_spans(word, locator, shell_dialect)
        .first()
        .is_some_and(|span| span.slice(source) == body)
}

#[cfg(test)]
mod tests {
    use shuck_ast::{Span, Word, WordPart, WordPartNode};
    use shuck_indexer::LineIndex;
    use shuck_parser::parser::Parser;
    use shuck_semantic::ShellDialect;

    use super::{
        collect_all_elements_array_slice_spans, collect_array_expansion_part_spans,
        collect_unquoted_all_elements_array_expansion_part_spans,
        collect_unquoted_array_expansion_part_spans, span_is_escaped,
        word_has_quoted_all_elements_array_slice, word_has_single_positional_at_splat_part,
        word_is_pure_positional_at_splat, word_positional_at_splat_spans,
        word_positional_at_splat_spans_in_source, word_quoted_all_elements_array_slice_spans,
        word_quoted_star_splat_spans, word_quoted_unindexed_bash_source_span_in_source,
        word_unquoted_star_parameter_spans, word_unquoted_star_splat_spans,
    };
    use crate::Locator;
    use crate::facts::word_spans::parameter_is_scalar_like;
    use crate::facts::words::{
        WordSubtreeVisitor, WordTraversalContext, WordTraversalState, walk_word_subtree,
    };

    fn all_elements_array_expansion_part_spans(word: &Word, source: &str) -> Vec<Span> {
        all_elements_array_expansion_part_spans_with_dialect(word, source, ShellDialect::Bash)
    }

    fn all_elements_array_expansion_part_spans_with_dialect(
        word: &Word,
        source: &str,
        shell_dialect: ShellDialect,
    ) -> Vec<Span> {
        let line_index = LineIndex::new(source);
        let locator = Locator::new(source, &line_index);
        super::all_elements_array_expansion_part_spans(word, locator, shell_dialect)
    }

    fn word_folded_all_elements_array_span_in_source(word: &Word, source: &str) -> Option<Span> {
        word_folded_all_elements_array_span_in_source_with_dialect(word, source, ShellDialect::Bash)
    }

    fn word_folded_all_elements_array_span_in_source_with_dialect(
        word: &Word,
        source: &str,
        shell_dialect: ShellDialect,
    ) -> Option<Span> {
        let line_index = LineIndex::new(source);
        let locator = Locator::new(source, &line_index);
        super::word_folded_all_elements_array_span_in_source(word, locator, shell_dialect)
    }

    fn word_folded_positional_at_splat_span_in_source(word: &Word, source: &str) -> Option<Span> {
        super::word_folded_positional_at_splat_span_in_source(word, source)
    }

    fn word_has_direct_all_elements_array_expansion_in_source(word: &Word, source: &str) -> bool {
        word_has_direct_all_elements_array_expansion_in_source_with_dialect(
            word,
            source,
            ShellDialect::Bash,
        )
    }

    fn word_has_direct_all_elements_array_expansion_in_source_with_dialect(
        word: &Word,
        source: &str,
        shell_dialect: ShellDialect,
    ) -> bool {
        let line_index = LineIndex::new(source);
        let locator = Locator::new(source, &line_index);
        super::word_has_direct_all_elements_array_expansion_in_source(word, locator, shell_dialect)
    }

    fn array_expansion_part_spans(word: &Word, _source: &str) -> Vec<Span> {
        let mut spans = Vec::new();
        collect_array_expansion_part_spans(word, &mut spans);
        spans
    }

    fn unquoted_all_elements_array_expansion_part_spans(word: &Word, source: &str) -> Vec<Span> {
        unquoted_all_elements_array_expansion_part_spans_with_dialect(
            word,
            source,
            ShellDialect::Bash,
        )
    }

    fn unquoted_all_elements_array_expansion_part_spans_with_dialect(
        word: &Word,
        source: &str,
        shell_dialect: ShellDialect,
    ) -> Vec<Span> {
        let mut spans = Vec::new();
        collect_unquoted_all_elements_array_expansion_part_spans(
            word,
            source,
            shell_dialect,
            &mut spans,
        );
        spans
    }

    fn word_all_elements_array_slice_spans(word: &Word) -> Vec<Span> {
        let mut spans = Vec::new();
        collect_all_elements_array_slice_spans(&word.parts, false, false, &mut spans);
        spans
    }

    fn word_all_elements_array_slice_span_in_source(word: &Word, source: &str) -> Option<Span> {
        word_all_elements_array_slice_spans(word)
            .into_iter()
            .find(|span| !span_is_escaped(*span, source))
    }

    fn unquoted_array_expansion_part_spans(word: &Word, _source: &str) -> Vec<Span> {
        let mut spans = Vec::new();
        collect_unquoted_array_expansion_part_spans(word, &mut spans);
        spans
    }

    fn word_folded_positional_at_splat_span(word: &Word) -> Option<Span> {
        let spans = word_positional_at_splat_spans(word);
        if spans.is_empty() || (spans.len() == 1 && word_has_single_positional_at_splat_part(word))
        {
            return None;
        }

        spans.into_iter().next()
    }

    fn word_has_folded_positional_at_splat(word: &Word) -> bool {
        word_folded_positional_at_splat_span(word).is_some()
    }

    fn scalar_expansion_part_spans(word: &Word, _source: &str) -> Vec<Span> {
        let mut spans = Vec::new();
        let mut visitor = TestScalarExpansionSpanVisitor {
            spans: &mut spans,
            only_unquoted: false,
        };
        walk_word_subtree(
            word,
            WordTraversalContext {
                source: "",
                locator: None,
                shell_dialect: ShellDialect::Bash,
            },
            &mut visitor,
        );
        spans
    }

    struct TestScalarExpansionSpanVisitor<'spans> {
        spans: &'spans mut Vec<Span>,
        only_unquoted: bool,
    }

    impl<'a> WordSubtreeVisitor<'a> for TestScalarExpansionSpanVisitor<'_> {
        fn visit_part(&mut self, part: &'a WordPartNode, state: WordTraversalState<'a>) {
            if !state.processes_root_word() {
                return;
            }
            let quoted = state.in_double_quote;

            match &part.kind {
                WordPart::Literal(_)
                | WordPart::SingleQuoted { .. }
                | WordPart::DoubleQuoted { .. } => {}
                WordPart::ZshQualifiedGlob(_) => {}
                WordPart::CommandSubstitution { .. } | WordPart::ProcessSubstitution { .. } => {}
                WordPart::Parameter(parameter) => {
                    if parameter_is_scalar_like(parameter) && (!self.only_unquoted || !quoted) {
                        self.spans.push(part.span);
                    }
                }
                WordPart::Variable(name) if matches!(name.as_str(), "@" | "*") => {}
                WordPart::Variable(_)
                | WordPart::ArithmeticExpansion { .. }
                | WordPart::Length(_)
                | WordPart::ArrayLength(_)
                | WordPart::Substring { .. }
                | WordPart::PrefixMatch { .. } => {
                    if !self.only_unquoted || !quoted {
                        self.spans.push(part.span);
                    }
                }
                WordPart::ParameterExpansion { reference, .. }
                | WordPart::IndirectExpansion { reference, .. }
                | WordPart::Transformation { reference, .. } => {
                    if !reference.has_array_selector() && (!self.only_unquoted || !quoted) {
                        self.spans.push(part.span);
                    }
                }
                WordPart::ArrayAccess(reference) => {
                    if !reference.has_array_selector() && (!self.only_unquoted || !quoted) {
                        self.spans.push(part.span);
                    }
                }
                WordPart::ArrayIndices(_) | WordPart::ArraySlice { .. } => {}
            }
        }
    }

    #[test]
    fn array_expansion_spans_only_return_array_like_parts() {
        let source = "printf '%s\\n' ${arr[@]} ${arr[@]+fallback} ${arr[*]:-fallback} ${arr[*]@Q} ${arr[0]}\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .skip(1)
            .flat_map(|word| array_expansion_part_spans(word, source))
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();
        assert_eq!(
            spans,
            vec!["${arr[@]}", "${arr[*]:-fallback}", "${arr[*]@Q}"]
        );
    }

    #[test]
    fn selector_helpers_distinguish_splats_from_indexed_and_quoted_keys() {
        let source = "printf '%s\\n' ${arr[@]} ${arr[*]} ${arr[0]} ${assoc[\"key\"]}\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(command.args.len(), 5);
        assert_eq!(
            array_expansion_part_spans(&command.args[1], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]}"]
        );
        assert_eq!(
            array_expansion_part_spans(&command.args[2], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[*]}"]
        );
        assert!(array_expansion_part_spans(&command.args[3], source).is_empty());
        assert!(array_expansion_part_spans(&command.args[4], source).is_empty());

        assert!(scalar_expansion_part_spans(&command.args[1], source).is_empty());
        assert!(scalar_expansion_part_spans(&command.args[2], source).is_empty());
        assert_eq!(
            scalar_expansion_part_spans(&command.args[3], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[0]}"]
        );
        assert_eq!(
            scalar_expansion_part_spans(&command.args[4], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${assoc[\"key\"]}"]
        );
    }

    #[test]
    fn all_elements_array_expansion_spans_only_return_at_style_parts() {
        let source =
            "printf '%s\\n' $@ $* \"$@\" \"$*\" ${arr[@]} ${arr[*]} ${arr[@]:1:2} ${arr[*]:1:2}\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[1], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$@"]
        );
        assert!(all_elements_array_expansion_part_spans(&command.args[2], source).is_empty());
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[3], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$@"]
        );
        assert!(all_elements_array_expansion_part_spans(&command.args[4], source).is_empty());
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[5], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]}"]
        );
        assert!(all_elements_array_expansion_part_spans(&command.args[6], source).is_empty());
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[7], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]:1:2}"]
        );
        assert!(all_elements_array_expansion_part_spans(&command.args[8], source).is_empty());
    }

    #[test]
    fn all_elements_array_expansion_spans_normalize_parser_misalignment() {
        let source = "\
#!/bin/bash
shims=(a)
eval \\
\"conda_shim() {
  case \\\"\\${1##*/}\\\" in
    ${shims[@]}
    *) return 1;;
  esac
}\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[1].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = all_elements_array_expansion_part_spans(&command.args[0], source);
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].slice(source), "${shims[@]}");
        assert_eq!(spans[0].start.column(), 5);
        assert_eq!(spans[0].end.column(), 16);
    }

    #[test]
    fn all_elements_array_expansion_spans_ignore_escaped_literal_expansions() {
        let source = "\
#!/bin/bash
eval command sudo \\\"\\${sudo_args[@]}\\\" \\\"\\$@\\\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(all_elements_array_expansion_part_spans(&command.args[2], source).is_empty());
    }

    #[test]
    fn all_elements_array_expansion_spans_track_safe_quoted_name_fanout() {
        let source = "\
printf '%s\\n' ${#arr[@]} ${!arr[@]} ${!cfg@} ${name:-safe[@]} ${arr[@]}
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(all_elements_array_expansion_part_spans(&command.args[1], source).is_empty());
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[2], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${!arr[@]}"]
        );
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[3], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${!cfg@}"]
        );
        assert!(all_elements_array_expansion_part_spans(&command.args[4], source).is_empty());
        assert_eq!(
            all_elements_array_expansion_part_spans(&command.args[5], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]}"]
        );
    }

    #[test]
    fn unquoted_all_elements_array_expansion_spans_only_return_unquoted_at_style_parts() {
        let source = "printf '%s\\n' $@ $* \"$@\" \"$*\" ${arr[@]} ${arr[*]} ${arr[@]:1:2} ${arr[*]:1:2} ${!arr[@]} ${arr[@]/#/#} ${arr[@]@Q} ${arr[@]:-fallback} ${arr[@]:+fallback} ${1+\"$@\"}\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[1], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$@"]
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[2], source).is_empty()
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[3], source).is_empty()
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[4], source).is_empty()
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[5], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]}"]
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[6], source).is_empty()
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[7], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]:1:2}"]
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[8], source).is_empty()
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[9], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${!arr[@]}"]
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[10], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]/#/#}"]
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[11], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]@Q}"]
        );
        assert_eq!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[12], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${arr[@]:-fallback}"]
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[13], source).is_empty()
        );
        assert!(
            unquoted_all_elements_array_expansion_part_spans(&command.args[14], source).is_empty()
        );
    }

    #[test]
    fn positional_parameters_are_treated_like_array_splats() {
        let source = "printf '%s\\n' $@ $* \"$@\" \"$*\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            array_expansion_part_spans(&command.args[1], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$@"]
        );
        assert_eq!(
            array_expansion_part_spans(&command.args[2], source)
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$*"]
        );
    }

    #[test]
    fn word_all_elements_array_slice_spans_track_at_selector_slice_forms_only() {
        let source = "\
printf '%s\\n' ${@:2} ${@:2:3} ${arr[@]:1} ${arr[@]:1:2} ${arr[*]:1} ${*:2} ${arr[0]:1} ${@} ${arr[@]}
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(word_all_elements_array_slice_spans)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            spans,
            vec!["${@:2}", "${@:2:3}", "${arr[@]:1}", "${arr[@]:1:2}"]
        );
    }

    #[test]
    fn word_quoted_all_elements_array_slice_spans_track_only_quoted_forms() {
        let source = "\
printf '%s\\n' \"${@:2}\" \"x${@:2}y\" \"${arr[@]:1}\" \"${arr[@]:1:2}\" ${@:2} \"${arr[*]:1}\" \"${*:2}\" \"\\${@:2}\" \"${@:-fallback}\" \"${@}\" \"${arr[@]}\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(word_quoted_all_elements_array_slice_spans)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            spans,
            vec!["${@:2}", "${@:2}", "${arr[@]:1}", "${arr[@]:1:2}"]
        );
        assert!(word_has_quoted_all_elements_array_slice(&command.args[1]));
        assert!(!word_has_quoted_all_elements_array_slice(&command.args[5]));
    }

    #[test]
    fn word_has_direct_all_elements_array_expansion_ignores_nested_or_scalar_operator_uses() {
        let source = "\
printf '%s\\n' \"$@\" \"${arr[@]}\" \"${arr[@]:1}\" \"${arr[@]:-fallback}\" \"${@:+ok}\" \"${arr[@]:+ok}\" \"${target=\"$@\"}\" \"$(echo \"$@\")\" \"${arr[*]}\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .skip(1)
            .map(|word| word_has_direct_all_elements_array_expansion_in_source(word, source))
            .collect::<Vec<_>>();

        assert_eq!(
            matches,
            vec![true, true, true, true, false, false, false, false, false]
        );
    }

    #[test]
    fn word_has_direct_all_elements_array_expansion_handles_backslash_parity() {
        let source = "\
printf '%s\\n' \"\\$@\" \"\\\\$@\" \"\\${@:2}\" \"\\\\${@:2}\" \"\\${arr[@]}\" \"\\\\${arr[@]}\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .skip(1)
            .map(|word| word_has_direct_all_elements_array_expansion_in_source(word, source))
            .collect::<Vec<_>>();

        assert_eq!(matches, vec![false, true, false, true, false, true]);
    }

    #[test]
    fn word_has_direct_all_elements_array_expansion_ignores_zsh_positional_subscripts() {
        let source = "print \"${@[_i]}\" \"$@[_i]\" \"${@[2,-1]:-}\" \"${@[5,-1]:-fallback}\"\n";
        let output = Parser::with_dialect(source, shuck_parser::parser::ShellDialect::Zsh)
            .parse()
            .unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .map(|word| {
                word_has_direct_all_elements_array_expansion_in_source_with_dialect(
                    word,
                    source,
                    ShellDialect::Zsh,
                )
            })
            .collect::<Vec<_>>();

        assert_eq!(matches, vec![false, false, false, false]);
    }

    #[test]
    fn positional_subscript_exemption_is_zsh_only() {
        assert!(super::candidate_is_all_elements_array_expansion(
            "${@[_i]:-}",
            ShellDialect::Bash,
        ));
        assert!(!super::candidate_is_all_elements_array_expansion(
            "${@[_i]:-}",
            ShellDialect::Zsh,
        ));
        assert!(super::candidate_is_direct_all_elements_array_expansion(
            "${@[2,-1]:-fallback}",
            ShellDialect::Bash,
        ));
        assert!(!super::candidate_is_direct_all_elements_array_expansion(
            "${@[2,-1]:-fallback}",
            ShellDialect::Zsh,
        ));

        let source = "print \"$@[_i]\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(super::word_has_all_elements_array_expansion_syntax(
            &command.args[0],
            source,
            ShellDialect::Bash,
        ));
        assert!(!super::word_has_all_elements_array_expansion_syntax(
            &command.args[0],
            source,
            ShellDialect::Zsh,
        ));
    }

    #[test]
    fn zsh_positional_selector_subscripts_still_count_as_all_elements_splats() {
        assert!(super::candidate_is_all_elements_array_expansion(
            "${@[*]:-fallback}",
            ShellDialect::Zsh,
        ));
        assert!(super::candidate_is_direct_all_elements_array_expansion(
            "${@[*]:-fallback}",
            ShellDialect::Zsh,
        ));

        let source = "print \"${@[*]}\" \"$@[*]\" \"${@[@]}\" \"$@[@]\"\n";
        let output = Parser::with_dialect(source, shuck_parser::parser::ShellDialect::Zsh)
            .parse()
            .unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .map(|word| {
                word_has_direct_all_elements_array_expansion_in_source_with_dialect(
                    word,
                    source,
                    ShellDialect::Zsh,
                )
            })
            .collect::<Vec<_>>();

        assert_eq!(matches, vec![true, true, true, true]);
    }

    #[test]
    fn word_has_direct_all_elements_array_expansion_ignores_escaped_parameter_nesting() {
        let source = "\
printf '%s\\n' \"\\${1+'\\\"$@\\\"'}\" \"$@\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .skip(1)
            .map(|word| word_has_direct_all_elements_array_expansion_in_source(word, source))
            .collect::<Vec<_>>();

        assert_eq!(matches, vec![false, true]);
    }

    #[test]
    fn word_has_direct_all_elements_array_expansion_ignores_quoted_braces_in_escaped_text() {
        let source = "\
printf '%s\\n' \"\\${1+'} \\\"$@\\\"'}\" \"$@\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let matches = command
            .args
            .iter()
            .skip(1)
            .map(|word| word_has_direct_all_elements_array_expansion_in_source(word, source))
            .collect::<Vec<_>>();

        assert_eq!(matches, vec![false, true]);
    }

    #[test]
    fn word_all_elements_array_slice_span_in_source_ignores_escaped_markers() {
        let source = "printf '%s\\n' \"\\${arr[@]:1}\" \"${arr[@]:1}\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(word_all_elements_array_slice_span_in_source(&command.args[1], source).is_none());
        assert_eq!(
            word_all_elements_array_slice_span_in_source(&command.args[2], source)
                .expect("expected array slice span")
                .slice(source),
            "${arr[@]:1}"
        );
    }

    #[test]
    fn word_quoted_unindexed_bash_source_span_in_source_tracks_scalar_forms() {
        let source = "\
printf '%s\\n' \"$BASH_SOURCE\" \"${BASH_SOURCE}\" \"$(dirname \"$BASH_SOURCE\")\" \"${BASH_SOURCE[0]}\" \"\\$BASH_SOURCE\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            word_quoted_unindexed_bash_source_span_in_source(&command.args[1], source)
                .expect("expected BASH_SOURCE span")
                .slice(source),
            "$BASH_SOURCE"
        );
        assert_eq!(
            word_quoted_unindexed_bash_source_span_in_source(&command.args[2], source)
                .expect("expected BASH_SOURCE span")
                .slice(source),
            "${BASH_SOURCE}"
        );
        assert!(
            word_quoted_unindexed_bash_source_span_in_source(&command.args[3], source).is_none()
        );
        assert!(
            word_quoted_unindexed_bash_source_span_in_source(&command.args[4], source).is_none()
        );
        assert!(
            word_quoted_unindexed_bash_source_span_in_source(&command.args[5], source).is_none()
        );
    }

    #[test]
    fn word_unquoted_star_splat_spans_tracks_star_selector_forms_only() {
        let source = "\
printf '%s\\n' $* ${*} ${*:1} ${arr[*]} ${arr[*]:1:2} ${arr[*]:-fallback} ${arr[*]@Q} ${!arr[*]} ${arr[@]} ${arr[@]:1} ${arr[0]} \"$*\" \"${arr[*]}\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(word_unquoted_star_splat_spans)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            spans,
            vec![
                "$*",
                "${*}",
                "${*:1}",
                "${arr[*]}",
                "${arr[*]:1:2}",
                "${arr[*]:-fallback}",
                "${arr[*]@Q}"
            ]
        );
    }

    #[test]
    fn word_unquoted_star_parameter_spans_tracks_star_selector_forms_only() {
        let source = "\
printf '%s\\n' $* ${arr[*]} ${arr[*]:1:2} ${arr[*]:-fallback} ${arr[*]@Q} ${!arr[*]} ${arr[@]} ${arr[@]:1} ${arr[0]} \"$*\" \"${arr[*]}\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(|word| {
                let unquoted_array_spans = unquoted_array_expansion_part_spans(word, source);
                word_unquoted_star_parameter_spans(word, &unquoted_array_spans)
            })
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            spans,
            vec![
                "$*",
                "${arr[*]}",
                "${arr[*]:1:2}",
                "${arr[*]:-fallback}",
                "${arr[*]@Q}"
            ]
        );
    }

    #[test]
    fn word_quoted_star_splat_spans_tracks_double_quoted_star_selector_forms_only() {
        let source = "\
printf '%s\\n' \"$*\" \"${*}\" \"${*:1}\" \"${arr[*]}\" \"${arr[*]:1:2}\" \"${!arr[*]}\" \"${arr[@]}\" \"${arr[@]:1}\" \"$@\" ${arr[*]}
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(word_quoted_star_splat_spans)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            spans,
            vec!["$*", "${*}", "${*:1}", "${arr[*]}", "${arr[*]:1:2}"]
        );
    }

    #[test]
    fn word_positional_at_splat_spans_tracks_positional_forms_only() {
        let source = "\
printf '%s\\n' $@ ${@} ${@:1:2} \"${@}\" \"x$@y\" ${array[@]} ${array[@]:1} $* \"${*}\" ${!@}
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let spans = command
            .args
            .iter()
            .flat_map(word_positional_at_splat_spans)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(spans, vec!["$@", "${@}", "${@:1:2}", "${@}", "$@"]);
    }

    #[test]
    fn word_is_pure_positional_at_splat_rejects_mixed_words() {
        let source = "\
printf '%s\\n' \"$@\" ${@} \"${@:1}\" \"$@$@\" \"prefix$@suffix\" ${array[@]} \"$*\" \"$1\" \"${@:-fallback}\"
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let pure = command
            .args
            .iter()
            .map(word_is_pure_positional_at_splat)
            .collect::<Vec<_>>();

        assert_eq!(
            pure,
            vec![
                false, true, true, true, true, false, false, false, false, false
            ]
        );
    }

    #[test]
    fn word_folded_positional_at_splat_span_tracks_only_folding_forms() {
        let source = "\
printf '%s\\n' \"$@\" \"${@}\" \"${@:1}\" \"$@$@\" \"$@\"\"$@\" \"x$@y\" x$@y ${@} ${@:1} ${@:-fallback}
";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let folded = command
            .args
            .iter()
            .filter_map(word_folded_positional_at_splat_span)
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(folded, vec!["$@", "$@", "$@", "$@"]);
        assert!(!word_has_folded_positional_at_splat(&command.args[1]));
        assert!(word_has_folded_positional_at_splat(&command.args[4]));
    }

    #[test]
    fn word_folded_positional_at_splat_span_in_source_ignores_standalone_expansions() {
        let source = "\
exec \"$@\" \"${@}\" \"${@:1}\" \"${@:-fallback}\" \"${@:${args_offset}}\" \"${@//-I\\/usr\\/include/-I${XBPS_CROSS_BASE}\\/usr\\/include}\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(command.args.iter().all(|word| {
            word_folded_positional_at_splat_span_in_source(word, source).is_none()
        }));
    }

    #[test]
    fn word_folded_positional_at_splat_span_in_source_ignores_escaped_positional_markers() {
        let source = "eval command \"\\$@\" \"x\\$@y\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert!(word_folded_positional_at_splat_span_in_source(&command.args[0], source).is_none());
        assert!(word_folded_positional_at_splat_span_in_source(&command.args[1], source).is_none());
    }

    #[test]
    fn word_folded_positional_at_splat_span_in_source_tracks_unescaped_splats_after_escaped_literals()
     {
        let source = "echo \"gvm_pkgset_use: \\$@   => $@\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            word_folded_positional_at_splat_span_in_source(&command.args[0], source)
                .expect("expected folded positional span")
                .slice(source),
            "$@"
        );
    }

    #[test]
    fn word_folded_all_elements_array_span_in_source_tracks_array_splats_in_larger_words() {
        let source = "\
printf '%s\\n' \"${arr[@]}\" \"x${arr[@]}\" \"x${!arr[@]}\" \"x${arr[@]:1}\" \"x${arr[@]/a/b}\" \"x${arr[*]}\" \"\\${arr[@]}\" \"$@\" \"x$@\" \"${arr[@]+ ${arr[*]}}\" \"x${arr[@]+ ${arr[*]}}\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        let folded = command
            .args
            .iter()
            .filter_map(|word| word_folded_all_elements_array_span_in_source(word, source))
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(
            folded,
            vec![
                "${arr[@]}",
                "${!arr[@]}",
                "${arr[@]:1}",
                "${arr[@]/a/b}",
                "$@",
                "${arr[@]+ ${arr[*]}}"
            ]
        );
    }

    #[test]
    fn word_positional_at_splat_spans_in_source_tracks_operation_forms() {
        let source = "printf '%s\\n' \"${@:-fallback}\" \"$@\" \"\\$@\"\n";
        let output = Parser::new(source).parse().unwrap();
        let command = &output.file.body[0].command;
        let shuck_ast::Command::Simple(command) = command else {
            panic!("expected simple command");
        };

        assert_eq!(
            word_positional_at_splat_spans_in_source(&command.args[1], source)
                .into_iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["${@:-fallback}"]
        );
        assert_eq!(
            word_positional_at_splat_spans_in_source(&command.args[2], source)
                .into_iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["$@"]
        );
        assert!(word_positional_at_splat_spans_in_source(&command.args[3], source).is_empty());
    }
}
