use std::fmt;
use std::str::FromStr;

use indexmap::IndexMap;
use regex::Regex;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::CompletionItemLabelDetails;
use tower_lsp::lsp_types::Documentation;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use tower_lsp::lsp_types::MarkupKind;
use tower_lsp::lsp_types::ParameterInformation;
use tower_lsp::lsp_types::ParameterLabel;
use tower_lsp::lsp_types::SignatureHelp;
use tower_lsp::lsp_types::SignatureInformation;

use crate::ModuleId;
use crate::execution::annotations;
use crate::execution::annotations::VersionConstraint;
use crate::parsing::ast::types::Annotation;
use crate::parsing::ast::types::Expr;
use crate::parsing::ast::types::ImportSelector;
use crate::parsing::ast::types::ItemVisibility;
use crate::parsing::ast::types::LiteralValue;
use crate::parsing::ast::types::Node;
use crate::parsing::ast::types::NonCodeValue;
use crate::parsing::ast::types::VariableKind;
use crate::parsing::token::NumericSuffix;

pub fn walk_prelude() -> ModData {
    visit_module("prelude", "", WalkForNames::All).unwrap()
}

pub fn walk_stdlib() -> ModData {
    let mut stdlib = walk_prelude();

    #[expect(clippy::single_element_loop)]
    for module_name in ["solver"] {
        let mut module = visit_module(module_name, &format!("{module_name}::"), WalkForNames::All).unwrap();
        module.preferred_name = module_name.to_owned();
        stdlib
            .children
            .insert(format!("M:{}", module.qual_name), DocData::Mod(module));
    }

    stdlib
}

#[derive(Clone, Debug)]
enum WalkForNames<'a> {
    All,
    Selected(Vec<&'a str>),
}

impl<'a> WalkForNames<'a> {
    fn contains(&self, name: &str) -> bool {
        match self {
            WalkForNames::All => true,
            WalkForNames::Selected(names) => names.contains(&name),
        }
    }

    #[must_use]
    fn intersect(&self, names: impl Iterator<Item = &'a str>) -> Self {
        match self {
            WalkForNames::All => WalkForNames::Selected(names.collect()),
            WalkForNames::Selected(mine) => WalkForNames::Selected(names.filter(|n| mine.contains(n)).collect()),
        }
    }
}

fn visit_module(name: &str, preferred_prefix: &str, names: WalkForNames) -> Result<ModData, String> {
    let mut result = ModData::new(name, preferred_prefix);

    let source = crate::modules::read_std(name).unwrap();
    let parsed = crate::parsing::parse_str(source, ModuleId::from_usize(0))
        .parse_errs_as_err()
        .unwrap();

    let mut summary = String::new();
    let mut description = None;
    for n in &parsed.non_code_meta.start_nodes {
        match &n.value {
            NonCodeValue::BlockComment { value, .. } if value.starts_with('/') => {
                let rest = &value[1..];
                if rest.trim().is_empty() {
                    match &mut description {
                        None => description = Some(String::new()),
                        Some(d) => d.push('\n'),
                    }
                } else {
                    let line = rest.trim_end();
                    let line = line.strip_prefix(' ').unwrap_or(line);
                    match &mut description {
                        None => {
                            summary.push_str(line.trim());
                            summary.push(' ');
                        }
                        Some(d) => {
                            d.push_str(line);
                            d.push('\n');
                        }
                    }
                }
            }
            _ => break,
        }
    }
    if !summary.is_empty() {
        result.summary = Some(summary);
    }
    result.description = description;

    for n in &parsed.body {
        if n.visibility() != ItemVisibility::Export {
            continue;
        }
        match n {
            crate::parsing::ast::types::BodyItem::ImportStatement(import) => match &import.path {
                crate::parsing::ast::types::ImportPath::Std { path } => {
                    let m = match &import.selector {
                        ImportSelector::Glob(..) => Some(visit_module(&path[1], "", names.clone())?),
                        ImportSelector::None { .. } => {
                            let name = import.module_name().unwrap();
                            if names.contains(&name) {
                                Some(visit_module(&path[1], &format!("{name}::"), WalkForNames::All)?)
                            } else {
                                None
                            }
                        }
                        ImportSelector::List { items } => Some(visit_module(
                            &path[1],
                            "",
                            names.intersect(items.iter().map(|n| &*n.name.name)),
                        )?),
                    };
                    if let Some(m) = m {
                        let key = format!("M:{}", &m.qual_name);
                        let mut dd = DocData::Mod(m);
                        dd.with_meta(&import.outer_attrs);
                        result.children.insert(key, dd);
                    }
                }
                p => return Err(format!("Unexpected import: `{p}`")),
            },
            crate::parsing::ast::types::BodyItem::VariableDeclaration(var) => {
                if !names.contains(var.name()) {
                    continue;
                }
                let qual = format!("{}::", &result.qual_name);
                let mut dd = match var.kind {
                    VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual, preferred_prefix, &result.name)),
                    VariableKind::Const => {
                        DocData::Const(ConstData::from_ast(var, qual, preferred_prefix, &result.name))
                    }
                };
                let key = format!("I:{}", dd.qual_name());
                if result.children.contains_key(&key) {
                    continue;
                }

                dd.with_meta(&var.outer_attrs);
                for a in &var.outer_attrs {
                    dd.with_comments(&a.pre_comments);
                }
                dd.with_comments(n.get_comments());

                result.children.insert(key, dd);
            }
            crate::parsing::ast::types::BodyItem::TypeDeclaration(ty) => {
                if !names.contains(ty.name()) {
                    continue;
                }
                let qual = format!("{}::", &result.qual_name);
                let mut dd = DocData::Ty(TyData::from_ast(ty, qual, preferred_prefix, &result.name));
                let key = format!("T:{}", dd.qual_name());
                if result.children.contains_key(&key) {
                    continue;
                }

                dd.with_meta(&ty.outer_attrs);
                for a in &ty.outer_attrs {
                    dd.with_comments(&a.pre_comments);
                }
                dd.with_comments(n.get_comments());

                result.children.insert(key, dd);
            }
            _ => {}
        }
    }

    Ok(result)
}

#[derive(Debug, Clone)]
pub enum DocData {
    Fn(FnData),
    Const(ConstData),
    Ty(TyData),
    Mod(ModData),
}

impl DocData {
    #[cfg(test)]
    pub fn name(&self) -> &str {
        match self {
            DocData::Fn(f) => &f.name,
            DocData::Const(c) => &c.name,
            DocData::Ty(t) => &t.name,
            DocData::Mod(m) => &m.name,
        }
    }

    pub fn preferred_name(&self) -> &str {
        match self {
            DocData::Fn(f) => &f.preferred_name,
            DocData::Const(c) => &c.preferred_name,
            DocData::Ty(t) => &t.preferred_name,
            DocData::Mod(m) => &m.preferred_name,
        }
    }

    pub fn qual_name(&self) -> &str {
        match self {
            DocData::Fn(f) => &f.qual_name,
            DocData::Const(c) => &c.qual_name,
            DocData::Ty(t) => &t.qual_name,
            DocData::Mod(m) => &m.qual_name,
        }
    }

    /// The name of the module in which the item is declared, e.g., `sketch`
    #[allow(dead_code)]
    pub fn module_name(&self) -> &str {
        match self {
            DocData::Fn(f) => &f.module_name,
            DocData::Const(c) => &c.module_name,
            DocData::Ty(t) => &t.module_name,
            DocData::Mod(m) => &m.module_name,
        }
    }

    /// The effective documentation category, considering any `doc_category` override.
    #[cfg(test)]
    pub fn doc_category(&self) -> DocCategory {
        match self {
            DocData::Fn(f) => f.properties.doc_category.unwrap_or(DocCategory::Functions),
            DocData::Const(c) => c.properties.doc_category.unwrap_or(DocCategory::Constants),
            DocData::Ty(t) => t.properties.doc_category.unwrap_or(DocCategory::Types),
            DocData::Mod(_) => DocCategory::Modules,
        }
    }

    #[cfg(test)]
    pub fn file_name(&self) -> String {
        format!(
            "{}/{}",
            self.doc_category().file_prefix(),
            self.qual_name().replace("::", "-")
        )
    }

    #[cfg(test)]
    pub fn example_name(&self) -> String {
        format!(
            "{}_{}",
            self.doc_category().example_prefix(),
            self.qual_name().replace("::", "-")
        )
    }

    /// The path to the module through which the item is accessed, e.g., `std::sketch`
    #[cfg(test)]
    pub fn mod_name(&self) -> String {
        let q = match self {
            DocData::Fn(f) => &f.qual_name,
            DocData::Const(c) => &c.qual_name,
            DocData::Ty(t) => {
                if t.properties.impl_kind == annotations::Impl::Primitive {
                    return "Primitive types".to_owned();
                }
                &t.qual_name
            }
            DocData::Mod(m) => &m.qual_name,
        };
        q[0..q.rfind("::").unwrap()].to_owned()
    }

    #[cfg(test)]
    pub fn hide(&self) -> bool {
        match self {
            DocData::Fn(f) => f.properties.doc_hidden || f.properties.deprecated,
            DocData::Const(c) => c.properties.doc_hidden || c.properties.deprecated,
            DocData::Ty(t) => t.properties.doc_hidden || t.properties.deprecated,
            DocData::Mod(_) => false,
        }
    }

    pub fn is_experimental(&self) -> bool {
        self.properties().experimental
    }

    pub fn properties(&self) -> &Properties {
        match self {
            DocData::Fn(f) => &f.properties,
            DocData::Const(c) => &c.properties,
            DocData::Ty(t) => &t.properties,
            DocData::Mod(d) => &d.properties,
        }
    }

    pub fn to_completion_item(&self) -> Option<CompletionItem> {
        match self {
            DocData::Fn(f) => Some(f.to_completion_item()),
            DocData::Const(c) => Some(c.to_completion_item()),
            DocData::Ty(t) => Some(t.to_completion_item()),
            DocData::Mod(_) => None,
        }
    }

    pub fn to_signature_help(&self) -> Option<SignatureHelp> {
        match self {
            DocData::Fn(f) => Some(f.to_signature_help()),
            DocData::Const(_) => None,
            DocData::Ty(_) => None,
            DocData::Mod(_) => None,
        }
    }

    fn with_meta(&mut self, attrs: &[Node<Annotation>]) {
        match self {
            DocData::Fn(f) => f.with_meta(attrs),
            DocData::Const(c) => c.with_meta(attrs),
            DocData::Ty(t) => t.with_meta(attrs),
            DocData::Mod(m) => m.with_meta(attrs),
        }
    }

    fn with_comments(&mut self, comments: &[String]) {
        match self {
            DocData::Fn(f) => f.with_comments(comments),
            DocData::Const(c) => c.with_comments(comments),
            DocData::Ty(t) => t.with_comments(comments),
            DocData::Mod(m) => m.with_comments(comments),
        }
    }

    fn expect_mod(&self) -> &ModData {
        match self {
            DocData::Mod(m) => m,
            _ => unreachable!(),
        }
    }

    #[cfg(test)]
    pub(super) fn summary(&self) -> Option<&String> {
        match self {
            DocData::Fn(f) => f.summary.as_ref(),
            DocData::Const(c) => c.summary.as_ref(),
            DocData::Ty(t) => t.summary.as_ref(),
            DocData::Mod(m) => m.summary.as_ref(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ConstData {
    // TODO (#11345) Only read via DocData::name() in test code; removing requires updating ~30 test call sites.
    // Also uses of name (like PI) will need special treatment
    #[allow(dead_code)]
    pub name: String,
    /// How the const is indexed, etc.
    pub preferred_name: String,
    /// The fully qualified name.
    pub qual_name: String,
    pub value: Option<String>,
    pub ty: Option<String>,
    pub properties: Properties,

    /// The summary of the function.
    pub summary: Option<String>,
    /// The description of the function.
    pub description: Option<String>,
    /// Code examples.
    /// These are tested and we know they compile and execute.
    pub examples: Vec<(String, ExampleProperties)>,

    pub module_name: String,
}

impl ConstData {
    fn from_ast(
        var: &crate::parsing::ast::types::VariableDeclaration,
        mut qual_name: String,
        preferred_prefix: &str,
        module_name: &str,
    ) -> Self {
        assert_eq!(var.kind, crate::parsing::ast::types::VariableKind::Const);

        let (value, ty) = match &var.declaration.init {
            crate::parsing::ast::types::Expr::Literal(lit) => (
                Some(lit.raw.clone()),
                Some(match &lit.value {
                    crate::parsing::ast::types::LiteralValue::Number { suffix, .. } => {
                        if *suffix == NumericSuffix::None || *suffix == NumericSuffix::Count {
                            "number".to_owned()
                        } else {
                            format!("number({suffix})")
                        }
                    }
                    crate::parsing::ast::types::LiteralValue::String { .. } => "string".to_owned(),
                    crate::parsing::ast::types::LiteralValue::Bool { .. } => "boolean".to_owned(),
                }),
            ),
            crate::parsing::ast::types::Expr::AscribedExpression(e) => (None, Some(e.ty.to_string())),
            _ => (None, None),
        };

        let name = var.declaration.id.name.clone();
        qual_name.push_str(&name);
        ConstData {
            preferred_name: format!("{preferred_prefix}{name}"),
            name,
            qual_name,
            value,
            // TODO use type decl when we have them.
            ty,
            properties: Properties {
                exported: !var.visibility.is_default(),
                deprecated: false,
                deprecated_since: None,
                experimental: false,
                doc_hidden: false,
                impl_kind: annotations::Impl::Kcl,
                doc_category: None,
            },
            summary: None,
            description: None,
            examples: Vec::new(),
            module_name: module_name.to_owned(),
        }
    }

    fn short_docs(&self) -> Option<String> {
        match (&self.summary, &self.description) {
            (None, None) => None,
            (None, Some(d)) | (Some(d), None) => Some(d.clone()),
            (Some(s), Some(d)) => Some(format!("{s}\n\n{d}")),
        }
    }

    fn to_completion_item(&self) -> CompletionItem {
        let mut detail = self.qual_name.clone();
        if let Some(ty) = &self.ty {
            detail.push_str(": ");
            detail.push_str(ty);
        }
        CompletionItem {
            label: self.preferred_name.clone(),
            label_details: Some(CompletionItemLabelDetails {
                detail: self.value.clone(),
                description: None,
            }),
            kind: self
                .properties
                .doc_category
                .map(DocCategory::to_completion_item_kind)
                .or(Some(CompletionItemKind::CONSTANT)),
            detail: Some(detail),
            documentation: self.short_docs().map(|s| {
                Documentation::MarkupContent(MarkupContent {
                    kind: MarkupKind::Markdown,
                    value: remove_md_links(&s),
                })
            }),
            deprecated: Some(self.properties.deprecated),
            preselect: None,
            sort_text: None,
            filter_text: None,
            insert_text: None,
            insert_text_format: None,
            insert_text_mode: None,
            text_edit: None,
            additional_text_edits: None,
            command: None,
            commit_characters: None,
            data: None,
            tags: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ModData {
    pub name: String,
    /// How the module is indexed, etc.
    #[allow(dead_code)]
    pub preferred_name: String,
    /// The fully qualified name.
    pub qual_name: String,
    /// The summary of the module.
    pub summary: Option<String>,
    /// The description of the module.
    pub description: Option<String>,
    pub module_name: String,
    pub properties: Properties,

    pub children: IndexMap<String, DocData>,
}

impl ModData {
    fn new(name: &str, preferred_prefix: &str) -> Self {
        let (name, qual_name, module_name) = if name == "prelude" {
            ("std", "std".to_owned(), String::new())
        } else {
            (name, format!("std::{name}"), "std".to_owned())
        };
        Self {
            preferred_name: format!("{preferred_prefix}{name}"),
            name: name.to_owned(),
            qual_name,
            summary: None,
            description: None,
            children: IndexMap::new(),
            module_name,
            properties: Properties {
                exported: false,
                deprecated: false,
                deprecated_since: None,
                experimental: false,
                doc_hidden: false,
                impl_kind: Default::default(),
                doc_category: None,
            },
        }
    }

    #[cfg(test)]
    pub fn find_by_name(&self, name: &str) -> Option<&DocData> {
        if let Some(result) = self
            .children
            .values()
            .find(|dd| dd.name() == name && !matches!(dd, DocData::Mod(_)))
        {
            return Some(result);
        }

        #[allow(clippy::iter_over_hash_type)]
        for (k, v) in &self.children {
            if k.starts_with("M:")
                && let Some(result) = v.expect_mod().find_by_name(name)
            {
                return Some(result);
            }
        }

        None
    }

    pub fn all_docs(&self) -> impl Iterator<Item = &DocData> {
        let result = self.children.values();
        // TODO really this should be recursive, currently assume std is only one module deep.
        result.chain(
            self.children
                .iter()
                .filter(|(k, _)| k.starts_with("M:"))
                .flat_map(|(_, d)| d.expect_mod().children.values()),
        )
    }
}

#[derive(Debug, Clone)]
pub struct FnData {
    /// The name of the function.
    pub name: String,
    /// How the function is indexed, etc.
    pub preferred_name: String,
    /// The fully qualified name.
    pub qual_name: String,
    /// The args of the function.
    pub args: Vec<ArgData>,
    /// The return value of the function.
    pub return_type: Option<String>,
    pub properties: Properties,

    /// The summary of the function.
    pub summary: Option<String>,
    /// The description of the function.
    pub description: Option<String>,
    /// Code examples.
    /// These are tested and we know they compile and execute.
    pub examples: Vec<(String, ExampleProperties)>,

    pub module_name: String,
}

impl FnData {
    fn from_ast(
        var: &crate::parsing::ast::types::VariableDeclaration,
        mut qual_name: String,
        preferred_prefix: &str,
        module_name: &str,
    ) -> Self {
        assert_eq!(var.kind, crate::parsing::ast::types::VariableKind::Fn);
        let crate::parsing::ast::types::Expr::FunctionExpression(expr) = &var.declaration.init else {
            unreachable!();
        };
        let name = var.declaration.id.name.clone();
        qual_name.push_str(&name);

        FnData {
            preferred_name: format!("{preferred_prefix}{name}"),
            name,
            qual_name,
            args: expr.params.iter().map(ArgData::from_ast).collect(),
            return_type: expr.return_type.as_ref().map(|t| t.to_string()),
            properties: Properties {
                exported: !var.visibility.is_default(),
                deprecated: false,
                deprecated_since: None,
                experimental: false,
                doc_hidden: false,
                impl_kind: annotations::Impl::Kcl,
                doc_category: None,
            },
            summary: None,
            description: None,
            examples: Vec::new(),
            module_name: module_name.to_owned(),
        }
    }

    fn short_docs(&self) -> Option<String> {
        match (&self.summary, &self.description) {
            (None, None) => None,
            (None, Some(d)) | (Some(d), None) => Some(d.clone()),
            (Some(s), Some(d)) => Some(format!("{s}\n\n{d}")),
        }
    }

    pub fn fn_signature(&self) -> String {
        let mut signature = String::new();

        if self.args.is_empty() {
            signature.push_str("()");
        } else if self.args.len() == 1 {
            signature.push('(');
            signature.push_str(&self.args[0].to_string());
            signature.push(')');
        } else {
            signature.push('(');
            for a in &self.args {
                signature.push_str("\n  ");
                signature.push_str(&a.to_string());
                signature.push(',');
            }
            signature.push('\n');
            signature.push(')');
        }

        if let Some(ty) = &self.return_type {
            signature.push_str(&format!(": {ty}"));
        }

        signature
    }

    fn to_completion_item(&self) -> CompletionItem {
        CompletionItem {
            label: self.name.clone(),
            label_details: Some(CompletionItemLabelDetails {
                detail: Some(self.fn_signature()),
                description: None,
            }),
            kind: self
                .properties
                .doc_category
                .map(DocCategory::to_completion_item_kind)
                .or(Some(CompletionItemKind::FUNCTION)),
            detail: Some(self.qual_name.clone()),
            documentation: self.short_docs().map(|s| {
                Documentation::MarkupContent(MarkupContent {
                    kind: MarkupKind::Markdown,
                    value: remove_md_links(&s),
                })
            }),
            deprecated: Some(self.properties.deprecated),
            preselect: None,
            sort_text: None,
            filter_text: None,
            insert_text: Some(self.to_autocomplete_snippet()),
            insert_text_format: Some(InsertTextFormat::SNIPPET),
            insert_text_mode: None,
            text_edit: None,
            additional_text_edits: None,
            command: None,
            commit_characters: None,
            data: None,
            tags: None,
        }
    }

    pub(super) fn to_autocomplete_snippet(&self) -> String {
        if self.name == "loft" {
            return "loft([${0:sketch000}, ${1:sketch001}])".to_owned();
        } else if self.name == "union" {
            return "union([${0:extrude001}, ${1:extrude002}])".to_owned();
        } else if self.name == "split" {
            return "split([${0:extrude001}], tools = [${1:extrude002}])".to_owned();
        } else if self.name == "subtract" {
            return "subtract([${0:extrude001}], tools = [${1:extrude002}])".to_owned();
        } else if self.name == "subtract2d" {
            return "subtract2d(tool = ${0:profileToSubtract})".to_owned();
        } else if self.name == "intersect" {
            return "intersect([${0:extrude001}, ${1:extrude002}])".to_owned();
        } else if self.name == "clone" {
            return "clone(${0:part001})".to_owned();
        } else if self.name == "hole" {
            return "hole(${0:holeSketch}, ${1:%})".to_owned();
        } else if self.name == "extrude" {
            return "extrude(length = ${0:10})".to_owned();
        } else if self.name == "translate" {
            return "translate(x = ${0:0}, y = ${1:0}, z = ${2:0})".to_owned();
        }
        let mut args = Vec::new();
        let mut index = 0;
        for arg in self.args.iter() {
            if let Some((i, arg_str)) = arg.get_autocomplete_snippet(index) {
                index = i + 1;
                args.push(arg_str);
            }
        }
        format!("{}({})", self.preferred_name, args.join(", "))
    }

    pub(crate) fn to_signature_help(&self) -> SignatureHelp {
        // TODO Fill this in based on the current position of the cursor.
        let active_parameter = None;

        SignatureHelp {
            signatures: vec![SignatureInformation {
                label: self.preferred_name.clone() + &self.fn_signature(),
                documentation: self.short_docs().map(|s| {
                    Documentation::MarkupContent(MarkupContent {
                        kind: MarkupKind::Markdown,
                        value: s,
                    })
                }),
                parameters: Some(self.args.iter().map(|arg| arg.to_param_info()).collect()),
                active_parameter,
            }],
            active_signature: Some(0),
            active_parameter,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocCategory {
    Functions,
    Constants,
    Modules,
    Types,
}

impl DocCategory {
    fn from_str(s: &str) -> Option<Self> {
        match s {
            "functions" => Some(DocCategory::Functions),
            "consts" => Some(DocCategory::Constants),
            "modules" => Some(DocCategory::Modules),
            "types" => Some(DocCategory::Types),
            _ => None,
        }
    }

    #[cfg(test)]
    fn file_prefix(self) -> &'static str {
        match self {
            DocCategory::Functions => "functions",
            DocCategory::Constants => "consts",
            DocCategory::Modules => "modules",
            DocCategory::Types => "types",
        }
    }

    #[cfg(test)]
    fn example_prefix(self) -> &'static str {
        match self {
            DocCategory::Functions => "fn",
            DocCategory::Constants => "const",
            DocCategory::Modules => "module",
            DocCategory::Types => "ty",
        }
    }

    fn to_completion_item_kind(self) -> CompletionItemKind {
        match self {
            DocCategory::Functions => CompletionItemKind::FUNCTION,
            DocCategory::Constants => CompletionItemKind::CONSTANT,
            DocCategory::Types => CompletionItemKind::STRUCT,
            DocCategory::Modules => CompletionItemKind::MODULE,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Properties {
    pub deprecated: bool,
    /// Constraint on the KCL version at or after which this item is deprecated,
    /// e.g. "2.0".
    pub deprecated_since: Option<VersionConstraint>,
    pub experimental: bool,
    pub doc_hidden: bool,
    #[allow(dead_code)]
    pub exported: bool,
    pub impl_kind: annotations::Impl,
    pub doc_category: Option<DocCategory>,
}

#[derive(Debug, Clone)]
pub enum ExampleSketchSyntax {
    SketchSyntaxAgnostic,
    Legacy,
    SketchSolve,
}

impl ExampleSketchSyntax {
    fn from_attr(attr: &str) -> Option<Self> {
        match attr {
            "sketchSyntaxAgnostic" => Some(Self::SketchSyntaxAgnostic),
            "legacy" | "legacySketch" | "legacySketchSyntax" | "old" | "oldSketchSyntax" => Some(Self::Legacy),
            "sketchSolve" | "sketch_solve" | "new" | "newSketchSyntax" | "sketchSolveSyntax" => Some(Self::SketchSolve),
            _ => None,
        }
    }

    fn infer_from_source(source: &str) -> Self {
        if source.contains("sketch(on =") {
            Self::SketchSolve
        } else if source.contains("startSketchOn") || source.contains("startProfile") {
            Self::Legacy
        } else {
            Self::SketchSyntaxAgnostic
        }
    }
}

#[derive(Debug, Clone)]
pub struct ExampleProperties {
    #[allow(dead_code)]
    pub norun: bool,
    #[allow(dead_code)]
    pub no3d: bool,
    pub inline: bool,
    pub sketch_syntax: ExampleSketchSyntax,
    pub sketch_syntax_explicit: bool,
}

#[derive(Debug, Clone)]
pub struct ArgData {
    /// The name of the argument.
    pub name: String,
    /// Whether this argument is experimental.
    pub experimental: bool,
    /// The type of the argument.
    pub ty: Option<String>,
    /// If the argument is required.
    pub kind: ArgKind,
    pub override_in_snippet: Option<bool>,
    /// Additional information that could be used instead of the type's description.
    /// This is helpful if the type is really basic, like "number" -- that won't tell the user much about
    /// how this argument is meant to be used.
    pub docs: Option<String>,
    /// If given, LSP should use these as completion items.
    pub snippet_array: Option<Vec<String>>,
    /// Whether this argument is deprecated regardless of the KCL version.
    pub deprecated: bool,
    /// Constraint on the KCL version at or after which this argument is deprecated.
    pub deprecated_since: Option<VersionConstraint>,
}

impl fmt::Display for ArgData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.kind {
            ArgKind::Special => write!(f, "@{}", self.name)?,
            ArgKind::Labelled(false) => f.write_str(&self.name)?,
            ArgKind::Labelled(true) => write!(f, "{}?", self.name)?,
        }
        if let Some(ty) = &self.ty {
            write!(f, ": {ty}")?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArgKind {
    Special,
    // Parameter is whether the arg is optional.
    // TODO should store default value if present
    Labelled(bool),
}

impl ArgData {
    fn from_ast(arg: &crate::parsing::ast::types::Parameter) -> Self {
        let mut result = ArgData {
            snippet_array: Default::default(),
            name: arg.identifier.name.clone(),
            experimental: arg.experimental,
            ty: arg.param_type.as_ref().map(|t| t.to_string()),
            docs: None,
            override_in_snippet: None,
            kind: if arg.labeled {
                ArgKind::Labelled(arg.optional())
            } else {
                ArgKind::Special
            },
            deprecated: arg.deprecated,
            deprecated_since: arg.deprecated_since.clone(),
        };

        for attr in &arg.identifier.outer_attrs {
            if let Annotation {
                name: None,
                properties: Some(props),
                ..
            } = &attr.inner
            {
                for p in props {
                    if p.key.name == "includeInSnippet" {
                        if let Some(b) = p.value.literal_bool() {
                            result.override_in_snippet = Some(b);
                        } else {
                            panic!(
                                "Invalid value for `includeInSnippet`, expected bool literal, found {:?}",
                                p.value
                            );
                        }
                    } else if p.key.name == "snippetArray" {
                        let Expr::ArrayExpression(arr) = &p.value else {
                            panic!(
                                "Invalid value for `snippetArray`, expected array literal, found {:?}",
                                p.value
                            );
                        };
                        let mut items = Vec::new();
                        for s in &arr.elements {
                            let Expr::Literal(lit) = s else {
                                panic!(
                                    "Invalid value in `snippetArray`, all items must be string literals but found {s:?}"
                                );
                            };
                            let LiteralValue::String(litstr) = &lit.inner.value else {
                                panic!(
                                    "Invalid value in `snippetArray`, all items must be string literals but found {s:?}"
                                );
                            };
                            items.push(litstr.to_owned());
                        }
                        result.snippet_array = Some(items);
                    }
                }
            }
        }

        result.with_comments(&arg.identifier.pre_comments);
        result
    }

    pub fn get_autocomplete_snippet(&self, index: usize) -> Option<(usize, String)> {
        match self.override_in_snippet {
            Some(false) => return None,
            None if !self.kind.required() => return None,
            _ => {}
        }

        let label = if self.kind == ArgKind::Special {
            String::new()
        } else {
            format!("{} = ", self.name)
        };
        if let Some(vals) = &self.snippet_array {
            let mut snippet = label;
            snippet.push('[');
            let n = vals.len();
            for (i, val) in vals.iter().enumerate() {
                snippet.push_str(&format!("${{{}:{}}}", index + i, val));
                if i != n - 1 {
                    snippet.push_str(", ");
                }
            }
            snippet.push(']');
            return Some((index + n - 1, snippet));
        }
        match self.ty.as_deref() {
            Some("Sketch") if self.kind == ArgKind::Special => None,
            Some(s) if s.starts_with("number") => {
                let value = match &*self.name {
                    "angleStart" => "0deg",
                    "angleEnd" => "180deg",
                    "angle" => "180deg",
                    "arcDegrees" => "360deg",
                    _ => "10",
                };
                Some((index, format!(r#"{label}${{{index}:{value}}}"#)))
            }
            Some("Point2d") => Some((index + 1, format!(r#"{label}[${{{}:0}}, ${{{}:0}}]"#, index, index + 1))),
            Some("Point3d") => Some((
                index + 2,
                format!(
                    r#"{label}[${{{}:0}}, ${{{}:0}}, ${{{}:0}}]"#,
                    index,
                    index + 1,
                    index + 2
                ),
            )),
            Some("Axis2d | Edge")
            | Some("Axis3d | Edge")
            | Some("Axis2d | Edge | any")
            | Some("Axis3d | Edge | any")
            | Some("Axis2d | Edge | Segment")
            | Some("Axis3d | Edge | Segment")
            | Some("Axis2d | Edge | Segment | any")
            | Some("Axis3d | Edge | Segment | any") => Some((index, format!(r#"{label}${{{index}:X}}"#))),
            Some("Sketch") | Some("Sketch | Helix") | Some("Sketch | Helix | [Segment; 1+]") => {
                Some((index, format!(r#"{label}${{{index}:sketch000}}"#)))
            }
            Some("Edge") => Some((index, format!(r#"{label}${{{index}:tag_or_edge_fn}}"#))),
            Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))),
            Some("Plane") | Some("Solid | Plane") => Some((index, format!(r#"{label}${{{index}:XY}}"#))),
            Some("[TaggedFace; 2]") => Some((
                index + 1,
                format!(r#"{label}[${{{}:tag}}, ${{{}:tag}}]"#, index, index + 1),
            )),

            Some("string") => {
                if self.name == "color" {
                    Some((index, format!(r"{label}${{{}:{}}}", index, "\"#ff0000\"")))
                } else {
                    Some((index, format!(r#"{label}${{{index}:"string"}}"#)))
                }
            }
            Some("bool") => Some((index, format!(r#"{label}${{{index}:false}}"#))),
            _ => None,
        }
    }

    fn to_param_info(&self) -> ParameterInformation {
        ParameterInformation {
            label: ParameterLabel::Simple(self.name.clone()),
            documentation: self.docs.as_ref().map(|docs| {
                Documentation::MarkupContent(MarkupContent {
                    kind: MarkupKind::Markdown,
                    value: docs.clone(),
                })
            }),
        }
    }

    /// Is this a normal, labelled arg,
    /// or the special unlabelled first arg?
    pub fn is_labelled(&self) -> bool {
        matches!(self.kind, ArgKind::Labelled(_))
    }
}

impl ArgKind {
    pub fn required(self) -> bool {
        match self {
            ArgKind::Special => true,
            ArgKind::Labelled(opt) => !opt,
        }
    }
}

#[derive(Debug, Clone)]
pub struct TyData {
    /// The name of the function.
    pub name: String,
    /// How the type is indexed, etc.
    pub preferred_name: String,
    /// The fully qualified name.
    pub qual_name: String,
    pub properties: Properties,
    pub alias: Option<String>,

    /// The summary of the function.
    pub summary: Option<String>,
    /// The description of the function.
    pub description: Option<String>,
    /// Code examples.
    /// These are tested and we know they compile and execute.
    pub examples: Vec<(String, ExampleProperties)>,

    pub module_name: String,
}

impl TyData {
    fn from_ast(
        ty: &crate::parsing::ast::types::TypeDeclaration,
        mut qual_name: String,
        preferred_prefix: &str,
        module_name: &str,
    ) -> Self {
        let name = ty.name.name.clone();
        qual_name.push_str(&name);

        TyData {
            preferred_name: format!("{preferred_prefix}{name}"),
            name,
            qual_name,
            properties: Properties {
                exported: !ty.visibility.is_default(),
                deprecated: false,
                deprecated_since: None,
                experimental: false,
                doc_hidden: false,
                impl_kind: annotations::Impl::Kcl,
                doc_category: None,
            },
            alias: ty.alias.as_ref().map(|t| t.to_string()),
            summary: None,
            description: None,
            examples: Vec::new(),
            module_name: module_name.to_owned(),
        }
    }

    pub fn qual_name(&self) -> &str {
        if self.properties.impl_kind == annotations::Impl::Primitive {
            &self.name
        } else {
            &self.qual_name
        }
    }

    fn short_docs(&self) -> Option<String> {
        match (&self.summary, &self.description) {
            (None, None) => None,
            (None, Some(d)) | (Some(d), None) => Some(d.clone()),
            (Some(s), Some(d)) => Some(format!("{s}\n\n{d}")),
        }
    }

    fn to_completion_item(&self) -> CompletionItem {
        CompletionItem {
            label: self.preferred_name.clone(),
            label_details: self.alias.as_ref().map(|t| CompletionItemLabelDetails {
                detail: Some(format!("type {} = {t}", self.name)),
                description: None,
            }),
            kind: self
                .properties
                .doc_category
                .map(DocCategory::to_completion_item_kind)
                .or(Some(CompletionItemKind::STRUCT)),
            detail: Some(self.qual_name().to_owned()),
            documentation: self.short_docs().map(|s| {
                Documentation::MarkupContent(MarkupContent {
                    kind: MarkupKind::Markdown,
                    value: remove_md_links(&s),
                })
            }),
            deprecated: Some(self.properties.deprecated),
            preselect: None,
            sort_text: None,
            filter_text: None,
            insert_text: Some(self.preferred_name.clone()),
            insert_text_format: Some(InsertTextFormat::SNIPPET),
            insert_text_mode: None,
            text_edit: None,
            additional_text_edits: None,
            command: None,
            commit_characters: None,
            data: None,
            tags: None,
        }
    }
}

pub(super) fn remove_md_links(s: &str) -> String {
    let re = Regex::new(r"\[([^\]]*)\]\([^\)]*\)").unwrap();
    re.replace_all(s, "$1").to_string()
}

trait ApplyMeta {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        examples: Vec<(String, ExampleProperties)>,
    );
    fn deprecated(&mut self, deprecated: bool);
    fn deprecated_since(&mut self, deprecated_since: Option<VersionConstraint>);
    fn experimental(&mut self, experimental: bool);
    fn doc_hidden(&mut self, doc_hidden: bool);
    fn impl_kind(&mut self, impl_kind: annotations::Impl);
    fn doc_category(&mut self, doc_category: DocCategory);

    fn with_comments(&mut self, comments: &[String]) {
        if comments.iter().all(|s| s.is_empty()) {
            return;
        }

        let mut summary = None;
        let mut description: Option<String> = None;
        let mut example: Option<(String, ExampleProperties)> = None;
        let mut examples = Vec::new();
        for l in comments.iter().filter(|l| l.starts_with("///")).map(|l| {
            if let Some(ll) = l.strip_prefix("/// ") {
                ll
            } else {
                &l[3..]
            }
        }) {
            #[allow(clippy::manual_strip)]
            if l.starts_with("```") {
                if let Some((e, mut p)) = example {
                    if !p.sketch_syntax_explicit {
                        p.sketch_syntax = ExampleSketchSyntax::infer_from_source(&e);
                    }
                    if p.inline {
                        description.as_mut().unwrap().push_str("```\n");
                    } else {
                        examples.push((e.trim().to_owned(), p));
                    }
                    example = None;
                } else {
                    let args = l[3..].split(',');
                    let mut inline = false;
                    let mut norun = false;
                    let mut no3d = false;
                    let mut sketch_syntax = ExampleSketchSyntax::SketchSyntaxAgnostic;
                    let mut sketch_syntax_explicit = false;
                    for a in args {
                        match a.trim() {
                            "inline" => inline = true,
                            "norun" | "no_run" => norun = true,
                            "no3d" | "no_3d" => no3d = true,
                            other => {
                                if let Some(tag) = ExampleSketchSyntax::from_attr(other) {
                                    sketch_syntax = tag;
                                    sketch_syntax_explicit = true;
                                }
                            }
                        }
                    }
                    example = Some((
                        String::new(),
                        ExampleProperties {
                            norun,
                            no3d,
                            inline,
                            sketch_syntax,
                            sketch_syntax_explicit,
                        },
                    ));

                    if inline {
                        description.as_mut().unwrap().push_str("```js\n");
                    }
                }
                continue;
            }
            if let Some((e, p)) = &mut example {
                e.push_str(l);
                e.push('\n');
                if !p.inline {
                    continue;
                }
            }

            // An empty line outside of an example. This either starts the description (with or
            // without a summary) or adds a blank line to the description.
            if l.is_empty() {
                match &mut description {
                    Some(d) => {
                        d.push('\n');
                    }
                    None => description = Some(String::new()),
                }
                continue;
            }

            // Our first line, start the summary.
            if description.is_none() && summary.is_none() {
                summary = Some(l.to_owned());
                continue;
            }

            // Append the line to either the description or summary.
            match &mut description {
                Some(d) => {
                    d.push_str(l);
                    d.push('\n');
                }
                None => {
                    let s = summary.as_mut().unwrap();
                    s.push(' ');
                    s.push_str(l);
                }
            }
        }
        assert!(example.is_none());
        if let Some(d) = &mut description
            && d.is_empty()
        {
            description = None;
        }

        self.apply_docs(
            summary.map(|s| s.trim().to_owned()),
            description.map(|s| s.trim().to_owned()),
            examples,
        );
    }

    fn with_meta(&mut self, attrs: &[Node<Annotation>]) {
        for attr in attrs {
            if let Annotation {
                name: None,
                properties: Some(props),
                ..
            } = &attr.inner
            {
                for p in props {
                    match &*p.key.name {
                        annotations::IMPL => {
                            if let Some(s) = p.value.ident_name() {
                                self.impl_kind(annotations::Impl::from_str(s).unwrap());
                            }
                        }
                        annotations::DEPRECATED => {
                            if let Some(b) = p.value.literal_bool() {
                                self.deprecated(b);
                            }
                        }
                        annotations::DEPRECATED_SINCE => {
                            if let Some(s) = p.value.literal_str()
                                && let Some(v) = VersionConstraint::parse(s)
                            {
                                self.deprecated_since(Some(v));
                            }
                        }
                        annotations::EXPERIMENTAL => {
                            if let Some(b) = p.value.literal_bool() {
                                self.experimental(b);
                            }
                        }
                        "doc_hidden" => {
                            if let Some(b) = p.value.literal_bool() {
                                self.doc_hidden(b);
                            }
                        }
                        annotations::DOC_CATEGORY => {
                            if let Some(s) = p.value.literal_str()
                                && let Some(cat) = DocCategory::from_str(s)
                            {
                                self.doc_category(cat);
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }
}

impl ApplyMeta for ConstData {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        examples: Vec<(String, ExampleProperties)>,
    ) {
        self.summary = summary;
        self.description = description;
        self.examples = examples;
    }

    fn deprecated(&mut self, deprecated: bool) {
        self.properties.deprecated = deprecated;
    }

    fn deprecated_since(&mut self, deprecated_since: Option<VersionConstraint>) {
        self.properties.deprecated_since = deprecated_since;
    }

    fn experimental(&mut self, experimental: bool) {
        self.properties.experimental = experimental;
    }

    fn doc_hidden(&mut self, doc_hidden: bool) {
        self.properties.doc_hidden = doc_hidden;
    }

    fn impl_kind(&mut self, _impl_kind: annotations::Impl) {}

    fn doc_category(&mut self, doc_category: DocCategory) {
        self.properties.doc_category = Some(doc_category);
    }
}

impl ApplyMeta for FnData {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        examples: Vec<(String, ExampleProperties)>,
    ) {
        self.summary = summary;
        self.description = description;
        self.examples = examples;
    }

    fn deprecated(&mut self, deprecated: bool) {
        self.properties.deprecated = deprecated;
    }

    fn deprecated_since(&mut self, deprecated_since: Option<VersionConstraint>) {
        self.properties.deprecated_since = deprecated_since;
    }

    fn experimental(&mut self, experimental: bool) {
        self.properties.experimental = experimental;
    }

    fn doc_hidden(&mut self, doc_hidden: bool) {
        self.properties.doc_hidden = doc_hidden;
    }

    fn impl_kind(&mut self, impl_kind: annotations::Impl) {
        self.properties.impl_kind = impl_kind;
    }

    fn doc_category(&mut self, doc_category: DocCategory) {
        self.properties.doc_category = Some(doc_category);
    }
}

impl ApplyMeta for ModData {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        examples: Vec<(String, ExampleProperties)>,
    ) {
        self.summary = summary;
        self.description = description;
        assert!(examples.is_empty());
    }

    fn deprecated(&mut self, deprecated: bool) {
        assert!(!deprecated);
    }

    fn deprecated_since(&mut self, deprecated_since: Option<VersionConstraint>) {
        assert!(deprecated_since.is_none());
    }

    fn experimental(&mut self, experimental: bool) {
        self.properties.experimental = experimental;
    }

    fn doc_hidden(&mut self, doc_hidden: bool) {
        assert!(!doc_hidden);
    }

    fn impl_kind(&mut self, _: annotations::Impl) {}

    fn doc_category(&mut self, _: DocCategory) {
        panic!("doc_category is not supported for modules");
    }
}

impl ApplyMeta for TyData {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        examples: Vec<(String, ExampleProperties)>,
    ) {
        self.summary = summary;
        self.description = description;
        self.examples = examples;
    }

    fn deprecated(&mut self, deprecated: bool) {
        self.properties.deprecated = deprecated;
    }

    fn deprecated_since(&mut self, deprecated_since: Option<VersionConstraint>) {
        self.properties.deprecated_since = deprecated_since;
    }

    fn experimental(&mut self, experimental: bool) {
        self.properties.experimental = experimental;
    }

    fn doc_hidden(&mut self, doc_hidden: bool) {
        self.properties.doc_hidden = doc_hidden;
    }

    fn impl_kind(&mut self, impl_kind: annotations::Impl) {
        self.properties.impl_kind = impl_kind;
    }

    fn doc_category(&mut self, doc_category: DocCategory) {
        self.properties.doc_category = Some(doc_category);
    }
}

impl ApplyMeta for ArgData {
    fn apply_docs(
        &mut self,
        summary: Option<String>,
        description: Option<String>,
        _examples: Vec<(String, ExampleProperties)>,
    ) {
        let Some(mut docs) = summary else {
            return;
        };
        if let Some(desc) = description {
            docs.push_str("\n\n");
            docs.push_str(&desc);
        }

        self.docs = Some(docs);
    }

    fn deprecated(&mut self, _deprecated: bool) {
        unreachable!();
    }

    fn deprecated_since(&mut self, _deprecated_since: Option<VersionConstraint>) {
        unreachable!();
    }

    fn experimental(&mut self, _experimental: bool) {
        unreachable!();
    }

    fn doc_hidden(&mut self, _doc_hidden: bool) {
        unreachable!();
    }

    fn impl_kind(&mut self, _impl_kind: annotations::Impl) {
        unreachable!();
    }

    fn doc_category(&mut self, _doc_category: DocCategory) {
        unreachable!();
    }
}

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

    use kcl_derive_docs::for_all_example_test;
    use kcl_derive_docs::for_each_example_test;

    use super::*;

    fn stdlib_module_path(module_name: &str) -> PathBuf {
        let file_stem = match module_name {
            "std" | "" => "prelude",
            other => other,
        };
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("std")
            .join(format!("{file_stem}.kcl"))
    }

    #[test]
    fn smoke() {
        let result = walk_stdlib();
        if let DocData::Const(d) = result.find_by_name("PI").unwrap()
            && d.name == "PI"
        {
            assert!(d.value.as_ref().unwrap().starts_with('3'));
            assert_eq!(d.ty, Some("number(_?)".to_owned()));
            assert_eq!(d.qual_name, "std::math::PI");
            assert!(d.summary.is_some());
            assert!(!d.examples.is_empty());
            return;
        }
        panic!("didn't find PI");
    }

    #[test]
    fn walk_stdlib_includes_solver_without_exposing_it_in_prelude() {
        let prelude = walk_prelude();
        assert!(prelude.find_by_name("coincident").is_none());

        let stdlib = walk_stdlib();
        assert!(matches!(stdlib.find_by_name("coincident"), Some(DocData::Fn(_))));
    }

    #[test]
    fn test_remove_md_links() {
        assert_eq!(
            remove_md_links("sdf dsf sd fj sdk fasdfs. asad[sdfs] dfsdf(dsfs, dsf)"),
            "sdf dsf sd fj sdk fasdfs. asad[sdfs] dfsdf(dsfs, dsf)".to_owned()
        );
        assert_eq!(remove_md_links("[]()"), "".to_owned());
        assert_eq!(remove_md_links("[foo](bar)"), "foo".to_owned());
        assert_eq!(
            remove_md_links("asdasda dsa[foo](http://www.bar/baz/qux.md). asdasdasdas asdas"),
            "asdasda dsafoo. asdasdasdas asdas".to_owned()
        );
        assert_eq!(
            remove_md_links("a [foo](bar) b [2](bar) c [_](bar)"),
            "a foo b 2 c _".to_owned()
        );
    }

    #[for_all_example_test]
    #[tokio::test(flavor = "multi_thread")]
    async fn missing_test_examples() {
        fn check_mod(m: &ModData) {
            for d in m.children.values() {
                let DocData::Fn(f) = d else {
                    continue;
                };

                for (i, (_, props)) in f.examples.iter().enumerate() {
                    if props.norun {
                        continue;
                    }
                    let name = format!("{}-{i}", f.qual_name.replace("::", "-"));
                    assert!(
                        TEST_NAMES.contains(&&*name),
                        "Missing test for example \"{name}\", maybe need to update kcl-derive-docs/src/example_tests.rs?"
                    )
                }
            }
        }

        let data = walk_stdlib();

        check_mod(&data);
        for m in data.children.values() {
            if let DocData::Mod(m) = m {
                check_mod(m);
            }
        }
    }

    #[for_each_example_test]
    #[tokio::test(flavor = "multi_thread")]
    async fn kcl_test_examples() {
        let std = walk_stdlib();

        let names = NAME.split('-');
        let mut mods: Vec<_> = names.collect();
        let number = mods.pop().unwrap();
        let number: usize = number.parse().unwrap();
        let name = mods.pop().unwrap();
        let mut qualname = mods.join("::");
        qualname.push_str("::");
        qualname.push_str(name);

        let data = if mods.len() == 1 {
            &std
        } else {
            std.children.get(&format!("M:std::{}", mods[1])).unwrap().expect_mod()
        };

        let Some(DocData::Fn(d)) = data.children.get(&format!("I:{qualname}")) else {
            panic!(
                "Could not find data for {NAME} (missing a child entry for {qualname}), maybe need to update kcl-derive-docs/src/example_tests.rs?"
            );
        };

        let source_path = stdlib_module_path(&d.module_name);
        let owner_name = d.qual_name.as_str();
        for (i, eg) in d.examples.iter().enumerate() {
            if i != number {
                continue;
            }
            eprintln!("Testing example {NAME} for {owner_name} in {}", source_path.display());
            eprintln!("KCL program:\n---\n{}\n---", eg.0.trim_end());
            let result = match crate::test_server::execute_and_snapshot_3d(&eg.0, None).await {
                Err(crate::errors::ExecError::Kcl(e)) => {
                    panic!(
                        "Error testing example {NAME} for {owner_name} in {}: {}",
                        source_path.display(),
                        e.error.message()
                    );
                }
                Err(other_err) => panic!(
                    "Error testing example {NAME} for {owner_name} in {}: {other_err}",
                    source_path.display()
                ),
                Ok(img) => img,
            };
            if eg.1.norun {
                return;
            }
            twenty_twenty::assert_image(
                format!(
                    "tests/outputs/serial_test_example_fn_{}{i}.png",
                    qualname.replace("::", "-")
                ),
                &result.image,
                0.99,
            );
            for gltf_file in result.gltf {
                let path = format!(
                    "tests/outputs/models/serial_test_example_fn_{}{i}_{}",
                    qualname.replace("::", "-"),
                    gltf_file.name,
                );
                let mut f = std::fs::File::create(path).expect("could not create file");
                std::io::Write::write_all(&mut f, &gltf_file.contents).expect("could not write to file");
            }
            return;
        }

        panic!(
            "Could not find data for {NAME} (no example {number}), maybe need to update kcl-derive-docs/src/example_tests.rs?"
        );
    }
}
