use std::{
    collections::HashSet,
    fs,
    path::{Path, PathBuf},
};

#[test]
fn markdown_links_point_to_existing_files() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let files = markdown_files(root);
    let mut failures = Vec::new();

    for file in &files {
        let content = fs::read_to_string(file).expect("read markdown");
        for link in markdown_links(&content) {
            if should_skip_link(&link) {
                continue;
            }
            let target = file
                .parent()
                .unwrap_or(root)
                .join(link.split('#').next().unwrap_or_default());
            if !target.exists() {
                failures.push(format!(
                    "{} -> {}",
                    file.strip_prefix(root).unwrap().display(),
                    link
                ));
            }
        }
    }

    assert!(
        failures.is_empty(),
        "broken markdown links:\n{}",
        failures.join("\n")
    );
}

#[test]
fn docs_do_not_contain_local_paths_or_secret_patterns() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let files = markdown_files(root);
    let sensitive_patterns = [
        "/Users/",
        "password=",
        "secret=",
        "token=",
        "AKIA",
        "BEGIN PRIVATE KEY",
    ];
    let mut failures = Vec::new();

    for file in &files {
        let content = fs::read_to_string(file).expect("read markdown");
        for pattern in sensitive_patterns {
            if content.contains(pattern) {
                failures.push(format!(
                    "{} contains {}",
                    file.strip_prefix(root).unwrap().display(),
                    pattern
                ));
            }
        }
    }

    assert!(
        failures.is_empty(),
        "sensitive documentation patterns:\n{}",
        failures.join("\n")
    );
}

fn markdown_files(root: &Path) -> Vec<PathBuf> {
    let mut files = vec![root.join("README.md")];
    collect_markdown(&root.join("docs"), &mut files);
    files
}

fn collect_markdown(dir: &Path, files: &mut Vec<PathBuf>) {
    for entry in fs::read_dir(dir).expect("read docs dir") {
        let path = entry.expect("dir entry").path();
        if path.is_dir() {
            collect_markdown(&path, files);
        } else if path.extension().is_some_and(|ext| ext == "md") {
            files.push(path);
        }
    }
}

fn markdown_links(content: &str) -> HashSet<String> {
    let mut links = HashSet::new();
    let bytes = content.as_bytes();
    let mut index = 0;
    while index < bytes.len() {
        let Some(open_text) = content[index..].find('[').map(|offset| index + offset) else {
            break;
        };
        let Some(close_text) = content[open_text..]
            .find(']')
            .map(|offset| open_text + offset)
        else {
            break;
        };
        let after_text = close_text + 1;
        if content[after_text..].starts_with('(')
            && let Some(close_link) = content[after_text + 1..]
                .find(')')
                .map(|offset| after_text + 1 + offset)
        {
            links.insert(content[after_text + 1..close_link].trim().to_string());
            index = close_link + 1;
        } else {
            index = after_text;
        }
    }
    links
}

fn should_skip_link(link: &str) -> bool {
    link.is_empty()
        || link.starts_with('#')
        || link.starts_with("http://")
        || link.starts_with("https://")
        || link.starts_with("mailto:")
        || link.starts_with("rustdoc:")
}
