//! `NewtypeDeriveSpec` — single-field-struct emitter primitive.
//!
//! The newtype pattern (`pub struct Wrapper(Inner)`) is the densest
//! site of mechanical impl repetition in pleme-io's Rust corpus
//! (~840+ `impl From<T> for Wrapper` blocks per the 2026-05-28
//! survey). This emitter takes ONE template — the full impl
//! block(s) — and parameterizes it over:
//!
//! - `#self_name` — the consumer's struct identifier
//! - `#inner_ty`  — the single field's type
//!
//! At consumer-compile-time the macro extracts the inner type from
//! `Data::Struct(Fields::Unnamed)` (tuple) and refuses anything else
//! with a span-tagged compile error.
//!
//! Multiple impl blocks in one template: supported (just concat them
//! — the whole text is wrapped in `quote!{}`).
//!
//! Real worked Specs ship in `tatara-rust-examples`.

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

/// Which struct shape the derive supports. Today: tuple-newtype only
/// (`pub struct W(T);`). Named-field newtype + multi-field structs
/// land as future variants.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NewtypeTarget {
    /// `Data::Struct(_, Fields::Unnamed(one))` with exactly one tuple field.
    Tuple,
}

/// Typed spec for a derive that wraps a single-tuple-field struct.
///
/// `impl_template` is the FULL impl block(s) text — embedded raw
/// inside a `quote!{}`. Splice holes: `#self_name`, `#inner_ty`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewtypeDeriveSpec {
    /// `#[derive(<trait_name>)]` user-facing identifier.
    pub trait_name: Ident,
    /// Input shape we accept (today: tuple-newtype only).
    pub target: NewtypeTarget,
    /// Full impl-block(s) template. Concatenated raw into a quote!{}.
    /// Splice holes: `#self_name`, `#inner_ty`.
    pub impl_template: String,
}

impl NewtypeDeriveSpec {
    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 NewtypeDeriveSpec {
    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,
        "Newtype derive proc-macro emitted from a tatara-rust-derive NewtypeDeriveSpec.",
    )
}

/// Render lib.rs via the typed AST substrate (proc-macro2 + quote +
/// syn + prettyplease). The user-supplied `impl_template` parses to
/// a TokenStream and embeds verbatim into the inner quote!{} that
/// runs at consumer compile time — `#inner_ty` and `#self_name`
/// splice holes resolve there, not here.
fn render_lib_rs(spec: &NewtypeDeriveSpec) -> 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());

    let impl_body: TokenStream = spec
        .impl_template
        .parse()
        .expect("impl_template must parse as TokenStream");

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

        use proc_macro::TokenStream;
        use quote::quote;
        use syn::{Data, DataStruct, 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 inner_ty = match &input.data {
                Data::Struct(DataStruct { fields: Fields::Unnamed(u), .. })
                    if u.unnamed.len() == 1 =>
                {
                    &u.unnamed.first().expect("one field").ty
                }
                _ => {
                    return syn::Error::new_spanned(
                        self_name,
                        "NewtypeDerive requires a single-tuple-field struct: `pub struct W(Inner);`",
                    )
                    .to_compile_error()
                    .into();
                }
            };

            let expanded = quote! {
                #impl_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 impl_from() -> NewtypeDeriveSpec {
        NewtypeDeriveSpec {
            trait_name: Ident::new("ImplFrom"),
            target: NewtypeTarget::Tuple,
            impl_template:
                "impl ::std::convert::From<#inner_ty> for #self_name { fn from(v: #inner_ty) -> Self { Self(v) } } \
                 impl ::std::convert::From<#self_name> for #inner_ty { fn from(w: #self_name) -> Self { w.0 } }"
                    .into(),
        }
    }

    fn asref_newtype() -> NewtypeDeriveSpec {
        NewtypeDeriveSpec {
            trait_name: Ident::new("AsRefNewtype"),
            target: NewtypeTarget::Tuple,
            impl_template:
                "impl ::std::convert::AsRef<#inner_ty> for #self_name { fn as_ref(&self) -> &#inner_ty { &self.0 } }"
                    .into(),
        }
    }

    fn deref_newtype() -> NewtypeDeriveSpec {
        NewtypeDeriveSpec {
            trait_name: Ident::new("DerefNewtype"),
            target: NewtypeTarget::Tuple,
            impl_template:
                "impl ::std::ops::Deref for #self_name { type Target = #inner_ty; fn deref(&self) -> &#inner_ty { &self.0 } }"
                    .into(),
        }
    }

    fn inner_newtype() -> NewtypeDeriveSpec {
        NewtypeDeriveSpec {
            trait_name: Ident::new("InnerNewtype"),
            target: NewtypeTarget::Tuple,
            impl_template:
                "impl #self_name { pub fn inner(&self) -> &#inner_ty { &self.0 } pub fn into_inner(self) -> #inner_ty { self.0 } }"
                    .into(),
        }
    }

    #[test]
    fn fn_name_snake_cases_trait_id() {
        assert_eq!(impl_from().fn_name(), "derive_impl_from");
        assert_eq!(asref_newtype().fn_name(), "derive_as_ref_newtype");
    }

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

    #[test]
    fn lib_rs_emits_derive_fn_with_unnamed_field_extraction() {
        let scaffold = impl_from().compile_to_crate("impl-from-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert_tokens_contain!(&lib, "#[proc_macro_derive(ImplFrom)]");
        assert_tokens_contain!(&lib, "pub fn derive_impl_from");
        assert_tokens_contain!(&lib, "Fields::Unnamed");
        assert_tokens_contain!(&lib, "u.unnamed.first()");
    }

    #[test]
    fn lib_rs_embeds_template_under_quote() {
        let scaffold = impl_from().compile_to_crate("impl-from-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert_tokens_contain!(&lib, "#self_name");
        assert_tokens_contain!(&lib, "#inner_ty");
        assert_tokens_contain!(&lib, "From<#inner_ty> for #self_name");
    }

    #[test]
    fn lib_rs_rejects_non_tuple_with_typed_error() {
        let scaffold = impl_from().compile_to_crate("impl-from-derive").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        // The error message is a string literal in the emitted code —
        // assert on its quoted form so canonical_tokens parses it as
        // a single Literal token (not as hyphen-separated idents).
        assert_tokens_contain!(
            &lib,
            r#""NewtypeDerive requires a single-tuple-field struct: `pub struct W(Inner);`""#
        );
        assert_tokens_contain!(&lib, "syn :: Error :: new_spanned");
    }

    #[test]
    fn every_canonical_spec_compiles_clean() {
        for spec in [impl_from(), asref_newtype(), deref_newtype(), inner_newtype()] {
            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();
            // Token-stable contract: must declare a proc-macro derive
            // and use #self_name as the type interpolation marker.
            // Fragments must be balanced syntax — interpolate the
            // spec's trait name instead of using a hanging bracket.
            let trait_name = &spec.trait_name.0;
            let expected_attr = format!("#[proc_macro_derive({trait_name})]");
            assert_tokens_contain!(&lib, &expected_attr);
            assert_tokens_contain!(&lib, "#self_name");
        }
    }

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