#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]

//! Types and trait for TOML configuration documentation schemas.
//!
//! This crate defines the `ConfigDocSchema` trait and `FieldDoc` type that are
//! implemented by the `#[derive(ConfigDoc)]` proc macro. The docs site uses
//! these to generate the configuration reference page.

use std::collections::BTreeMap;

#[cfg(test)]
extern crate self as hyperchad_docs_config;

/// Nested schema metadata for a field that expands into dotted sub-keys.
#[derive(Clone)]
pub enum NestedFieldDoc {
    /// Inline nested struct, expanded as `<parent>.<child>`.
    Inline {
        /// Child field docs from the nested schema.
        fields: Vec<FieldDoc>,
        /// Default values keyed by child field name.
        defaults: BTreeMap<String, String>,
    },
    /// Map value schema, expanded as `<parent>.<key_placeholder>.<child>`.
    Map {
        /// Placeholder label rendered in docs for dynamic map keys.
        key_placeholder: &'static str,
        /// Child field docs from the map value schema.
        value_fields: Vec<FieldDoc>,
        /// Default values keyed by child field name.
        value_defaults: BTreeMap<String, String>,
    },
    /// List item schema, expanded as `<parent>.<index_placeholder>.<child>`.
    List {
        /// Placeholder label rendered in docs for dynamic list indexes.
        index_placeholder: &'static str,
        /// Child field docs from the list item schema.
        item_fields: Vec<FieldDoc>,
        /// Default values keyed by list item field name.
        item_defaults: BTreeMap<String, String>,
    },
    /// Primitive or opaque map value schema, expanded as `<parent>.<key_placeholder>`.
    MapValue {
        /// Placeholder label rendered in docs for dynamic map keys.
        key_placeholder: &'static str,
        /// Human-readable map value type name.
        value_type_display: &'static str,
        /// Description rendered for each dynamic map entry.
        value_description: &'static str,
        /// For enum-typed map values, the list of valid TOML values.
        value_enum_values: Option<&'static [&'static str]>,
    },
    /// Primitive or opaque list item schema, expanded as `<parent>.<index_placeholder>`.
    ListValue {
        /// Placeholder label rendered in docs for dynamic list indexes.
        index_placeholder: &'static str,
        /// Human-readable list item type name.
        item_type_display: &'static str,
        /// Description rendered for each dynamic list item.
        item_description: &'static str,
        /// For enum-typed list items, the list of valid TOML values.
        item_enum_values: Option<&'static [&'static str]>,
    },
}

/// Metadata for a single configuration field.
#[derive(Clone)]
pub struct FieldDoc {
    /// TOML key name for this field.
    pub toml_key: &'static str,
    /// Human-readable type name (e.g. "bool", "string", "integer").
    pub type_display: &'static str,
    /// Description extracted from the field's `///` doc comment.
    pub description: &'static str,
    /// For enum-typed fields, the list of valid TOML values.
    pub enum_values: Option<&'static [&'static str]>,
    /// Optional nested schema metadata for dotted-key expansion.
    pub nested: Option<NestedFieldDoc>,
}

/// Trait implemented by config structs via `#[derive(ConfigDoc)]`.
///
/// Provides all metadata needed to generate a configuration reference.
pub trait ConfigDocSchema: Default {
    /// TOML section name (e.g. `"general"`, `"behavior"`).
    fn section_name() -> &'static str;

    /// Section description extracted from the struct's doc comment.
    fn section_description() -> &'static str;

    /// Metadata for each field in this config section.
    fn field_docs() -> Vec<FieldDoc>;

    /// Default values for each field, serialized as TOML strings.
    ///
    /// Keys are TOML field names, values are the serialized default.
    /// Generated by constructing `Self::default()` and serializing each field.
    fn default_values() -> BTreeMap<String, String>;
}

#[cfg(test)]
mod tests {
    use super::{ConfigDocSchema, NestedFieldDoc};
    use hyperchad_docs_config_derive::{ConfigDoc, ConfigDocEnum};
    use serde::{Deserialize, Serialize};
    use std::collections::BTreeMap;

    #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, ConfigDocEnum)]
    #[serde(rename_all = "snake_case")]
    enum Mode {
        #[default]
        Auto,
        Manual,
    }

    #[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigDoc)]
    #[config_doc(section = "child")]
    struct ChildConfig {
        /// Child switch.
        pub enabled: bool,
    }

    #[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigDoc)]
    #[config_doc(section = "sample")]
    struct SampleConfig {
        /// Dynamic boolean map.
        #[config_doc(map_key = "<flag>")]
        pub flags: BTreeMap<String, bool>,
        /// Dynamic opaque request map.
        #[config_doc(
            map_key = "<request-key>",
            value_type = "any",
            value_description = "Provider request value."
        )]
        pub request: BTreeMap<String, serde_json::Value>,
        /// Dynamic enum list.
        #[config_doc(list_index = "<index>")]
        pub modes: Vec<Mode>,
        /// Nested map.
        #[config_doc(nested, map_key = "<child>")]
        pub children: BTreeMap<String, ChildConfig>,
        /// Skipped runtime field.
        #[allow(dead_code)]
        #[config_doc(skip)]
        #[serde(skip)]
        pub skipped: String,
    }

    #[test]
    fn derive_documents_dynamic_map_and_list_values() {
        let docs = SampleConfig::field_docs();

        let flags = docs.iter().find(|field| field.toml_key == "flags").unwrap();
        let Some(NestedFieldDoc::MapValue {
            key_placeholder,
            value_type_display,
            ..
        }) = &flags.nested
        else {
            panic!("flags should be a dynamic map value");
        };
        assert_eq!((*key_placeholder, *value_type_display), ("<flag>", "bool"));

        let request = docs
            .iter()
            .find(|field| field.toml_key == "request")
            .unwrap();
        let Some(NestedFieldDoc::MapValue {
            key_placeholder,
            value_type_display,
            value_description,
            ..
        }) = &request.nested
        else {
            panic!("request should be a dynamic map value");
        };
        assert_eq!(
            (*key_placeholder, *value_type_display),
            ("<request-key>", "any")
        );
        assert_eq!(*value_description, "Provider request value.");

        let modes = docs.iter().find(|field| field.toml_key == "modes").unwrap();
        let Some(NestedFieldDoc::ListValue {
            index_placeholder,
            item_type_display,
            item_enum_values,
            ..
        }) = &modes.nested
        else {
            panic!("modes should be a dynamic list value");
        };
        assert_eq!(
            (*index_placeholder, *item_type_display),
            ("<index>", "string")
        );
        assert_eq!(*item_enum_values, Some(&["auto", "manual"][..]));

        let children = docs
            .iter()
            .find(|field| field.toml_key == "children")
            .unwrap();
        assert!(matches!(children.nested, Some(NestedFieldDoc::Map { .. })));
        assert!(docs.iter().all(|field| field.toml_key != "skipped"));
    }

    #[test]
    fn derive_renders_empty_map_and_list_defaults() {
        let defaults = SampleConfig::default_values();

        assert_eq!(defaults.get("flags").map(String::as_str), Some("{}"));
        assert_eq!(defaults.get("modes").map(String::as_str), Some("[]"));
    }
}
