//! Shared test helpers for the lint crate. Rule-family test files live
//! as sibling submodules and pull helpers in via `use super::*;`.

pub(super) use crate::*;
pub(super) use harn_lexer::{FixEdit, Lexer};
pub(super) use harn_parser::Parser;
pub(super) use std::collections::HashSet;

pub(super) fn lint_source(source: &str) -> Vec<LintDiagnostic> {
    let mut lexer = Lexer::new(source);
    let tokens = lexer.tokenize().unwrap();
    let mut parser = Parser::new(tokens);
    let program = parser.parse().unwrap();
    lint_with_source(&program, source)
}

pub(super) fn has_rule(diagnostics: &[LintDiagnostic], rule: &str) -> bool {
    diagnostics.iter().any(|d| d.rule == rule)
}

pub(super) fn count_rule(diagnostics: &[LintDiagnostic], rule: &str) -> usize {
    diagnostics.iter().filter(|d| d.rule == rule).count()
}

pub(super) fn get_fix(diagnostics: &[LintDiagnostic], rule: &str) -> Option<Vec<FixEdit>> {
    diagnostics
        .iter()
        .find(|d| d.rule == rule)
        .and_then(|d| d.fix.clone())
}

pub(super) fn apply_fixes(source: &str, diagnostics: &[LintDiagnostic]) -> String {
    let mut edits: Vec<&FixEdit> = diagnostics
        .iter()
        .filter_map(|d| d.fix.as_ref())
        .flatten()
        .collect();
    edits.sort_by_key(|edit| std::cmp::Reverse(edit.span.start));
    let mut accepted: Vec<&FixEdit> = Vec::new();
    for edit in &edits {
        let overlaps = accepted
            .iter()
            .any(|prev| edit.span.start < prev.span.end && edit.span.end > prev.span.start);
        if !overlaps {
            accepted.push(edit);
        }
    }
    let mut result = source.to_string();
    for edit in &accepted {
        let before = &result[..edit.span.start];
        let after = &result[edit.span.end..];
        result = format!("{before}{}{after}", edit.replacement);
    }
    result
}

pub(super) fn lint_with_require_header(
    source: &str,
    path: Option<&std::path::Path>,
) -> Vec<LintDiagnostic> {
    let mut lexer = Lexer::new(source);
    let tokens = lexer.tokenize().unwrap();
    let mut parser = Parser::new(tokens);
    let program = parser.parse().unwrap();
    let options = LintOptions {
        file_path: path,
        require_file_header: true,
        ..Default::default()
    };
    lint_with_options(&program, &[], Some(source), &HashSet::new(), &options)
}

pub(super) fn lint_with_docstrings(source: &str) -> Vec<LintDiagnostic> {
    let mut lexer = Lexer::new(source);
    let tokens = lexer.tokenize().unwrap();
    let mut parser = Parser::new(tokens);
    let program = parser.parse().unwrap();
    let options = LintOptions {
        require_docstrings: true,
        ..Default::default()
    };
    lint_with_options(&program, &[], Some(source), &HashSet::new(), &options)
}

pub(super) fn lint_with_stdlib_metadata(source: &str) -> Vec<LintDiagnostic> {
    let mut lexer = Lexer::new(source);
    let tokens = lexer.tokenize().unwrap();
    let mut parser = Parser::new(tokens);
    let program = parser.parse().unwrap();
    let options = LintOptions {
        require_stdlib_metadata: true,
        ..Default::default()
    };
    lint_with_options(&program, &[], Some(source), &HashSet::new(), &options)
}

mod ambient_capabilities;
mod ambient_clock;
mod ambient_stdio;
mod assert_pipeline;
mod autofix;
mod boolean_patterns;
mod break_loop;
mod complexity;
mod connector_effect_policy;
mod empty_blocks;
mod engine_rules;
mod file_header;
mod formatting;
mod harndoc;
mod hitl;
mod hygiene;
mod imports;
mod invalid_binop;
mod llm_rules;
mod long_running;
mod mcp_tools;
mod mutability;
mod naming_types;
mod optional_shorthand;
mod persona_steps;
mod redundant_nil_ternary;
mod secret_scan_rules;
mod shadowing;
mod stdlib_metadata;
mod unnecessary_cast;
mod unnecessary_parentheses;
mod unreachable;
mod unused;
mod unused_function;
