use crate::helpers::{filter_custom_parameter_attributes, is_testr_attribute};
use darling::ast::NestedMeta;
use darling::{Error, FromMeta};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{
    Attribute, FnArg, GenericArgument, ItemFn, LitStr, Pat, PatType, Path, PathArguments,
    PathSegment, ReturnType, Token, Type, TypeParamBound, TypePath, parse_macro_input,
};

/// Sharing strategy declared via `#[test_dep(scope = ...)]`. Parsed from
/// `darling` metadata; mirrors `test_r_core::internal::DepScope` but lives in
/// the proc-macro crate to avoid a runtime dependency.
///
/// We accept both bare identifiers (`scope = PerWorker`) and string literals
/// (`scope = "PerWorker"`), via a custom `FromMeta` implementation. The bare
/// identifier form is the documented surface; the string form remains for
/// users who prefer it or whose tooling otherwise rewrites attribute values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum Scope {
    #[default]
    Shared,
    PerWorker,
    Cloneable,
    Hosted,
    /// Owner held in parent; workers call back over IPC via a generated or
    /// manually-written stub. Requires the dep return type to implement
    /// `HostedRpcDep` and a `stub = StubType` attribute that names the
    /// worker-visible handle type tests parameterise on.
    HostedRpc,
}

impl Scope {
    fn as_tokens(self) -> proc_macro2::TokenStream {
        match self {
            Scope::Shared => quote!(test_r::core::DepScope::Shared),
            Scope::PerWorker => quote!(test_r::core::DepScope::PerWorker),
            Scope::Cloneable => quote!(test_r::core::DepScope::Cloneable),
            Scope::Hosted => quote!(test_r::core::DepScope::Hosted),
            Scope::HostedRpc => quote!(test_r::core::DepScope::HostedRpc),
        }
    }

    fn from_ident_name(name: &str) -> Option<Self> {
        match name {
            "Shared" => Some(Scope::Shared),
            "PerWorker" => Some(Scope::PerWorker),
            "Cloneable" => Some(Scope::Cloneable),
            "Hosted" => Some(Scope::Hosted),
            "HostedRpc" => Some(Scope::HostedRpc),
            _ => None,
        }
    }
}

/// Parsed value of `#[test_dep(... worker = …)]`. The picker says what shape
/// the **worker side** of a parent-hosted dep sees. The owner always lives in
/// the parent process; this enum only chooses the worker-side handle.
///
/// Surface (only valid together with `scope = Hosted`):
///
/// - `worker = descriptor` — workers reconstruct the dep value from
///   bytes via [`HostedDep`] / [`AsyncHostedDep`]. Same shape as
///   `scope = Hosted` and is the implicit default when `worker = …` is
///   omitted.
/// - `worker = rpc(Trait)` — workers receive a `<Trait>Stub`
///   generated by `#[hosted_rpc]`; calls round-trip to the parent's
///   owner via the IPC HostedRpc transport. Equivalent to the legacy
///   `scope = HostedRpc, stub = <Trait>Stub` registration and the
///   macro currently translates one to the other internally.
/// - `worker = both(Trait)` — workers receive **both** the
///   descriptor-reconstructed handle AND the `<Trait>Stub`. Targets
///   the `EnvBasedTestDependencies` shape where bulk-data gRPC keeps
///   the descriptor path and a small control surface goes over RPC.
///   This is lowered to two coordinated registrations for one dep.
#[derive(Debug, Clone)]
pub(crate) enum WorkerView {
    Descriptor,
    Rpc(Path),
    Both(Path),
}

impl darling::FromMeta for WorkerView {
    fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
        let nv = match item {
            syn::Meta::NameValue(nv) => nv,
            _ => {
                return Err(darling::Error::unsupported_format(
                    "`worker` expects `worker = descriptor`, \
                     `worker = rpc(Trait)`, or `worker = both(Trait)`",
                )
                .with_span(item));
            }
        };

        match &nv.value {
            // `worker = descriptor`
            syn::Expr::Path(p) => {
                let ident = p.path.get_ident().ok_or_else(|| {
                    darling::Error::unsupported_format(
                        "`worker` expects `descriptor`, `rpc(Trait)`, or `both(Trait)`",
                    )
                    .with_span(p)
                })?;
                match ident.to_string().as_str() {
                    "descriptor" => Ok(WorkerView::Descriptor),
                    other => Err(darling::Error::unknown_value(other).with_span(ident)),
                }
            }
            // `worker = rpc(Trait)` or `worker = both(Trait)`
            syn::Expr::Call(call) => {
                let func_ident = match &*call.func {
                    syn::Expr::Path(p) => p.path.get_ident().cloned(),
                    _ => None,
                }
                .ok_or_else(|| {
                    darling::Error::unsupported_format(
                        "`worker` call form must be `rpc(Trait)` or `both(Trait)`",
                    )
                    .with_span(&*call.func)
                })?;

                if call.args.len() != 1 {
                    return Err(darling::Error::unsupported_format(
                        "`worker = rpc(...)` / `worker = both(...)` expect \
                         exactly one trait path",
                    )
                    .with_span(call));
                }

                let trait_path = match &call.args[0] {
                    syn::Expr::Path(p) => p.path.clone(),
                    other => {
                        return Err(darling::Error::unsupported_format(
                            "the argument to `worker = rpc(...)` / `worker = both(...)` \
                             must be a trait path",
                        )
                        .with_span(other));
                    }
                };

                // Reject trait paths that carry generic arguments such
                // as `rpc(Trait::<T>)` or `rpc(mod::Trait<T>)`. The
                // derived stub path would just blindly mirror them and
                // almost certainly point at a non-existent
                // `TraitStub::<T>`, so fail fast with a clear message
                // rather than letting downstream typeck blame the wrong
                // span.
                if trait_path
                    .segments
                    .iter()
                    .any(|s| !matches!(s.arguments, syn::PathArguments::None))
                {
                    return Err(darling::Error::unsupported_format(
                        "`worker = rpc(...)` / `worker = both(...)` expect \
                         a plain trait path without generic arguments \
                         (the matching `<TraitName>Stub` would inherit them)",
                    )
                    .with_span(&trait_path));
                }

                match func_ident.to_string().as_str() {
                    "rpc" => Ok(WorkerView::Rpc(trait_path)),
                    "both" => Ok(WorkerView::Both(trait_path)),
                    other => Err(darling::Error::unknown_value(other).with_span(&func_ident)),
                }
            }
            _ => Err(darling::Error::unsupported_format(
                "`worker` expects `descriptor`, `rpc(Trait)`, or `both(Trait)`",
            )
            .with_span(&nv.value)),
        }
    }
}

impl WorkerView {
    /// Derive the `<TraitName>Stub` type path from a `rpc(Trait)` /
    /// `both(Trait)` trait reference. The stub is the type the macro
    /// `#[hosted_rpc]` generates next to the trait, named by
    /// appending `Stub` to the last segment of the trait path while
    /// preserving any module qualifier prefix.
    fn stub_path_from_trait(trait_path: &Path) -> Path {
        let mut stub_path = trait_path.clone();
        if let Some(last) = stub_path.segments.last_mut() {
            // Mirror the ident-building strategy used by
            // `#[hosted_rpc]` in `hosted_rpc.rs` (`format_ident!(
            // "{}Stub", trait_ident)`) so the two derivations stay in
            // lockstep on raw idents / hygiene edge cases.
            let new_ident = quote::format_ident!("{}Stub", last.ident);
            last.ident = new_ident;
        }
        stub_path
    }
}

impl darling::FromMeta for Scope {
    fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
        match item {
            // `scope = "PerWorker"` form.
            syn::Meta::NameValue(nv) => {
                if let syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Str(s),
                    ..
                }) = &nv.value
                {
                    let name = s.value();
                    Scope::from_ident_name(&name)
                        .ok_or_else(|| darling::Error::unknown_value(&name).with_span(s))
                } else if let syn::Expr::Path(p) = &nv.value {
                    // `scope = PerWorker` form (unquoted ident).
                    let ident = p.path.get_ident().ok_or_else(|| {
                        darling::Error::unsupported_format(
                            "scope must be one of Shared, PerWorker, Cloneable, Hosted, HostedRpc",
                        )
                        .with_span(p)
                    })?;
                    let name = ident.to_string();
                    Scope::from_ident_name(&name)
                        .ok_or_else(|| darling::Error::unknown_value(&name).with_span(ident))
                } else {
                    Err(darling::Error::unsupported_format(
                        "scope must be one of Shared, PerWorker, Cloneable, Hosted, HostedRpc",
                    )
                    .with_span(&nv.value))
                }
            }
            _ => Err(darling::Error::unsupported_format(
                "scope expects `scope = Value` or `scope = \"Value\"`",
            )
            .with_span(item)),
        }
    }

    fn from_string(value: &str) -> darling::Result<Self> {
        Scope::from_ident_name(value).ok_or_else(|| darling::Error::unknown_value(value))
    }
}

pub fn test_dep(attr: TokenStream, item: TokenStream) -> TokenStream {
    let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
        Ok(v) => v,
        Err(e) => {
            return TokenStream::from(Error::from(e).write_errors());
        }
    };

    let args = match TestDepArgs::from_list(&attr_args) {
        Ok(v) => v,
        Err(e) => {
            return TokenStream::from(e.write_errors());
        }
    };

    let mut ast: ItemFn = syn::parse(item).expect("test ast");
    let ctor_name = ast.sig.ident.clone();

    let dep_type = match &ast.sig.output {
        ReturnType::Default => {
            panic!("Dependency constructor must have a return type")
        }
        ReturnType::Type(_, typ) => match &**typ {
            Type::Path(path) => path.clone(),
            _ => {
                panic!("Dependency constructor must return a single concrete type")
            }
        },
    };

    // `worker = both(Trait)` is lowered into two `RegisteredDependency`
    // entries that share one parent owner via `HostedBothShared`. The shape is
    // materially different from the single-registration path (two ctors, two
    // getters, a shared weak cache), so we intercept here and delegate to a
    // dedicated expander instead of plumbing it through every branch below.
    if let (Some(declared_scope), Some(WorkerView::Both(trait_path))) =
        (args.scope, args.worker.as_ref())
    {
        if !matches!(declared_scope, Scope::Hosted) {
            // We intercept the picker before the general translation runs, so
            // defend the invariant here: `worker = both(T)` only applies on a
            // declared `scope = Hosted` dep.
            panic!(
                "`worker = both(...)` is only valid together with `scope = Hosted` \
                 (saw `scope = {declared_scope:?}`)."
            );
        }
        return expand_hosted_both_dep(&args, ast, trait_path);
    }

    // Keep a copy of the *user-declared* scope before worker-view translation
    // rewrites it. The `async_worker` deprecation/diagnostic below is reasoned
    // about from the user's surface, not from the post-translation effective
    // scope.
    let declared_scope = args.scope.unwrap_or_default();
    let mut scope = declared_scope;

    // Translate the `worker = …` picker into the existing (`scope`, `stub = …`)
    // representation so the rest of the code-gen path doesn't need to know
    // about the picker. `worker = rpc(Trait)` produces a translated stub; the
    // descriptor and both branches are handled directly.
    let translated_stub_from_worker: Option<Path> = match (scope, args.worker.as_ref()) {
        // No picker → no translation. Existing code-gen runs unchanged.
        (_, None) => None,

        // `worker = descriptor` on a Hosted dep is the documented
        // default behavior. Accept it as a no-op so users can spell
        // out their intent explicitly.
        (Scope::Hosted, Some(WorkerView::Descriptor)) => None,

        // `worker = rpc(Trait)` on a Hosted dep is shorthand for
        // `scope = HostedRpc, stub = <Trait>Stub`. Translate and run the
        // existing HostedRpc code-gen path.
        (Scope::Hosted, Some(WorkerView::Rpc(trait_path))) => {
            scope = Scope::HostedRpc;
            Some(WorkerView::stub_path_from_trait(trait_path))
        }

        // `worker = both(Trait)` is intercepted at the top of `test_dep()` and
        // routed through `expand_hosted_both_dep`; control should never reach
        // this arm in practice. Keep it explicit so future refactors that
        // remove the early branch fail loudly instead of silently dropping the
        // picker.
        (Scope::Hosted, Some(WorkerView::Both(_))) => {
            unreachable!("`worker = both(...)` must be handled by the early branch in test_dep()");
        }

        // `worker = …` on any scope other than `Hosted` is rejected:
        // Cloneable / PerWorker / Shared deps auto-derive their worker
        // handle shape from the dep type alone, and HostedRpc keeps its
        // legacy `stub = …` form so existing call sites don't churn.
        (other_scope, Some(_)) => {
            panic!(
                "`worker = …` is only valid together with `scope = Hosted` \
                 (saw `scope = {other_scope:?}`). Drop `worker = …`, or \
                 switch the scope."
            );
        }
    };

    // For HostedRpc: tests parameterise on the *stub* type, not the owner
    // type. The constructor returns the owner; we register the dependency
    // under the stub type so that `#[test]` parameters of type `&Stub`
    // resolve correctly.
    //
    // Two sources of the stub path:
    //   1. legacy: `#[test_dep(scope = HostedRpc, stub = X)]` → `args.stub`
    //   2. shorthand: `#[test_dep(scope = Hosted, worker = rpc(Trait))]`
    //      translated above into `translated_stub_from_worker`.
    // Specifying both at once is a user error and rejected explicitly.
    if args.stub.is_some() && translated_stub_from_worker.is_some() {
        panic!(
            "`stub = …` and `worker = rpc(…)` both name the worker-side \
             stub type. Pick one: either keep the legacy \
             `scope = HostedRpc, stub = StubType` form, or use the \
             `scope = Hosted, worker = rpc(Trait)` shorthand."
        );
    }

    // `async_worker` is deprecated and no longer controls codegen on any path.
    // The flag is ignored on every Hosted path, including the RPC translation,
    // and the user will get the standard deprecation warning when the dep is
    // registered. We intentionally do not special-case this combination.

    let stub_type_path: Option<TypePath> = match (
        scope,
        args.stub.as_ref(),
        translated_stub_from_worker.as_ref(),
    ) {
        (Scope::HostedRpc, Some(stub_path), _) => Some(TypePath {
            qself: None,
            path: stub_path.clone(),
        }),
        (Scope::HostedRpc, None, Some(translated)) => Some(TypePath {
            qself: None,
            path: translated.clone(),
        }),
        (Scope::HostedRpc, None, None) => {
            panic!(
                "`scope = HostedRpc` requires `stub = StubType` so the runtime \
                 knows which type to inject into tests. The constructor returns \
                 the owner; tests parameterise on the stub."
            );
        }
        (_, Some(_), _) => {
            panic!("`stub = ...` is only valid together with `scope = HostedRpc`.");
        }
        (_, None, _) => None,
    };

    let dep_name_str = match &stub_type_path {
        Some(stub_path) => type_path_to_string(stub_path, args.tagged_as.into()),
        None => type_path_to_string(&dep_type, args.tagged_as.into()),
    };
    let register_ident = Ident::new(
        &format!("test_r_register_dep_{dep_name_str}"),
        Span::call_site(),
    );

    let is_async = ast.sig.asyncness.is_some();
    let (dep_getters, dep_names, _dep_dimensions) = get_dependency_params(&ast, false);

    if !_dep_dimensions.is_empty() {
        panic!("Matrix dimensions are not supported on #[test_dep] constructor parameters");
    }
    filter_custom_parameter_attributes(&mut ast);

    // `worker = ...` is fully wired above (translated into the existing
    // `scope`/`stub` representation). All remaining validation lives in the
    // translation `match` block, so no further rejection is needed here.

    // Constructor dependencies are resolved in the parent materialisation
    // context for Cloneable, Hosted, and HostedRpc scopes. Workers still treat
    // those shared dependency values as leaves reconstructed from wire bytes
    // or from an RPC channel, so constructor-only dependencies are not
    // instantiated in worker subprocesses.

    // `async_worker = true` is deprecated and no longer controls codegen. The Hosted helper functions in
    // `test_r::core` now auto-select the right reconstructor based on
    // whether `test-r-core`'s `tokio` cargo feature is enabled (which
    // is what selects the tokio test runner). The flag is still parsed
    // for one transition release so existing source compiles, but it
    // is reduced to one of two diagnostics:
    //
    // - `async_worker = true` on any non-Hosted scope is still a hard
    //   error, because it was never meaningful there.
    // - `async_worker = true` on `scope = Hosted` injects a
    //   `#[deprecated]` helper call into the generated registration
    //   block, so users get a real rustc deprecation warning pointing
    //   at the registration site of the dep. The flag is otherwise
    //   ignored: the generated codec / reconstructor always go through
    //   the new helpers.
    // Key all `async_worker` diagnostics off the user-declared scope, not the
    // post-translation effective scope. Otherwise `scope = Hosted,
    // worker = rpc(T)` would hit the non-Hosted hard error even though the user
    // wrote `scope = Hosted`.
    let async_worker = args.async_worker;
    if async_worker && !matches!(declared_scope, Scope::Hosted) {
        panic!(
            "`async_worker` has been removed for non-Hosted scopes; \
             it was only ever meaningful with `scope = Hosted`. Remove \
             the flag (and, if needed, set `scope = Hosted`)."
        );
    }
    let async_worker_deprecation_tokens = if async_worker {
        quote! {
            {
                // The body is empty: the warning fires solely from the
                // `#[deprecated]`-tagged item being *called*. The note
                // is shown verbatim by rustc. The wording is
                // intentionally worker-view-neutral so it applies
                // equally to plain `scope = Hosted` and to the
                // `worker = rpc(T)` / `worker = both(T)` variants.
                #[deprecated(
                    note = "`async_worker` is deprecated and ignored on \
                            `scope = Hosted` deps. test-r now selects the \
                            worker-side behavior automatically from the \
                            Hosted registration shape (descriptor / RPC \
                            / both) and the active runtime (sync / \
                            tokio). Remove `async_worker`."
                )]
                fn __test_r_async_worker_deprecated() {}
                __test_r_async_worker_deprecated();
            }
        }
    } else {
        quote! {}
    };

    let scope_tokens = scope.as_tokens();
    let dep_ty = &dep_type;

    // Cloneable codec + worker reconstructor, only emitted for scope=Cloneable.
    let (cloneable_codec_expr, cloneable_worker_fn_expr) = if matches!(scope, Scope::Cloneable) {
        let codec = quote! {
            Some(test_r::core::CloneableCodec {
                to_wire: std::sync::Arc::new(|__any: std::sync::Arc<dyn std::any::Any + Send + Sync>| {
                    let __value: std::sync::Arc<#dep_ty> = __any
                        .downcast::<#dep_ty>()
                        .expect("Cloneable dependency type mismatch in to_wire");
                    <#dep_ty as test_r::core::CloneableDep>::to_wire(&*__value)
                }),
                from_wire_bytes: std::sync::Arc::new(|__bytes: &[u8]| {
                    let __wire_value: #dep_ty = <#dep_ty as test_r::core::CloneableDep>::from_wire(__bytes);
                    let __boxed: std::sync::Arc<dyn std::any::Any + Send + Sync> =
                        std::sync::Arc::new(__wire_value);
                    __boxed
                }),
            })
        };
        let worker_fn = quote! {
            Some(test_r::core::WorkerReconstructor::Sync(std::sync::Arc::new(
                |__wire_payload: std::sync::Arc<dyn std::any::Any + Send + Sync>, _deps| {
                    // The wire payload is already the reconstructed dep value.
                    __wire_payload
                },
            )))
        };
        (codec, worker_fn)
    } else {
        (quote! { None }, quote! { None })
    };

    // Hosted descriptor codec + worker reconstructor are built by feature-gated
    // helpers in `test-r-core`. The helpers are cfg-selected on the `tokio`
    // cargo feature in `test-r-core`, so:
    //
    // - tokio builds: helpers dispatch through `AsyncHostedDep`,
    //   producing a `WorkerReconstructor::Async`. Sync `HostedDep`
    //   impls still work transparently via the blanket bridge.
    // - sync builds: helpers dispatch through `HostedDep`, producing
    //   a `WorkerReconstructor::Sync`. An async-only `AsyncHostedDep`
    //   impl simply fails to compile against the sync helpers — a
    //   deliberate tightening over the old runtime panic.
    //
    // The `async_worker` macro flag no longer influences codegen on
    // this path; if set on a Hosted dep it only produces a
    // `#[deprecated]` warning at the registration site (see the
    // deprecation helper emitted above).
    let (hosted_codec_expr, hosted_worker_fn_expr) = if matches!(scope, Scope::Hosted) {
        let codec = quote! {
            Some(test_r::core::__test_r_make_hosted_codec::<#dep_ty>())
        };
        let worker_fn = quote! {
            Some(test_r::core::__test_r_make_hosted_worker_reconstructor::<#dep_ty>())
        };
        (codec, worker_fn)
    } else {
        (quote! { None }, quote! { None })
    };

    // RpcFactory + owner-cell-wrapping constructor for scope = HostedRpc. The
    // constructor returns the owner, wraps it in a HostedRpcOwnerCell, and the
    // factory tells the runtime how to (a) downcast the owner Arc back to a
    // cell and (b) build a worker-side stub from a channel.
    let rpc_factory_expr = if matches!(scope, Scope::HostedRpc) {
        let stub_ty = stub_type_path.as_ref().expect("stub type checked above");
        quote! {
            Some(test_r::core::RpcFactory {
                owner_into_cell: std::sync::Arc::new(
                    |__any: std::sync::Arc<dyn std::any::Any + Send + Sync>| {
                        __any
                            .downcast::<test_r::core::HostedRpcOwnerCell>()
                            .expect("HostedRpc owner downcast to HostedRpcOwnerCell failed")
                    },
                ),
                build_stub: std::sync::Arc::new(
                    |__channel: test_r::core::HostedRpcChannel| {
                        let __stub: #stub_ty =
                            <#dep_ty as test_r::core::HostedRpcDep>::build_stub(__channel);
                        let __boxed: std::sync::Arc<dyn std::any::Any + Send + Sync> =
                            std::sync::Arc::new(__stub);
                        __boxed
                    },
                ),
            })
        }
    } else {
        quote! { None }
    };

    // Pick the right codec/reconstructor pair based on scope. Only one of
    // them is populated at any time; we encode that invariant by choosing
    // the appropriate token expression here so the runtime never has to deal
    // with both being `Some` simultaneously.
    let (worker_fn_expr, codec_expr, hosted_codec_field_expr) = match scope {
        Scope::Cloneable => (
            cloneable_worker_fn_expr,
            cloneable_codec_expr,
            quote! { None },
        ),
        Scope::Hosted => (hosted_worker_fn_expr, quote! { None }, hosted_codec_expr),
        _ => (quote! { None }, quote! { None }, quote! { None }),
    };

    // For HostedRpc the constructor returns the owner type, but the runtime
    // stores a HostedRpcOwnerCell. Emit the wrapping in the constructor
    // closure itself so the downcast in `owner_into_cell` always succeeds.
    let ctor_call_sync = if matches!(scope, Scope::HostedRpc) {
        quote! {
            {
                let __owner = #ctor_name(#(#dep_getters),*);
                let __cell = test_r::core::HostedRpcOwnerCell::from_owner(__owner);
                let __arc: std::sync::Arc<dyn std::any::Any + Send + Sync> =
                    std::sync::Arc::new(__cell);
                __arc
            }
        }
    } else {
        quote! { std::sync::Arc::new(#ctor_name(#(#dep_getters),*)) }
    };

    let ctor_call_async = if matches!(scope, Scope::HostedRpc) {
        quote! {
            {
                let __owner = #ctor_name(#(#dep_getters),*).await;
                let __cell = test_r::core::HostedRpcOwnerCell::from_owner(__owner);
                let __arc: std::sync::Arc<dyn std::any::Any + Send + Sync> =
                    std::sync::Arc::new(__cell);
                __arc
            }
        }
    } else {
        quote! {
            {
                let result: std::sync::Arc<dyn std::any::Any + Send + Sync> =
                    std::sync::Arc::new(#ctor_name(#(#dep_getters),*).await);
                result
            }
        }
    };

    let register_call = if is_async {
        quote! {
              test_r::core::register_dependency_constructor_with_scope(
                  #dep_name_str,
                  module_path!(),
                  test_r::core::DependencyConstructor::Async(std::sync::Arc::new(|__test_r_deps_arg| Box::pin(async move {
                    #ctor_call_async
                  }))),
                 vec![#(#dep_names),*],
                 #scope_tokens,
                 #worker_fn_expr,
                 #codec_expr,
                 #hosted_codec_field_expr,
                 #rpc_factory_expr,
              );
        }
    } else {
        quote! {
            test_r::core::register_dependency_constructor_with_scope(
                #dep_name_str,
                module_path!(),
                test_r::core::DependencyConstructor::Sync(std::sync::Arc::new(|__test_r_deps_arg| #ctor_call_sync)),
                vec![#(#dep_names),*],
                #scope_tokens,
                #worker_fn_expr,
                #codec_expr,
                #hosted_codec_field_expr,
                #rpc_factory_expr,
            );
        }
    };

    let getter_ident = Ident::new(&format!("test_r_get_dep_{dep_name_str}"), Span::call_site());

    // The getter must downcast to the *injected* type, which is the stub
    // for HostedRpc and the constructor return type for every other scope.
    let injected_ty: proc_macro2::TokenStream = match &stub_type_path {
        Some(stub_path) => quote! { #stub_path },
        None => quote! { #dep_type },
    };

    let getter_body = quote! {
        dependency_view
            .get(#dep_name_str)
            .expect("Dependency not found")
            .downcast::<#injected_ty>()
            .expect("Dependency type mismatch")
    };

    let result = quote! {
        #[cfg(test)]
        #[test_r::ctor::ctor(crate_path=::test_r::ctor)]
        fn #register_ident() {
             // Optional `#[deprecated]`-fueled compile-time warning, only
             // emitted when the user wrote `async_worker = true` on this dep.
             // The marker function exists *inside* `#register_ident` so the
             // warning lights up at the dep's registration site, not at some
             // shared re-export.
             #async_worker_deprecation_tokens
             #register_call
        }

        #[cfg(test)]
        fn #getter_ident<'a>(dependency_view: &'a impl test_r::core::DependencyView) -> std::sync::Arc<#injected_ty> {
            #getter_body
        }

        #ast
    };

    result.into()
}

/// Lowering for `#[test_dep(scope = Hosted, worker = both(Trait))]`.
///
/// One declared dep becomes **two** `RegisteredDependency` entries
/// that share a single parent-side owner via
/// [`test_r_core::internal::HostedBothShared`]:
///
/// - a Hosted-view registration keyed by the owner type's dep name.
///   Tests can parameterise on `&Owner` and get a descriptor-
///   reconstructed handle on workers (the same shape a plain
///   `scope = Hosted` dep would produce).
/// - a HostedRpc-view registration keyed by the `<Trait>Stub` type's
///   dep name. Tests parameterise on `&<Trait>Stub` and round-trip
///   calls via IPC back to the parent's owner.
///
/// Both registrations share one acquisition helper that owns a
/// `Weak<HostedBothShared>` cache and constructs the owner at most
/// once per process. Descriptor bytes are captured inside the cell on
/// first construction so the Hosted-view codec doesn't re-invoke
/// `HostedDep::descriptor` on every per-test resolution.
///
/// Supported surface:
///
/// - sync and async constructors both work;
/// - constructor dependencies are resolved in the parent acquisition context;
/// - no `tagged_as` (the dep is registered under two derived names already;
///   tagging would require two-name plumbing we don't have yet);
/// - no `stub = ...` (the stub type is derived from the trait path passed to
///   `both(...)`).
///
/// `async_worker` is still parsed for source-compatibility but only produces a
/// `#[deprecated]` warning at the registration site; it has no effect on
/// codegen.
fn expand_hosted_both_dep(args: &TestDepArgs, mut ast: ItemFn, trait_path: &Path) -> TokenStream {
    let ctor_name = ast.sig.ident.clone();

    let dep_type = match &ast.sig.output {
        ReturnType::Default => {
            panic!("Dependency constructor must have a return type")
        }
        ReturnType::Type(_, typ) => match &**typ {
            Type::Path(path) => path.clone(),
            _ => {
                panic!("Dependency constructor must return a single concrete type")
            }
        },
    };

    // Keep the `worker = both(...)` lowering small and explicit. Anything
    // beyond the supported shape is rejected at macro time with a clear
    // message so we don't quietly drop user intent. Async owner constructors
    // are supported by emitting async dependency constructors for both views.
    let is_async = ast.sig.asyncness.is_some();
    if args.tagged_as.is_some() {
        panic!(
            "`tagged_as = ...` is not supported with `worker = both(...)` yet. \
             The `both` lowering registers the dep under two derived names \
             (owner type + `<Trait>Stub`); a tag would have to apply to both \
             which is not wired up. Remove `tagged_as` from this dep."
        );
    }
    if args.stub.is_some() {
        panic!(
            "`stub = ...` is not valid together with `worker = both(...)`. \
             The `both` picker derives the stub type from the trait path \
             passed to `both(...)` (`<Trait>Stub`); supplying both would be \
             contradictory."
        );
    }

    let (dep_getters, dep_names, dep_dimensions) = get_dependency_params(&ast, false);

    if !dep_dimensions.is_empty() {
        panic!("Matrix dimensions are not supported on #[test_dep] constructor parameters");
    }
    // Constructor dependencies are resolved when the parent materialises the
    // shared owner. Worker-side descriptor handles and RPC stubs remain leaves
    // reconstructed from the parent's descriptor bytes or channel.

    // `async_worker` is deprecated and ignored on every Hosted variant. Re-use
    // the same deprecation tactic here so the `both` variant doesn't silently
    // accept the flag either.
    let async_worker_deprecation_tokens = if args.async_worker {
        quote! {
            {
                #[deprecated(
                    note = "`async_worker` is deprecated and ignored on \
                            `scope = Hosted` deps. test-r now selects the \
                            worker-side behavior automatically from the \
                            Hosted registration shape (descriptor / RPC \
                            / both) and the active runtime (sync / \
                            tokio). Remove `async_worker`."
                )]
                fn __test_r_async_worker_deprecated() {}
                __test_r_async_worker_deprecated();
            }
        }
    } else {
        quote! {}
    };

    filter_custom_parameter_attributes(&mut ast);

    let owner_dep_name = type_path_to_string(&dep_type, DependencyTag::None);
    let stub_path = WorkerView::stub_path_from_trait(trait_path);
    let stub_type_path = TypePath {
        qself: None,
        path: stub_path.clone(),
    };
    let stub_dep_name = type_path_to_string(&stub_type_path, DependencyTag::None);

    // If the owner type and the derived `<Trait>Stub` would lower to the same
    // dep name (for example because the user accidentally named a type
    // `FooStub` and asked for `worker = both(Foo)`, or because two
    // registrations would both key on the same qualified id), the two
    // `register_…` calls would collide on getter idents, runtime dep keys, and
    // pruning. Reject at macro time with an actionable message instead.
    if owner_dep_name == stub_dep_name {
        panic!(
            "`worker = both(...)` would register both views under the same dependency name \
             (`{owner_dep_name}`). Rename the owner type or use a different trait so the \
             derived `<Trait>Stub` does not collide with the owner type name."
        );
    }

    let register_ident = Ident::new(
        &format!("test_r_register_dep_{owner_dep_name}_both"),
        Span::call_site(),
    );
    let acquire_ident = Ident::new(
        &format!("__test_r_both_acquire_{owner_dep_name}"),
        Span::call_site(),
    );
    let owner_getter_ident = Ident::new(
        &format!("test_r_get_dep_{owner_dep_name}"),
        Span::call_site(),
    );
    let stub_getter_ident = Ident::new(
        &format!("test_r_get_dep_{stub_dep_name}"),
        Span::call_site(),
    );

    let dep_ty = &dep_type;
    let stub_ty = &stub_type_path;

    // Acquire helper. Two flavors:
    //
    // - **Sync ctor**: a plain `fn` that uses `OnceLock<Mutex<Weak<…>>>`
    //   and runs the user's sync `#ctor_name()` once.
    // - **Async ctor**: an `async fn` that uses the same `Mutex<Weak<…>>`
    //   cache but does the actual `#ctor_name().await`
    //   *outside* the std mutex (we never hold a sync lock across `.await`).
    //   A double-checked upgrade on the slow path makes the rare
    //   concurrent-acquire race benign — the second acquirer drops its
    //   freshly built owner and returns the first one. In normal test-r
    //   plans the parent resolves Hosted owners serially, so this is
    //   just defensive.
    let acquire_fn = if is_async {
        quote! {
            #[cfg(test)]
            async fn #acquire_ident(
                __test_r_deps_arg: std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>,
            ) -> std::sync::Arc<test_r::core::HostedBothShared> {
                use std::sync::{Arc, Mutex, OnceLock, Weak};
                static CACHE: OnceLock<Mutex<Weak<test_r::core::HostedBothShared>>> = OnceLock::new();
                let cache = CACHE.get_or_init(|| Mutex::new(Weak::new()));
                // Fast path: avoid running the async ctor if we already
                // have a live shared cell.
                {
                    let guard = cache
                        .lock()
                        .expect("HostedBothShared cache mutex poisoned");
                    if let Some(strong) = guard.upgrade() {
                        return strong;
                    }
                }
                // Slow path: build the owner *outside* the std mutex so
                // we never hold a sync lock across `.await`.
                let __owner: #dep_ty = #ctor_name(#(#dep_getters),*).await;
                let __shared = Arc::new(
                    test_r::core::__test_r_make_hosted_both_shared::<#dep_ty>(__owner),
                );
                let mut guard = cache
                    .lock()
                    .expect("HostedBothShared cache mutex poisoned");
                if let Some(strong) = guard.upgrade() {
                    // A concurrent acquirer beat us; throw away our
                    // freshly built owner and reuse theirs so both
                    // Hosted-view and HostedRpc-view registrations
                    // share exactly one owner instance.
                    drop(__shared);
                    return strong;
                }
                *guard = Arc::downgrade(&__shared);
                __shared
            }
        }
    } else {
        quote! {
            #[cfg(test)]
            fn #acquire_ident(
                __test_r_deps_arg: std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>,
            ) -> std::sync::Arc<test_r::core::HostedBothShared> {
                use std::sync::{Arc, Mutex, OnceLock, Weak};
                static CACHE: OnceLock<Mutex<Weak<test_r::core::HostedBothShared>>> = OnceLock::new();
                let cache = CACHE.get_or_init(|| Mutex::new(Weak::new()));
                let mut guard = cache
                    .lock()
                    .expect("HostedBothShared cache mutex poisoned");
                if let Some(strong) = guard.upgrade() {
                    return strong;
                }
                let __owner: #dep_ty = #ctor_name(#(#dep_getters),*);
                let __shared = Arc::new(
                    test_r::core::__test_r_make_hosted_both_shared::<#dep_ty>(__owner),
                );
                *guard = Arc::downgrade(&__shared);
                __shared
            }
        }
    };

    // Constructor adapters passed to `register_dependency_constructor_with_scope`.
    //
    // The Hosted-view and HostedRpc-view registrations share the same acquire
    // helper so they always point at one parent-side owner. Async owner
    // constructors are represented as async dependency constructors for both
    // views and are awaited by the parent-side collection pass.
    let (hosted_view_ctor_tokens, hosted_rpc_view_ctor_tokens, extra_helper_tokens) = if is_async {
        let hosted = quote! {
            test_r::core::DependencyConstructor::Async(std::sync::Arc::new(|__test_r_deps_arg| {
                std::boxed::Box::pin(async move {
                    let __shared: std::sync::Arc<test_r::core::HostedBothShared> = #acquire_ident(__test_r_deps_arg).await;
                    let __arc: std::sync::Arc<dyn std::any::Any + Send + Sync> = __shared;
                    __arc
                })
            }))
        };
        let hosted_rpc = quote! {
            test_r::core::DependencyConstructor::Async(std::sync::Arc::new(|__test_r_deps_arg| {
                std::boxed::Box::pin(async move {
                    let __shared: std::sync::Arc<test_r::core::HostedBothShared> = #acquire_ident(__test_r_deps_arg).await;
                    let __arc: std::sync::Arc<dyn std::any::Any + Send + Sync> = __shared;
                    __arc
                })
            }))
        };
        (hosted, hosted_rpc, quote! {})
    } else {
        let sync_ctor = quote! {
            test_r::core::DependencyConstructor::Sync(std::sync::Arc::new(|__test_r_deps_arg| {
                let __shared: std::sync::Arc<test_r::core::HostedBothShared> = #acquire_ident(__test_r_deps_arg);
                let __arc: std::sync::Arc<dyn std::any::Any + Send + Sync> = __shared;
                __arc
            }))
        };
        (sync_ctor.clone(), sync_ctor, quote! {})
    };

    let result = quote! {
        // Shared parent-side acquisition for the `worker = both(...)`
        // lowering. Both the Hosted-view and the HostedRpc-view
        // registrations call into this function, so they always see
        // the same `Arc<HostedBothShared>` (and therefore the same
        // owner). The cache is intentionally `Weak`: once the test
        // suite finishes and drops its references, the owner is freed
        // and recreated on the next run.
        #acquire_fn

        #extra_helper_tokens

        #[cfg(test)]
        #[test_r::ctor::ctor(crate_path=::test_r::ctor)]
        fn #register_ident() {
            #async_worker_deprecation_tokens

            // Hosted-view registration. Tests parameterise on
            // `&#dep_ty` (the owner type). The Hosted runtime path
            // calls `__test_r_make_hosted_both_codec().to_wire` to
            // serialize the cached descriptor bytes, then the
            // existing Hosted worker reconstructor (which calls
            // `#dep_ty::from_descriptor`) materialises the owner on
            // each worker exactly as a plain `scope = Hosted` dep
            // would.
            //
            // The two halves are registered as companions of each
            // other (planner-only sibling link) so the dependency
            // pruner retains them as a pair. Without that link, a
            // suite whose selected tests only parameterise on
            // `&#stub_ty` would have the Hosted owner half pruned —
            // for the async-ctor flavour that would empty the
            // shared `HostedBothShared` cache before the sync
            // HostedRpc-view resolver runs, and the resolver would
            // panic with a `Poll::Pending` diagnostic.
            test_r::core::register_dependency_constructor_with_scope_and_companions(
                #owner_dep_name,
                module_path!(),
                #hosted_view_ctor_tokens,
                vec![#(#dep_names),*],
                test_r::core::DepScope::Hosted,
                Some(test_r::core::__test_r_make_hosted_worker_reconstructor::<#dep_ty>()),
                None,
                Some(test_r::core::__test_r_make_hosted_both_codec()),
                None,
                vec![#stub_dep_name.to_string()],
            );

            // HostedRpc-view registration. Tests parameterise on
            // `&#stub_ty`. The HostedRpc runtime path calls
            // `__test_r_make_hosted_both_rpc_factory::<#dep_ty>()`
            // to (a) reach into the shared cell for the inner
            // `Arc<HostedRpcOwnerCell>` so dispatched calls hit the
            // exact same owner the descriptor was derived from, and
            // (b) build worker-side stubs via the user's
            // `HostedRpcDep::build_stub`.
            test_r::core::register_dependency_constructor_with_scope_and_companions(
                #stub_dep_name,
                module_path!(),
                #hosted_rpc_view_ctor_tokens,
                vec![#(#dep_names),*],
                test_r::core::DepScope::HostedRpc,
                None,
                None,
                None,
                Some(test_r::core::__test_r_make_hosted_both_rpc_factory::<#dep_ty, #stub_ty>()),
                vec![#owner_dep_name.to_string()],
            );
        }

        // Owner-view getter: tests parameterise on `&#dep_ty` and
        // resolve through this getter to the reconstructed owner.
        #[cfg(test)]
        fn #owner_getter_ident<'a>(
            dependency_view: &'a impl test_r::core::DependencyView,
        ) -> std::sync::Arc<#dep_ty> {
            dependency_view
                .get(#owner_dep_name)
                .expect("Dependency not found")
                .downcast::<#dep_ty>()
                .expect("Dependency type mismatch")
        }

        // Stub-view getter: tests parameterise on `&#stub_ty` and
        // resolve through this getter to the worker-side RPC stub.
        #[cfg(test)]
        fn #stub_getter_ident<'a>(
            dependency_view: &'a impl test_r::core::DependencyView,
        ) -> std::sync::Arc<#stub_ty> {
            dependency_view
                .get(#stub_dep_name)
                .expect("Dependency not found")
                .downcast::<#stub_ty>()
                .expect("Dependency type mismatch")
        }

        // Keep the user's constructor as a real function so any
        // helpers in the same module can still call it directly (it
        // is also what `#acquire_ident` invokes).
        #ast
    };

    result.into()
}

pub fn inherit_test_dep(item: TokenStream) -> TokenStream {
    let def: InheritTestDep = parse_macro_input!(item as InheritTestDep);
    let dep_type = match &def.typ {
        Type::Path(path) => path.clone(),
        _ => {
            panic!("Dependency constructor must return a single concrete type")
        }
    };

    let tag_str = def.attr.and_then(|a| get_lit_str_attr(&[a], "tagged_as"));
    let tag = match tag_str {
        Some(tag) => DependencyTag::Tagged(tag),
        None => DependencyTag::None,
    };

    let dep_name_str = type_path_to_string(&dep_type, tag);
    let getter_ident = Ident::new(&format!("test_r_get_dep_{dep_name_str}"), Span::call_site());

    let result = quote! {
        fn #getter_ident<'a>(dependency_view: &'a impl test_r::core::DependencyView) -> std::sync::Arc<#dep_type> {
            super::#getter_ident(dependency_view)
        }
    };

    result.into()
}

pub fn define_matrix_dimension(item: TokenStream) -> TokenStream {
    let def = parse_macro_input!(item as DefineMatrixDimension);
    let get_dep_tags_fn = Ident::new(
        &format!("test_r_get_dep_tags_{}", def.dim),
        Span::call_site(),
    );
    let typ = def.typ;

    let typ_path = match &typ {
        Type::Path(path) => path,
        _ => {
            panic!("Must use a single concrete type in define_matrix_dimension")
        }
    };

    let mut pushes = Vec::new();

    for tag in def.tags {
        let dep_tag = DependencyTag::Tagged(tag.value());
        let dep_name_str = type_path_to_string(typ_path, dep_tag);
        let getter_ident = Ident::new(&format!("test_r_get_dep_{dep_name_str}"), Span::call_site());

        let name = tag.value();
        pushes.push(quote! {
            result.push((#name.to_string(), #dep_name_str.to_string(), std::sync::Arc::new(|dependency_view: std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>| #getter_ident(&dependency_view))));
        });
    }

    let ast = quote! {
        fn #get_dep_tags_fn() -> Vec<(String, String, std::sync::Arc<dyn (Fn(std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>) -> std::sync::Arc<#typ>) + Send + Sync + 'static>)> {
            let mut result: Vec<(String, String, std::sync::Arc<dyn (Fn(std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>) -> std::sync::Arc<#typ>) + Send + Sync + 'static>)> = Vec::new();
            #(#pushes)*
            result
        }
    };
    ast.into()
}

pub fn get_dependency_params(
    ast: &ItemFn,
    is_bench: bool,
) -> (
    Vec<proc_macro2::TokenStream>,
    Vec<proc_macro2::TokenStream>,
    Vec<(usize, Ident)>,
) {
    let mut dep_getters = Vec::new();
    let mut dep_names = Vec::new();
    let mut dep_dimensions = Vec::new();

    for (idx, param) in ast.sig.inputs.iter().enumerate() {
        if !is_bench || idx > 0 {
            // TODO: verify that the first bench arg is a Bencher/AsyncBencher
            let (dep_type, tag) = match param {
                FnArg::Receiver(_) => {
                    panic!("Test functions cannot have a self parameter")
                }
                FnArg::Typed(typ) => {
                    let tag_str = get_lit_str_attr(&typ.attrs, "tagged_as");
                    let dim_str = get_ident_attr(&typ.attrs, "dimension");

                    let dep_tag = match (tag_str, dim_str) {
                        (Some(tag), None) => DependencyTag::Tagged(tag),
                        (None, Some(dim)) => DependencyTag::Matrix(dim),
                        (Some(_), Some(_)) => panic!(
                            "Cannot have both a tag and a dimension attribute on the same test parameter"
                        ),
                        (None, None) => DependencyTag::None,
                    };

                    if let DependencyTag::Matrix(dim) = &dep_tag {
                        dep_dimensions.push((idx, dim.clone()));
                    }

                    let typ = get_dependency_param_from_pat_type(typ);
                    (typ, dep_tag)
                }
            };

            let dep_name_str = type_path_to_string(&dep_type, tag);
            let getter_ident =
                Ident::new(&format!("test_r_get_dep_{dep_name_str}"), Span::call_site());

            dep_getters.push(quote! {
                &#getter_ident(&__test_r_deps_arg)
            });
            dep_names.push(quote! {
                #dep_name_str.to_string()
            });
        }
    }
    (dep_getters, dep_names, dep_dimensions)
}

pub fn get_dependency_params_for_closure<'a>(
    ast: impl Iterator<Item = &'a Pat>,
) -> (
    Vec<proc_macro2::TokenStream>,
    Vec<proc_macro2::TokenStream>,
    Vec<Ident>,
) {
    let mut dep_getters = Vec::new();
    let mut dep_names = Vec::new();
    let mut bindings = Vec::new();
    for pat in ast {
        let (dep_type, tag) = match pat {
            Pat::Type(typ) => {
                let optional_tag = match get_lit_str_attr(&typ.attrs, "tagged_as") {
                    Some(tag) => DependencyTag::Tagged(tag),
                    None => DependencyTag::None,
                };
                (get_dependency_param_from_pat_type(typ), optional_tag)
            }
            _ => {
                panic!(
                    "Test functions can only have parameters which are immutable references to concrete types, but got {:?}",
                    pat.to_token_stream()
                )
                // TODO: nicer error report
            }
        };
        let dep_name_str = type_path_to_string(&dep_type, tag);
        let binding = match pat {
            Pat::Type(typ) => match &*typ.pat {
                Pat::Ident(ident) => ident.ident.clone(),
                _ => {
                    panic!(
                        "Test functions can only have parameters which are immutable references to concrete types, but got {:?}",
                        typ.pat.to_token_stream()
                    )
                    // TODO: nicer error report
                }
            },
            _ => {
                panic!(
                    "Test functions can only have parameters which are immutable references to concrete types, but got {:?}",
                    pat.to_token_stream()
                )
                // TODO: nicer error report
            }
        };

        let getter_ident = Ident::new(&format!("test_r_get_dep_{dep_name_str}"), Span::call_site());

        dep_getters.push(quote! {
            &#getter_ident(&__test_r_deps_arg)
        });
        dep_names.push(quote! {
            #dep_name_str.to_string()
        });
        bindings.push(binding);
    }
    (dep_getters, dep_names, bindings)
}

fn get_dependency_param_from_pat_type(typ: &PatType) -> TypePath {
    match &*typ.ty {
        Type::Reference(reference) => {
            match &*reference.elem {
                Type::Path(path) => path.clone(),
                _ => {
                    panic!(
                        "Test functions can only have parameters which are immutable references to concrete types, but got {:?}",
                        reference.elem.to_token_stream()
                    )
                    // TODO: nicer error report
                }
            }
        }
        _ => {
            panic!(
                "Test functions can only have parameters which are immutable references to concrete types, but got {:?}",
                typ.ty.to_token_stream()
            )
            // TODO: nicer error report
        }
    }
}

#[derive(Debug, Clone)]
enum DependencyTag {
    None,
    Tagged(String),
    Matrix(Ident),
}

impl DependencyTag {
    fn into_iter(self) -> impl Iterator<Item = String> {
        match self {
            DependencyTag::Tagged(tag) => Some(tag),
            _ => None,
        }
        .into_iter()
    }
}

impl From<Option<String>> for DependencyTag {
    fn from(value: Option<String>) -> Self {
        match value {
            Some(tag) => DependencyTag::Tagged(tag),
            None => DependencyTag::None,
        }
    }
}

#[derive(Debug, darling::FromMeta)]
struct TestDepArgs {
    #[darling(default)]
    tagged_as: Option<String>,
    #[darling(default)]
    scope: Option<Scope>,
    /// Parsed `worker = …` picker. See [`WorkerView`] for the surface and
    /// intended translations.
    ///
    /// Only valid together with `scope = Hosted`. `Cloneable` /
    /// `Shared` / `PerWorker` deps auto-derive their worker handle
    /// shape from the dep type alone, so a `worker = …` value on
    /// them is rejected to avoid a silent no-op.
    #[darling(default)]
    worker: Option<WorkerView>,
    /// Required for `scope = HostedRpc`. Names the worker-visible stub type —
    /// i.e. `<Owner as HostedRpcDep>::Stub`. Tests parameterise on this type,
    /// not on the owner type. Rejected for any other scope.
    #[darling(default)]
    stub: Option<Path>,
    /// Opt this `scope = Hosted` dep into an async worker-side
    /// reconstruction by routing through
    /// [`AsyncHostedDep::from_descriptor`] (which returns a `Future`)
    /// instead of the sync [`HostedDep::from_descriptor`]. Required
    /// whenever worker-side reconstruction needs to `.await` (e.g.
    /// calling async `Provided*::new(...).await` constructors of
    /// downstream services).
    ///
    /// Rejected for any scope other than `Hosted`. Requires the
    /// `tokio` runtime feature, because the worker reconstructor is
    /// emitted as `WorkerReconstructor::Async`.
    #[darling(default)]
    async_worker: bool,
}

struct DefineMatrixDimension {
    dim: Ident,
    _colon: Token![:],
    typ: Type,
    _arrow: Token![->],
    tags: Punctuated<LitStr, Token![,]>,
}

impl Parse for DefineMatrixDimension {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(DefineMatrixDimension {
            dim: input.parse()?,
            _colon: input.parse()?,
            typ: input.parse()?,
            _arrow: input.parse()?,
            tags: input.parse_terminated(|i| i.parse::<LitStr>(), Token![,])?,
        })
    }
}

struct InheritTestDep {
    attr: Option<Attribute>,
    typ: Type,
}

impl Parse for InheritTestDep {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(Token![#]) {
            let mut attrs = Attribute::parse_outer(input)?;
            if attrs.len() != 1 {
                Err(syn::Error::new(
                    input.span(),
                    "Expected zero or one attribute",
                ))
            } else {
                Ok(InheritTestDep {
                    attr: Some(attrs.pop().unwrap()),
                    typ: input.parse()?,
                })
            }
        } else {
            Ok(InheritTestDep {
                attr: None,
                typ: input.parse()?,
            })
        }
    }
}

fn get_lit_str_attr(attrs: &[Attribute], ident: &str) -> Option<String> {
    attrs
        .iter()
        .find(|attr| is_testr_attribute(attr, ident))
        .map(|attr| {
            let tag = attr
                .parse_args::<LitStr>()
                .unwrap_or_else(|_| panic!("{ident} attribute's parameter must be a string"));
            tag.value()
        })
}

fn get_ident_attr(attrs: &[Attribute], ident: &str) -> Option<Ident> {
    attrs
        .iter()
        .find(|attr| is_testr_attribute(attr, ident))
        .map(|attr| {
            attr.parse_args::<Ident>()
                .unwrap_or_else(|_| panic!("{ident} attribute's parameter must be an identifier"))
        })
}

fn type_to_string(typ: &Type, optional_tag: DependencyTag) -> String {
    match typ {
        Type::Array(array) => {
            let inner_type = type_to_string(&array.elem, optional_tag);
            format!("array_{inner_type}")
        }
        Type::BareFn(_) => {
            panic!("Function pointers are not supported in dependency injection")
        }
        Type::Group(group) => type_to_string(&group.elem, optional_tag),
        Type::ImplTrait(impltrait) => {
            let mut result = "impl".to_string();
            for bound in &impltrait.bounds {
                if let TypeParamBound::Trait(trait_bound) = bound {
                    result.push('_');
                    result.push_str(
                        &trait_bound
                            .path
                            .segments
                            .iter()
                            .map(|s| segment_to_string(s, DependencyTag::None))
                            .collect::<Vec<_>>()
                            .join("_"),
                    );
                }
            }
            result
        }
        Type::Infer(_) => {
            panic!("Type inference is not supported in dependency injection type signatures")
        }
        Type::Macro(_) => {
            panic!("Macro invocations are not supported in dependency injection type signatures")
        }
        Type::Never(_) => "never".to_string(),
        Type::Paren(inner) => type_to_string(&inner.elem, optional_tag),
        Type::Path(path) => type_path_to_string(path, optional_tag),
        Type::Ptr(inner) => {
            let inner_type = type_to_string(&inner.elem, optional_tag);
            format!("ptr_{inner_type}")
        }
        Type::Reference(inner) => {
            let inner_type = type_to_string(&inner.elem, optional_tag);
            format!("ref_{inner_type}")
        }
        Type::Slice(inner) => {
            let inner_type = type_to_string(&inner.elem, optional_tag);
            format!("slice_{inner_type}")
        }
        Type::TraitObject(to) => {
            let mut result = "dyn".to_string();
            for bound in &to.bounds {
                if let TypeParamBound::Trait(trait_bound) = bound {
                    result.push('_');
                    result.push_str(
                        &trait_bound
                            .path
                            .segments
                            .iter()
                            .map(|s| segment_to_string(s, DependencyTag::None))
                            .collect::<Vec<_>>()
                            .join("_"),
                    );
                }
            }
            if let DependencyTag::Tagged(tag) = optional_tag {
                result.push('_');
                result.push_str(&tag);
            }
            result
        }
        Type::Tuple(tuple) => {
            let inner_types = tuple
                .elems
                .iter()
                .map(|t| type_to_string(t, DependencyTag::None))
                .chain(optional_tag.into_iter())
                .collect::<Vec<_>>()
                .join("_");
            format!("tuple_{inner_types}")
        }
        _ => "".to_string(),
    }
}

fn type_path_to_string(dep_type: &TypePath, optional_tag: DependencyTag) -> String {
    let merged_ident = dep_type
        .path
        .segments
        .iter()
        .map(|s| segment_to_string(s, DependencyTag::None))
        .chain(optional_tag.into_iter())
        .collect::<Vec<_>>()
        .join("_");
    let dep_name = Ident::new(&merged_ident, Span::call_site());
    dep_name.to_string().to_lowercase()
}

fn segment_to_string(segment: &PathSegment, optional_tag: DependencyTag) -> String {
    let mut result = segment.ident.to_string();
    match &segment.arguments {
        PathArguments::None => {}
        PathArguments::AngleBracketed(args) => {
            for arg in &args.args {
                result.push('_');
                result.push_str(&generic_argument_to_string(arg, optional_tag.clone()));
            }
        }
        PathArguments::Parenthesized(_args) => {
            panic!("Parenthesized type arguments are not supported - wrap the type in a newtype")
        }
    }
    result
}

fn generic_argument_to_string(arg: &GenericArgument, optional_tag: DependencyTag) -> String {
    match arg {
        GenericArgument::Type(typ) => type_to_string(typ, optional_tag),
        GenericArgument::Const(_) => {
            panic!("Const generics are not supported in dependency injection")
        }
        GenericArgument::AssocType(_) => {
            panic!("Associated types are not supported in dependency injection")
        }
        GenericArgument::AssocConst(_) => {
            panic!("Associated constants are not supported in dependency injection")
        }
        GenericArgument::Constraint(_) => {
            panic!("Constraints are not supported in dependency injection; introduce a newtype")
        }
        _ => "".to_string(),
    }
}

#[cfg(test)]
mod worker_view_tests {
    //! Unit tests for the `WorkerView` parser and the `<TraitName>Stub`
    //! derivation. End-to-end behaviour (the `worker = …` translation into the
    //! existing `scope`/`stub` representation) is exercised by the example
    //! crate integration tests; here we pin the smaller parser API in
    //! isolation.
    use super::WorkerView;
    use darling::FromMeta;
    use quote::ToTokens;
    use syn::parse_quote;

    /// `worker = descriptor` parses as the `Descriptor` variant.
    #[test]
    fn parses_descriptor_identifier() {
        let meta: syn::Meta = parse_quote!(worker = descriptor);
        let v = WorkerView::from_meta(&meta).expect("descriptor parses");
        assert!(matches!(v, WorkerView::Descriptor));
    }

    /// `worker = rpc(MyTrait)` parses as `Rpc(MyTrait)`.
    #[test]
    fn parses_rpc_with_simple_trait_path() {
        let meta: syn::Meta = parse_quote!(worker = rpc(MyTrait));
        let v = WorkerView::from_meta(&meta).expect("rpc(MyTrait) parses");
        match v {
            WorkerView::Rpc(p) => {
                assert_eq!(p.to_token_stream().to_string().replace(' ', ""), "MyTrait");
            }
            other => panic!("expected Rpc(...), got {other:?}"),
        }
    }

    /// `worker = both(some::module::Trait)` parses as `Both(...)` with
    /// the qualifier preserved.
    #[test]
    fn parses_both_with_qualified_trait_path() {
        let meta: syn::Meta = parse_quote!(worker = both(some::module::Trait));
        let v = WorkerView::from_meta(&meta).expect("both(some::module::Trait) parses");
        match v {
            WorkerView::Both(p) => {
                assert_eq!(
                    p.to_token_stream().to_string().replace(' ', ""),
                    "some::module::Trait"
                );
            }
            other => panic!("expected Both(...), got {other:?}"),
        }
    }

    /// `worker = descriptor_typo` (bare ident other than `descriptor`)
    /// is rejected with `unknown_value`.
    #[test]
    fn rejects_unknown_bare_identifier() {
        let meta: syn::Meta = parse_quote!(worker = foo);
        let err = WorkerView::from_meta(&meta)
            .expect_err("bare ident other than `descriptor` must be rejected");
        let msg = err.to_string();
        assert!(
            msg.to_lowercase().contains("foo") || msg.to_lowercase().contains("unknown"),
            "expected error mentioning the unknown value or 'unknown', got: {msg}"
        );
    }

    /// `worker = rpc()` (no trait path) is rejected.
    #[test]
    fn rejects_rpc_without_argument() {
        let meta: syn::Meta = parse_quote!(worker = rpc());
        let err = WorkerView::from_meta(&meta).expect_err("rpc() with no arg must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("exactly one trait path"),
            "expected the missing-arg diagnostic, got: {msg}"
        );
    }

    /// `worker = rpc(A, B)` (multiple arguments) is rejected.
    #[test]
    fn rejects_rpc_with_multiple_arguments() {
        let meta: syn::Meta = parse_quote!(worker = rpc(A, B));
        let err = WorkerView::from_meta(&meta).expect_err("rpc(A, B) must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("exactly one trait path"),
            "expected the missing-arg diagnostic, got: {msg}"
        );
    }

    /// `worker = wat(Trait)` (call form with unknown function name) is
    /// rejected.
    #[test]
    fn rejects_unknown_call_form() {
        let meta: syn::Meta = parse_quote!(worker = wat(Trait));
        let err = WorkerView::from_meta(&meta).expect_err("wat(...) must be rejected");
        let msg = err.to_string();
        assert!(
            msg.to_lowercase().contains("wat") || msg.to_lowercase().contains("unknown"),
            "expected error mentioning the unknown value, got: {msg}"
        );
    }

    /// The argument to `rpc(...)` must be a trait path, not e.g. a
    /// string literal or a numeric literal.
    #[test]
    fn rejects_rpc_with_non_path_argument() {
        let meta: syn::Meta = parse_quote!(worker = rpc("foo"));
        let err = WorkerView::from_meta(&meta)
            .expect_err("rpc(\"foo\") with string arg must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("trait path"),
            "expected the must-be-trait-path diagnostic, got: {msg}"
        );
    }

    /// `stub_path_from_trait(MyTrait)` produces `MyTraitStub`.
    #[test]
    fn stub_path_from_simple_trait() {
        let trait_path: syn::Path = parse_quote!(MyTrait);
        let stub = WorkerView::stub_path_from_trait(&trait_path);
        assert_eq!(
            stub.to_token_stream().to_string().replace(' ', ""),
            "MyTraitStub"
        );
    }

    /// `stub_path_from_trait(some::module::Trait)` preserves the
    /// module qualifier and appends `Stub` only to the last segment:
    /// `some::module::TraitStub`.
    #[test]
    fn stub_path_from_qualified_trait_preserves_qualifier() {
        let trait_path: syn::Path = parse_quote!(some::module::Trait);
        let stub = WorkerView::stub_path_from_trait(&trait_path);
        assert_eq!(
            stub.to_token_stream().to_string().replace(' ', ""),
            "some::module::TraitStub"
        );
    }

    /// `stub_path_from_trait` preserves a leading `::` on absolute paths so a
    /// trait coming from a different crate doesn't collapse to a relative path.
    #[test]
    fn stub_path_from_absolute_path_preserves_leading_colons() {
        let trait_path: syn::Path = parse_quote!(::some::krate::Trait);
        let stub = WorkerView::stub_path_from_trait(&trait_path);
        assert_eq!(
            stub.to_token_stream().to_string().replace(' ', ""),
            "::some::krate::TraitStub"
        );
    }

    /// Trait paths with turbofish generics (`Trait::<T>`) are rejected because
    /// the derived `TraitStub::<T>` would almost certainly not exist.
    #[test]
    fn rejects_rpc_with_turbofish_generic_trait_path() {
        let meta: syn::Meta = parse_quote!(worker = rpc(Trait::<T>));
        let err = WorkerView::from_meta(&meta).expect_err("rpc(Trait::<T>) must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("plain trait path") || msg.contains("generic"),
            "expected the generic-arguments rejection diagnostic, got: {msg}"
        );
    }

    /// Same rejection for the `both(...)` form.
    #[test]
    fn rejects_both_with_turbofish_generic_trait_path() {
        let meta: syn::Meta = parse_quote!(worker = both(Trait::<T>));
        let err = WorkerView::from_meta(&meta).expect_err("both(Trait::<T>) must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("plain trait path") || msg.contains("generic"),
            "expected the generic-arguments rejection diagnostic, got: {msg}"
        );
    }

    /// A generic argument on an intermediate qualifier segment
    /// (`mod::Foo<X>::Trait`) is also rejected. We inspect every path segment,
    /// not just the last.
    #[test]
    fn rejects_rpc_with_generic_on_intermediate_path_segment() {
        // Note: `mod::Foo::<X>::Trait` doesn't parse as a syn::Path;
        // the syntactically-realistic case is `Foo<X>::Trait` which is
        // not a legal Path either. The realistic angle-bracketed form
        // is on the *last* segment (covered above) or on an
        // intermediate segment under turbofish, e.g.
        // `outer::Wrapper::<T>::Trait`:
        let meta: syn::Meta = parse_quote!(worker = rpc(outer::Wrapper::<T>::Trait));
        let err = WorkerView::from_meta(&meta)
            .expect_err("rpc(outer::Wrapper::<T>::Trait) must be rejected");
        let msg = err.to_string();
        assert!(
            msg.contains("plain trait path") || msg.contains("generic"),
            "expected the generic-arguments rejection diagnostic, got: {msg}"
        );
    }
}
