//! `EnumFoldDeriveSpec` — per-variant fragment fold emitter primitive.
//!
//! Whereas [`crate::PerVariantDeriveSpec`] emits one method per variant
//! inside an impl block, this primitive emits ONE fragment per variant
//! and folds them into a single `&[...]`, `[...]`, or `const FOO =
//! ...` payload. The whole impl block is one item, the per-variant
//! work is the comma-joined contents.
//!
//! Survey signal (pleme-io 2026-05-28): ~129+ hand-rolled
//! `pub const ALL: &'static [Self] = &[Self::A, Self::B, ...]` blocks
//! across the corpus, especially in shikumi axis primitives. One typed
//! spec collapses them.
//!
//! Authoring shape:
//!
//! ```ignore
//! EnumFoldDeriveSpec {
//!     trait_name: Ident::new("AllVariants"),
//!     target: EnumFoldTarget::UnitVariantsOnly,
//!     per_variant_fragment: "Self::#variant_name".into(),
//!     fold_template: "impl #self_name { \
//!         pub const ALL: &'static [Self] = &[#fold]; \
//!     }".into(),
//! }
//! ```
//!
//! Splice holes:
//! - `#self_name`       — the consumer enum's identifier
//! - `#variant_name`    — the variant's Ident (per-fragment)
//! - `#variant_str`     — `"<variant>"` string literal (per-fragment)
//! - `#fold`            — the comma-joined per-variant fragments (template-level)
//! - `#fold_count`      — the variant count as a `usize` literal (template-level)

use serde::{Deserialize, Serialize};
use tatara_rust_ast::{AstError, CompileToCrate, CrateScaffold, Ident};

/// What enum shapes the derive supports.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EnumFoldTarget {
    /// Only `Fields::Unit` variants accepted. For derives that produce
    /// `Self::Variant` constructors directly (e.g. `AllVariants`).
    UnitVariantsOnly,
    /// Any variant shape. For derives whose per-variant fragment
    /// doesn't construct an instance (e.g. `VariantNames`, `VariantCount`).
    AnyVariants,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnumFoldDeriveSpec {
    /// `#[derive(<trait_name>)]` user-facing identifier.
    pub trait_name: Ident,
    /// Enum shape the derive accepts.
    pub target: EnumFoldTarget,
    /// Per-variant fragment template, joined with `,` at consumer-
    /// compile-time. Splice holes: `#variant_name`, `#variant_str`,
    /// `#self_name`.
    pub per_variant_fragment: String,
    /// Impl-block template. `#fold` is replaced by the comma-joined
    /// fragments; `#fold_count` by the variant count literal. Splice
    /// holes: `#self_name`, `#fold`, `#fold_count`.
    pub fold_template: String,
}

impl EnumFoldDeriveSpec {
    fn fn_name(&self) -> String {
        let s = &self.trait_name.0;
        let mut out = String::from("derive_");
        for (i, c) in s.chars().enumerate() {
            if c.is_uppercase() {
                if i > 0 {
                    out.push('_');
                }
                out.extend(c.to_lowercase());
            } else {
                out.push(c);
            }
        }
        out
    }
}

impl CompileToCrate for EnumFoldDeriveSpec {
    fn compile_to_crate(&self, crate_name: &str) -> Result<CrateScaffold, AstError> {
        let mut s = CrateScaffold::new(crate_name, "0.1.0");
        s.add_file("Cargo.toml", render_cargo_toml(crate_name));
        s.add_file("src/lib.rs", render_lib_rs(self));
        Ok(s)
    }
}

fn render_cargo_toml(crate_name: &str) -> String {
    tatara_rust_ast::render_proc_macro_cargo_toml(
        crate_name,
        "Enum-fold derive proc-macro emitted from a tatara-rust-derive EnumFoldDeriveSpec.",
    )
}

/// Render lib.rs by string concatenation. The per-variant fragment and
/// fold template are embedded raw inside `quote!{}` so consumer-side
/// syn ident bindings (`#self_name`, `#variant_name`, `#variant_str`)
/// resolve at macro-expansion time. `#fold` and `#fold_count` are
/// substituted in proc-macro Rust BEFORE quote so the emitted body is
/// a single legal expression.
/// Render lib.rs via the typed AST substrate (proc-macro2 + quote +
/// syn + prettyplease) per the ★★ TYPED EMISSION + ★★ EMITTER
/// SUBSTRATE rules — peer of `per_field::render_lib_rs`.
fn render_lib_rs(spec: &EnumFoldDeriveSpec) -> String {
    use proc_macro2::TokenStream;
    use quote::{format_ident, quote};

    let trait_id = format_ident!("{}", spec.trait_name.0);
    let fn_name = format_ident!("{}", spec.fn_name());

    // The fold template references #fold + #fold_count which substitute
    // into quote-time tokens. The two-pass substitution via a unique
    // intermediate sentinel avoids the substring collision where
    // `#fold_count`'s shared `#fold` prefix would otherwise get clobbered.
    let fold_body_text = spec
        .fold_template
        .replace("#fold_count", "@@FOLD_COUNT_LIT@@")
        .replace("#fold", "#(#fragments),*")
        .replace("@@FOLD_COUNT_LIT@@", "#fold_count_lit");
    let fold_body: TokenStream = fold_body_text
        .parse()
        .expect("fold_template must parse as TokenStream after substitution");
    let fragment_body: TokenStream = spec
        .per_variant_fragment
        .parse()
        .expect("per_variant_fragment must parse as TokenStream");

    let unit_only_guard: TokenStream = match spec.target {
        EnumFoldTarget::UnitVariantsOnly => quote! {
            for v in &de.variants {
                if !matches!(v.fields, Fields::Unit) {
                    return syn::Error::new_spanned(
                        &v.ident,
                        "EnumFoldDerive (unit-only) requires all variants be unit variants",
                    )
                    .to_compile_error()
                    .into();
                }
            }
        },
        EnumFoldTarget::AnyVariants => quote! {},
    };

    let file: TokenStream = quote! {
        #![doc = "GENERATED by tatara-rust-derive::EnumFoldDeriveSpec."]
        #![doc = "Do not hand-edit; regenerate from the (defenum-fold …) source."]

        use proc_macro::TokenStream;
        use quote::quote;
        use syn::{Data, DataEnum, DeriveInput, Fields, parse_macro_input};

        #[proc_macro_derive(#trait_id)]
        pub fn #fn_name(input: TokenStream) -> TokenStream {
            let input = parse_macro_input!(input as DeriveInput);
            let self_name = &input.ident;

            let de = match &input.data {
                Data::Enum(de) => de,
                _ => {
                    return syn::Error::new_spanned(
                        self_name,
                        "EnumFoldDerive requires an enum",
                    )
                    .to_compile_error()
                    .into();
                }
            };

            #unit_only_guard

            let fold_count = de.variants.len();
            let fold_count_lit = proc_macro2::Literal::usize_unsuffixed(fold_count);
            let _ = fold_count_lit.clone();

            let fragments: Vec<_> = de.variants.iter().map(|v| {
                let variant_name = &v.ident;
                let variant_str = variant_name.to_string();
                quote! {
                    #fragment_body
                }
            }).collect();
            let _ = &fragments;

            let expanded = quote! {
                #fold_body
            };
            TokenStream::from(expanded)
        }
    };

    let parsed: syn::File =
        syn::parse2(file).expect("emitted lib.rs must parse as syn::File");
    prettyplease::unparse(&parsed)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tatara_rust_snapshot::assert_tokens_contain;

    fn all_variants() -> EnumFoldDeriveSpec {
        EnumFoldDeriveSpec {
            trait_name: Ident::new("AllVariants"),
            target: EnumFoldTarget::UnitVariantsOnly,
            per_variant_fragment: "Self::#variant_name".into(),
            fold_template:
                "impl #self_name { pub const ALL: &'static [Self] = &[#fold]; pub const fn all() -> &'static [Self] { Self::ALL } }"
                    .into(),
        }
    }

    fn variant_count() -> EnumFoldDeriveSpec {
        EnumFoldDeriveSpec {
            trait_name: Ident::new("VariantCount"),
            target: EnumFoldTarget::AnyVariants,
            per_variant_fragment: "1".into(),
            fold_template:
                "impl #self_name { pub const COUNT: usize = #fold_count; pub const fn count() -> usize { Self::COUNT } }"
                    .into(),
        }
    }

    fn variant_names() -> EnumFoldDeriveSpec {
        EnumFoldDeriveSpec {
            trait_name: Ident::new("VariantNames"),
            target: EnumFoldTarget::AnyVariants,
            per_variant_fragment: "#variant_str".into(),
            fold_template:
                "impl #self_name { pub const NAMES: &'static [&'static str] = &[#fold]; pub const fn names() -> &'static [&'static str] { Self::NAMES } }"
                    .into(),
        }
    }

    #[test]
    fn fn_name_snake_cases_trait_id() {
        assert_eq!(all_variants().fn_name(), "derive_all_variants");
        assert_eq!(variant_count().fn_name(), "derive_variant_count");
        assert_eq!(variant_names().fn_name(), "derive_variant_names");
    }

    #[test]
    fn cargo_toml_marks_proc_macro() {
        let scaffold = all_variants().compile_to_crate("all-variants-derive").unwrap();
        let toml = scaffold.to_files().get("Cargo.toml").unwrap().clone();
        assert!(toml.contains("proc-macro = true"));
        assert!(toml.contains(r#"name = "all-variants-derive""#));
    }

    #[test]
    fn lib_rs_extracts_enum_variants() {
        let scaffold = all_variants().compile_to_crate("all-variants-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert_tokens_contain!(&lib, "#[proc_macro_derive(AllVariants)]");
        assert_tokens_contain!(&lib, "Data::Enum(de) => de");
        assert_tokens_contain!(&lib, "de.variants.iter().map");
    }

    #[test]
    fn unit_only_target_emits_guard() {
        let scaffold = all_variants().compile_to_crate("all-variants-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert!(
            lib.contains("EnumFoldDerive (unit-only)"),
            "unit-only guard missing: {lib}"
        );
    }

    #[test]
    fn any_variants_target_omits_guard() {
        let scaffold = variant_names().compile_to_crate("variant-names-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert!(!lib.contains("EnumFoldDerive (unit-only)"));
    }

    #[test]
    fn fold_template_substitutes_fold_for_fragment_splat() {
        let scaffold = all_variants().compile_to_crate("all-variants-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        // #fold → #(#fragments),* via the substitution pass.
        assert_tokens_contain!(&lib, "#(#fragments),*");
        // Original #fold token must NOT survive into the lib.
        assert!(
            !lib.contains("&[#fold]"),
            "literal #fold token leaked into output"
        );
    }

    #[test]
    fn fold_template_substitutes_fold_count() {
        let scaffold = variant_count().compile_to_crate("variant-count-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert_tokens_contain!(&lib, "#fold_count_lit");
        assert!(!lib.contains("#fold_count;"));
    }

    #[test]
    fn every_canonical_spec_compiles_clean() {
        for spec in [all_variants(), variant_count(), variant_names()] {
            let crate_name = format!("{}-derive", spec.trait_name.0.to_lowercase());
            let scaffold = spec.compile_to_crate(&crate_name).unwrap();
            let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
            assert!(lib.contains("#[proc_macro_derive"), "{crate_name}: missing derive");
            assert!(lib.contains("self_name"), "{crate_name}: missing self_name");
        }
    }

    #[test]
    fn serde_round_trip() {
        let spec = all_variants();
        let j = serde_json::to_string(&spec).unwrap();
        let back: EnumFoldDeriveSpec = serde_json::from_str(&j).unwrap();
        assert_eq!(spec, back);
    }
}
