use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::{Context, Result};
use globset::{Glob, GlobMatcher};
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;
use shuck_semantic::{UnreachedFunctionAnalysisOptions, UnusedAssignmentAnalysisOptions};

use crate::ambient_contracts::ResolvedAmbientContracts;
use crate::{Category, Rule, RuleSelector, RuleSet, Severity, ShellDialect};

const DEFAULT_DISABLED_NON_STYLE_RULES: &[Rule] = &[
    Rule::ImplicitGlobalInFunction,
    Rule::MutableGlobal,
    Rule::UnanchoredSourcePath,
    Rule::FunctionCalledBeforeDefined,
];
const DEFAULT_C160_ALLOWED_ANCHORS: &[&str] = &[
    "${BASH_SOURCE[0]%/*}",
    "$(dirname \"$0\")",
    "$(dirname \"${BASH_SOURCE[0]}\")",
];

/// Per-rule behavior overrides applied during lint analysis.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LinterRuleOptions {
    /// Behavior overrides for `C001`.
    pub c001: C001RuleOptions,
    /// Behavior overrides for `C063`.
    pub c063: C063RuleOptions,
    /// Behavior overrides for `S078`.
    pub s078: S078RuleOptions,
    /// Behavior overrides for `S079`.
    pub s079: S079RuleOptions,
    /// Behavior overrides for `S080`.
    pub s080: S080RuleOptions,
    /// Behavior overrides for `S081`.
    pub s081: S081RuleOptions,
    /// Behavior overrides for `S082`.
    pub s082: S082RuleOptions,
    /// Behavior overrides for `S083`.
    pub s083: S083RuleOptions,
    /// Behavior overrides for `S084`.
    pub s084: S084RuleOptions,
    /// Behavior overrides for `S085`.
    pub s085: S085RuleOptions,
    /// Behavior overrides for `C158`.
    pub c158: C158RuleOptions,
    /// Behavior overrides for `C159`.
    pub c159: C159RuleOptions,
    /// Behavior overrides for `C160`.
    pub c160: C160RuleOptions,
    /// Behavior overrides for `C161`.
    pub c161: C161RuleOptions,
}

/// Behavior overrides for `C001` unused-assignment analysis.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct C001RuleOptions {
    /// Whether scalar indirect-expansion targets like `${!name}` count as a use of the target.
    /// Disabled by default to match ShellCheck. Array-like targets such as
    /// `name=arr[@]; ${!name}` stay live in both modes.
    pub treat_indirect_expansion_targets_as_used: bool,
}

impl C001RuleOptions {
    pub(crate) fn semantic_options(&self) -> UnusedAssignmentAnalysisOptions {
        UnusedAssignmentAnalysisOptions {
            treat_indirect_expansion_targets_as_used: self.treat_indirect_expansion_targets_as_used,
            report_unreachable_assignments: true,
        }
    }
}

/// Behavior overrides for `C063` overwritten/unreached function analysis.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct C063RuleOptions {
    /// Whether nested function definitions should be reported when no reachable direct call
    /// reaches the enclosing function scope before that scope exits.
    pub report_unreached_nested_definitions: bool,
}

impl C063RuleOptions {
    pub(crate) fn semantic_options(&self) -> UnreachedFunctionAnalysisOptions {
        UnreachedFunctionAnalysisOptions {
            report_unreached_nested_definitions: self.report_unreached_nested_definitions,
        }
    }
}
/// Behavior overrides for `S080` script size policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S080RuleOptions {
    /// Maximum line count accepted for one script.
    pub max_lines: usize,
    /// Which source lines count toward the threshold.
    pub count: String,
}

impl Default for S080RuleOptions {
    fn default() -> Self {
        Self {
            max_lines: 100,
            count: "physical".to_owned(),
        }
    }
}

/// Behavior overrides for `S078` shebang shell policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S078RuleOptions {
    /// Interpreter names accepted in shebangs for this project.
    pub allowed_shells: Vec<String>,
}

impl Default for S078RuleOptions {
    fn default() -> Self {
        Self {
            allowed_shells: vec!["bash".to_owned()],
        }
    }
}

/// Behavior overrides for `S079` shebang invocation form policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S079RuleOptions {
    /// Invocation forms accepted for shebangs in this project.
    pub allowed_forms: Vec<String>,
    /// Exact shebang invocation strings that are accepted regardless of form.
    pub allowed_paths: Vec<String>,
}

impl Default for S079RuleOptions {
    fn default() -> Self {
        Self {
            allowed_forms: vec!["env-lookup".to_owned()],
            allowed_paths: vec!["/bin/bash".to_owned(), "/usr/bin/env bash".to_owned()],
        }
    }
}

/// Behavior overrides for `S081` file description comments.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct S081RuleOptions {
    /// Whether files containing only a shebang are exempt from the rule.
    pub ignore_shebang_only_files: bool,
}

/// Behavior overrides for `S082` TODO-style comment formatting.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S082RuleOptions {
    /// Comment markers that are checked at the start of a comment.
    pub kinds: Vec<String>,
    /// Whether a checked marker must be followed immediately by `(owner)`.
    pub require_owner: bool,
    /// Whether a checked marker must include non-empty explanatory text.
    pub require_message: bool,
}

impl Default for S082RuleOptions {
    fn default() -> Self {
        Self {
            kinds: vec!["TODO".to_owned(), "FIXME".to_owned(), "XXX".to_owned()],
            require_owner: true,
            require_message: true,
        }
    }
}

/// Which functions require a leading documentation comment for `S083`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum S083FunctionDocRequirement {
    All,
    Exported,
    #[default]
    Long,
    Parameterized,
}

/// Behavior overrides for `S083` missing function documentation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S083RuleOptions {
    pub require_for: S083FunctionDocRequirement,
    pub long_function_line_threshold: usize,
}

impl Default for S083RuleOptions {
    fn default() -> Self {
        Self {
            require_for: S083FunctionDocRequirement::Long,
            long_function_line_threshold: 10,
        }
    }
}

/// Behavior overrides for `S084` function documentation content.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S084RuleOptions {
    pub require_globals: bool,
    pub require_arguments: bool,
    pub require_outputs: bool,
    pub require_returns: bool,
}

impl Default for S084RuleOptions {
    fn default() -> Self {
        Self {
            require_globals: true,
            require_arguments: true,
            require_outputs: true,
            require_returns: true,
        }
    }
}

/// Behavior overrides for `S085` main entrypoint analysis.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct S085RuleOptions {
    /// Minimum source line count before the script is checked.
    pub non_trivial_line_threshold: usize,
    /// Minimum function definition count before the script is checked.
    pub non_trivial_function_count: usize,
    /// Expected entrypoint function name.
    pub main_name: String,
}

impl Default for S085RuleOptions {
    fn default() -> Self {
        Self {
            non_trivial_line_threshold: 30,
            non_trivial_function_count: 2,
            main_name: "main".to_owned(),
        }
    }
}

/// Behavior overrides for `C158` implicit global assignment analysis.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct C158RuleOptions {
    /// Whether top-level readonly declarations document intentional globals.
    pub treat_readonly_as_documented: bool,
    /// Whether top-level exported bindings document intentional globals.
    pub treat_export_as_intentional: bool,
}

impl Default for C158RuleOptions {
    fn default() -> Self {
        Self {
            treat_readonly_as_documented: true,
            treat_export_as_intentional: true,
        }
    }
}

/// Behavior overrides for `C159` mutable-global analysis.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct C159RuleOptions {
    /// Whether self-referential default initializers such as `name=${name:-value}` are allowed.
    pub allow_conditional_init: bool,
}

impl Default for C159RuleOptions {
    fn default() -> Self {
        Self {
            allow_conditional_init: true,
        }
    }
}

/// Behavior overrides for `C160` unanchored source path analysis.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct C160RuleOptions {
    /// Path prefix expressions accepted as script-directory anchors.
    pub allowed_anchors: Vec<String>,
}

impl Default for C160RuleOptions {
    fn default() -> Self {
        Self {
            allowed_anchors: DEFAULT_C160_ALLOWED_ANCHORS
                .iter()
                .map(|anchor| (*anchor).to_owned())
                .collect(),
        }
    }
}

/// Behavior overrides for `C161` function-call ordering analysis.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct C161RuleOptions {
    /// Whether calls after a source command are ignored because sourced files may define functions.
    pub ignore_after_source: bool,
}

impl Default for C161RuleOptions {
    fn default() -> Self {
        Self {
            ignore_after_source: true,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinterSettings {
    pub rules: RuleSet,
    pub severity_overrides: FxHashMap<Rule, Severity>,
    pub shell: ShellDialect,
    pub ambient_shell_options: AmbientShellOptions,
    pub ambient_contracts: Arc<ResolvedAmbientContracts>,
    pub analyzed_paths: Option<Arc<FxHashSet<PathBuf>>>,
    pub per_file_ignores: Arc<CompiledPerFileIgnoreList>,
    pub report_environment_style_names: bool,
    pub resolve_source_closure: bool,
    pub rule_options: LinterRuleOptions,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AmbientShellOptions {
    pub errexit: bool,
    pub pipefail: bool,
}

impl Default for LinterSettings {
    fn default() -> Self {
        Self {
            rules: Self::default_rules(),
            severity_overrides: FxHashMap::default(),
            shell: ShellDialect::Unknown,
            ambient_shell_options: AmbientShellOptions::default(),
            ambient_contracts: Arc::new(ResolvedAmbientContracts::default()),
            analyzed_paths: None,
            per_file_ignores: Arc::new(CompiledPerFileIgnoreList::default()),
            report_environment_style_names: false,
            resolve_source_closure: true,
            rule_options: LinterRuleOptions::default(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PerFileIgnore {
    pattern: String,
    rules: RuleSet,
}

impl PerFileIgnore {
    pub fn new(pattern: impl Into<String>, rules: RuleSet) -> Self {
        Self {
            pattern: pattern.into(),
            rules,
        }
    }

    pub fn pattern(&self) -> &str {
        &self.pattern
    }

    pub const fn rules(&self) -> RuleSet {
        self.rules
    }
}

#[derive(Debug, Clone, Default)]
pub struct CompiledPerFileIgnoreList {
    project_root: PathBuf,
    entries: Vec<CompiledPerFileIgnore>,
}

impl PartialEq for CompiledPerFileIgnoreList {
    fn eq(&self, other: &Self) -> bool {
        self.project_root == other.project_root && self.entries == other.entries
    }
}

impl Eq for CompiledPerFileIgnoreList {}

#[derive(Debug, Clone)]
struct CompiledPerFileIgnore {
    pattern: String,
    basename_matcher: GlobMatcher,
    relative_matcher: GlobMatcher,
    absolute_matcher: GlobMatcher,
    negated: bool,
    rules: RuleSet,
}

impl PartialEq for CompiledPerFileIgnore {
    fn eq(&self, other: &Self) -> bool {
        self.pattern == other.pattern && self.negated == other.negated && self.rules == other.rules
    }
}

impl Eq for CompiledPerFileIgnore {}

impl LinterSettings {
    pub fn for_rule(rule: Rule) -> Self {
        Self {
            rules: RuleSet::from_iter([rule]),
            ..Self::default()
        }
    }

    pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
        Self {
            rules: rules.into_iter().collect(),
            ..Self::default()
        }
    }

    pub fn default_rules() -> RuleSet {
        Rule::iter()
            .filter(|rule| !matches!(rule.category(), Category::Style))
            .collect::<RuleSet>()
            .subtract(&default_disabled_non_style_rules())
    }

    pub fn from_selectors(select: &[RuleSelector], ignore: &[RuleSelector]) -> Self {
        let mut rules = RuleSet::EMPTY;
        for selector in select {
            rules = rules.union(&selector.into_rule_set());
        }
        for selector in ignore {
            rules = rules.subtract(&selector.into_rule_set());
        }

        Self {
            rules,
            ..Self::default()
        }
    }

    pub fn with_shell(mut self, shell: ShellDialect) -> Self {
        self.shell = shell;
        self
    }

    pub fn with_ambient_shell_options(
        mut self,
        ambient_shell_options: AmbientShellOptions,
    ) -> Self {
        self.ambient_shell_options = ambient_shell_options;
        self
    }

    pub fn analyzed_path_set(paths: impl IntoIterator<Item = PathBuf>) -> Arc<FxHashSet<PathBuf>> {
        Arc::new(
            paths
                .into_iter()
                .map(|path| std::fs::canonicalize(&path).unwrap_or(path))
                .collect(),
        )
    }

    pub fn with_analyzed_path_set(mut self, paths: Arc<FxHashSet<PathBuf>>) -> Self {
        self.analyzed_paths = Some(paths);
        self
    }

    pub fn with_analyzed_paths(self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
        self.with_analyzed_path_set(Self::analyzed_path_set(paths))
    }

    pub fn with_c001_treat_indirect_expansion_targets_as_used(mut self, value: bool) -> Self {
        self.rule_options
            .c001
            .treat_indirect_expansion_targets_as_used = value;
        self
    }

    pub fn with_resolve_source_closure(mut self, value: bool) -> Self {
        self.resolve_source_closure = value;
        self
    }

    pub fn with_c063_report_unreached_nested_definitions(mut self, value: bool) -> Self {
        self.rule_options.c063.report_unreached_nested_definitions = value;
        self
    }

    pub fn with_s080_max_lines(mut self, value: usize) -> Self {
        self.rule_options.s080.max_lines = value;
        self
    }

    pub fn with_s080_count(mut self, value: impl Into<String>) -> Self {
        self.rule_options.s080.count = value.into();
        self
    }

    pub fn with_s081_ignore_shebang_only_files(mut self, value: bool) -> Self {
        self.rule_options.s081.ignore_shebang_only_files = value;
        self
    }

    pub fn with_s082_kinds(mut self, kinds: impl IntoIterator<Item = String>) -> Self {
        self.rule_options.s082.kinds = kinds.into_iter().collect();
        self
    }

    pub fn with_s082_require_owner(mut self, value: bool) -> Self {
        self.rule_options.s082.require_owner = value;
        self
    }

    pub fn with_s082_require_message(mut self, value: bool) -> Self {
        self.rule_options.s082.require_message = value;
        self
    }

    pub fn with_s083_require_for(mut self, value: S083FunctionDocRequirement) -> Self {
        self.rule_options.s083.require_for = value;
        self
    }

    pub fn with_s083_long_function_line_threshold(mut self, value: usize) -> Self {
        self.rule_options.s083.long_function_line_threshold = value;
        self
    }

    pub fn with_s084_require_outputs(mut self, value: bool) -> Self {
        self.rule_options.s084.require_outputs = value;
        self
    }

    pub fn with_s084_require_returns(mut self, value: bool) -> Self {
        self.rule_options.s084.require_returns = value;
        self
    }

    pub fn with_s085_non_trivial_line_threshold(mut self, value: usize) -> Self {
        self.rule_options.s085.non_trivial_line_threshold = value;
        self
    }

    pub fn with_s085_non_trivial_function_count(mut self, value: usize) -> Self {
        self.rule_options.s085.non_trivial_function_count = value;
        self
    }

    pub fn with_s085_main_name(mut self, value: impl Into<String>) -> Self {
        self.rule_options.s085.main_name = value.into();
        self
    }

    pub fn with_s078_allowed_shells(
        mut self,
        allowed_shells: impl IntoIterator<Item = impl Into<String>>,
    ) -> Self {
        self.rule_options.s078.allowed_shells =
            allowed_shells.into_iter().map(Into::into).collect();
        self
    }

    pub fn with_c158_treat_readonly_as_documented(mut self, value: bool) -> Self {
        self.rule_options.c158.treat_readonly_as_documented = value;
        self
    }

    pub fn with_c158_treat_export_as_intentional(mut self, value: bool) -> Self {
        self.rule_options.c158.treat_export_as_intentional = value;
        self
    }

    pub fn with_c159_allow_conditional_init(mut self, value: bool) -> Self {
        self.rule_options.c159.allow_conditional_init = value;
        self
    }

    pub fn with_c160_allowed_anchors<I, S>(mut self, anchors: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.rule_options.c160.allowed_anchors = anchors.into_iter().map(Into::into).collect();
        self
    }

    pub fn with_c161_ignore_after_source(mut self, value: bool) -> Self {
        self.rule_options.c161.ignore_after_source = value;
        self
    }

    pub fn with_s079_allowed_forms(
        mut self,
        allowed_forms: impl IntoIterator<Item = impl Into<String>>,
    ) -> Self {
        self.rule_options.s079.allowed_forms = allowed_forms.into_iter().map(Into::into).collect();
        self
    }

    pub fn with_s079_allowed_paths(
        mut self,
        allowed_paths: impl IntoIterator<Item = impl Into<String>>,
    ) -> Self {
        self.rule_options.s079.allowed_paths = allowed_paths.into_iter().map(Into::into).collect();
        self
    }

    pub fn per_file_ignored_rules(&self, path: Option<&Path>) -> RuleSet {
        path.map_or(RuleSet::EMPTY, |path| {
            self.per_file_ignores.ignored_rules(path)
        })
    }
}

fn default_disabled_non_style_rules() -> RuleSet {
    DEFAULT_DISABLED_NON_STYLE_RULES.iter().copied().collect()
}

impl CompiledPerFileIgnoreList {
    pub fn resolve(
        project_root: impl Into<PathBuf>,
        per_file_ignores: impl IntoIterator<Item = PerFileIgnore>,
    ) -> Result<Self> {
        let project_root = project_root.into();
        let entries = per_file_ignores
            .into_iter()
            .map(|per_file_ignore| {
                let mut pattern = per_file_ignore.pattern().to_owned();
                let negated = pattern.starts_with('!');
                if negated {
                    pattern.drain(..1);
                }

                let basename_matcher = Glob::new(&pattern)
                    .with_context(|| format!("invalid glob {:?}", per_file_ignore.pattern()))?
                    .compile_matcher();
                let relative_matcher = Glob::new(&pattern)
                    .with_context(|| format!("invalid glob {:?}", per_file_ignore.pattern()))?
                    .compile_matcher();
                let absolute_matcher = Glob::new(&pattern)
                    .with_context(|| format!("invalid glob {:?}", per_file_ignore.pattern()))?
                    .compile_matcher();

                Ok(CompiledPerFileIgnore {
                    pattern: per_file_ignore.pattern().to_owned(),
                    basename_matcher,
                    relative_matcher,
                    absolute_matcher,
                    negated,
                    rules: per_file_ignore.rules(),
                })
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(Self {
            project_root,
            entries,
        })
    }

    pub fn ignored_rules(&self, path: &Path) -> RuleSet {
        let relative_path = path.strip_prefix(&self.project_root).unwrap_or(path);
        let file_name = relative_path.file_name().or_else(|| path.file_name());
        let Some(file_name) = file_name else {
            return RuleSet::EMPTY;
        };

        self.entries.iter().fold(RuleSet::EMPTY, |ignored, entry| {
            let matches = entry.basename_matcher.is_match(file_name)
                || entry.relative_matcher.is_match(relative_path)
                || matches_absolute_path(&entry.absolute_matcher, path);
            let applies = if entry.negated { !matches } else { matches };

            if applies {
                ignored.union(&entry.rules)
            } else {
                ignored
            }
        })
    }
}

fn matches_absolute_path(matcher: &GlobMatcher, path: &Path) -> bool {
    matcher.is_match(path)
        || normalized_absolute_match_path(path)
            .as_deref()
            .is_some_and(|normalized| matcher.is_match(normalized))
}

fn normalized_absolute_match_path(path: &Path) -> Option<PathBuf> {
    let path = path.to_string_lossy();

    if let Some(stripped) = path.strip_prefix(r"\\?\UNC\") {
        return Some(PathBuf::from(format!(r"\\{stripped}")));
    }

    path.strip_prefix(r"\\?\").map(PathBuf::from)
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use tempfile::tempdir;

    use super::*;
    use crate::RuleSet;

    #[test]
    fn default_rules_exclude_all_style_rules() {
        let defaults = LinterSettings::default_rules();

        for rule in Rule::iter().filter(|rule| matches!(rule.category(), Category::Style)) {
            assert!(
                !defaults.contains(rule),
                "{rule:?} should be disabled by default"
            );
        }
    }

    #[test]
    fn default_rules_include_non_style_rules() {
        let defaults = LinterSettings::default_rules();

        assert!(defaults.contains(Rule::UndefinedVariable));
        assert!(defaults.contains(Rule::ConstantCaseSubject));
        assert!(defaults.contains(Rule::RmGlobOnVariablePath));
        assert!(!defaults.contains(Rule::ImplicitGlobalInFunction));
        assert!(!defaults.contains(Rule::MutableGlobal));
        assert!(!defaults.contains(Rule::UnanchoredSourcePath));
        assert!(!defaults.contains(Rule::FunctionCalledBeforeDefined));
        assert!(!defaults.contains(Rule::AmpersandSemicolon));
    }

    #[test]
    fn default_rules_exclude_verified_default_disabled_non_style_rules() {
        let defaults = LinterSettings::default_rules();

        for rule in DEFAULT_DISABLED_NON_STYLE_RULES {
            assert!(
                !defaults.contains(*rule),
                "{rule:?} should be excluded from the native default baseline"
            );
            assert!(
                !matches!(rule.category(), Category::Style),
                "{rule:?} must stay in the non-style default-disabled set"
            );
        }
    }

    #[test]
    fn with_analyzed_path_set_reuses_shared_set() {
        let tempdir = tempdir().unwrap();
        let script_path = tempdir.path().join("script.sh");
        std::fs::write(&script_path, "echo hi\n").unwrap();

        let analyzed_paths = LinterSettings::analyzed_path_set([script_path.clone()]);
        let settings =
            LinterSettings::default().with_analyzed_path_set(Arc::clone(&analyzed_paths));

        let stored = settings.analyzed_paths.as_ref().unwrap();
        assert!(Arc::ptr_eq(stored, &analyzed_paths));
        assert!(stored.contains(&std::fs::canonicalize(script_path).unwrap()));
    }

    #[test]
    fn matches_absolute_per_file_ignore_patterns() {
        let tempdir = tempdir().unwrap();
        let project_root = tempdir.path().to_path_buf();
        let script_path = project_root.join("nested").join("script.sh");
        let absolute_pattern = script_path
            .parent()
            .unwrap()
            .join("*.sh")
            .to_string_lossy()
            .into_owned();
        let per_file_ignores = CompiledPerFileIgnoreList::resolve(
            project_root,
            [PerFileIgnore::new(
                absolute_pattern,
                RuleSet::from_iter([Rule::UnusedAssignment]),
            )],
        )
        .unwrap();

        let ignored_rules = per_file_ignores.ignored_rules(&script_path);

        assert!(ignored_rules.contains(Rule::UnusedAssignment));
    }

    #[test]
    fn strips_windows_verbatim_disk_prefixes_for_absolute_matching() {
        assert_eq!(
            normalized_absolute_match_path(Path::new(r"\\?\C:\repo\nested\script.sh")),
            Some(PathBuf::from(r"C:\repo\nested\script.sh"))
        );
    }

    #[test]
    fn strips_windows_verbatim_unc_prefixes_for_absolute_matching() {
        assert_eq!(
            normalized_absolute_match_path(Path::new(r"\\?\UNC\server\share\script.sh")),
            Some(PathBuf::from(r"\\server\share\script.sh"))
        );
    }
}
