use std::sync::Arc;

use glua_parser::{
    LuaAstNode, LuaCallExpr, LuaExpr, LuaIndexMemberExpr, LuaLiteralToken, LuaTableExpr,
    LuaVarExpr, PathTrait,
};

use crate::{
    DbIndex, InferFailReason, InferGuard, InferGuardRef, LuaDocParamInfo, LuaDocReturnInfo,
    LuaFunctionType, LuaInferCache, LuaMemberIndexItem, LuaMemberKey, LuaMemberOwner, LuaSignature,
    LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, ReturnTypeKind, SignatureReturnStatus,
    TypeOps, get_real_type, humanize_type, infer_call_expr_func, infer_expr, infer_table_should_be,
};

use super::{
    ResolveResult, UnResolveCallClosureParams, UnResolveClosureReturn, UnResolveParentAst,
    UnResolveParentClosureParams, UnResolveReturn, find_decl_function::find_decl_function_type,
    resolve::try_resolve_return_point,
};

pub fn try_resolve_call_closure_params(
    db: &mut DbIndex,
    cache: &mut LuaInferCache,
    closure_params: &mut UnResolveCallClosureParams,
) -> ResolveResult {
    let call_expr = closure_params.call_expr.clone();
    let prefix_expr = call_expr.get_prefix_expr().ok_or(InferFailReason::None)?;
    let call_expr_type = infer_expr(db, cache, prefix_expr)?;

    let call_doc_func = infer_call_expr_func(
        db,
        cache,
        call_expr.clone(),
        call_expr_type,
        &InferGuard::new(),
        None,
    )?;

    let colon_call = call_expr.is_colon_call();
    let colon_define = call_doc_func.is_colon_define();

    let mut param_idx = closure_params.param_idx;
    match (colon_call, colon_define) {
        (true, false) => {
            param_idx += 1;
        }
        (false, true) => {
            if param_idx == 0 {
                return Ok(());
            }

            param_idx -= 1;
        }
        _ => {}
    }

    let Some(callback_func) = resolve_call_param_doc_function(
        db,
        &call_expr,
        &call_doc_func,
        param_idx,
        closure_params.signature_id,
        closure_params.file_id,
    ) else {
        return Ok(());
    };
    let async_state = callback_func.get_async_state();
    let params_to_insert = callback_func.get_params().to_vec();

    let signature = db
        .get_signature_index_mut()
        .get_mut(&closure_params.signature_id)
        .ok_or(InferFailReason::None)?;

    let signature_params = &mut signature.param_docs;
    for (idx, (name, type_ref)) in params_to_insert.iter().enumerate() {
        if signature_params.contains_key(&idx) {
            continue;
        }

        signature_params.insert(
            idx,
            LuaDocParamInfo {
                name: name.clone(),
                type_ref: type_ref.clone().unwrap_or(LuaType::Any),
                default_value: None,
                description: None,
                nullable: false,
                attributes: None,
            },
        );
    }
    if signature.params.len() < params_to_insert.len() {
        let missing_start = signature.params.len();
        for (name, _) in params_to_insert.iter().skip(missing_start) {
            signature.params.push(name.clone());
        }
    }

    signature.async_state = async_state;

    Ok(())
}

pub fn try_resolve_closure_return(
    db: &mut DbIndex,
    cache: &mut LuaInferCache,
    closure_return: &mut UnResolveClosureReturn,
) -> ResolveResult {
    let call_expr = closure_return.call_expr.clone();
    let prefix_expr = call_expr.get_prefix_expr().ok_or(InferFailReason::None)?;
    let call_expr_type = infer_expr(db, cache, prefix_expr)?;
    let mut param_idx = closure_return.param_idx;
    let call_doc_func = infer_call_expr_func(
        db,
        cache,
        call_expr.clone(),
        call_expr_type,
        &InferGuard::new(),
        None,
    )?;

    let colon_define = call_doc_func.is_colon_define();
    let colon_call = call_expr.is_colon_call();
    match (colon_define, colon_call) {
        (true, false) => {
            if param_idx == 0 {
                return Ok(());
            }
            param_idx -= 1
        }
        (false, true) => {
            param_idx += 1;
        }
        _ => {}
    }

    let Some(callback_func) = resolve_call_param_doc_function(
        db,
        &call_expr,
        &call_doc_func,
        param_idx,
        closure_return.signature_id,
        closure_return.file_id,
    ) else {
        return Ok(());
    };
    let ret_type = callback_func.get_ret().clone();

    let signature = db
        .get_signature_index_mut()
        .get_mut(&closure_return.signature_id)
        .ok_or(InferFailReason::None)?;

    if ret_type.contain_tpl() {
        return try_convert_to_func_body_infer(db, cache, closure_return);
    }

    match signature.resolve_return {
        SignatureReturnStatus::UnResolve => {}
        SignatureReturnStatus::InferResolve => {
            signature.return_docs.clear();
        }
        _ => return Ok(()),
    }

    signature.return_docs.push(LuaDocReturnInfo {
        name: None,
        type_ref: ret_type.clone(),
        default_value: None,
        description: None,
        attributes: None,
        return_kind: ReturnTypeKind::default(),
    });

    signature.resolve_return = SignatureReturnStatus::DocResolve;
    Ok(())
}

fn try_convert_to_func_body_infer(
    db: &mut DbIndex,
    cache: &mut LuaInferCache,
    closure_return: &mut UnResolveClosureReturn,
) -> ResolveResult {
    let mut unresolve = UnResolveReturn {
        file_id: closure_return.file_id,
        signature_id: closure_return.signature_id,
        return_points: closure_return.return_points.clone(),
    };

    try_resolve_return_point(db, cache, &mut unresolve)
}

fn resolve_call_param_doc_function(
    db: &DbIndex,
    call_expr: &LuaCallExpr,
    call_doc_func: &LuaFunctionType,
    param_idx: usize,
    origin_signature_id: LuaSignatureId,
    call_file_id: crate::FileId,
) -> Option<Arc<LuaFunctionType>> {
    if let Some(param_type) = call_doc_func.get_params().get(param_idx) {
        let param_type = get_real_type(db, param_type.1.as_ref().unwrap_or(&LuaType::Any))?;
        match param_type {
            LuaType::DocFunction(func) => return Some(func.clone()),
            LuaType::Union(union_types) => {
                if let Some(LuaType::DocFunction(func)) = union_types
                    .into_vec()
                    .iter()
                    .filter(|typ| matches!(typ, LuaType::DocFunction(_)))
                    .min_by_key(|typ| stable_type_selection_key(db, typ))
                {
                    return Some(func.clone());
                }
            }
            _ => {}
        }
    }

    resolve_gmod_hook_add_callback_doc_function(
        db,
        call_expr,
        param_idx,
        Some(origin_signature_id),
        call_file_id,
    )
}

fn stable_type_selection_key(db: &DbIndex, typ: &LuaType) -> String {
    humanize_type(db, typ, RenderLevel::Detailed)
}

pub fn resolve_gmod_hook_add_callback_doc_function(
    db: &DbIndex,
    call_expr: &LuaCallExpr,
    param_idx: usize,
    origin_signature_id: Option<LuaSignatureId>,
    call_file_id: crate::FileId,
) -> Option<Arc<LuaFunctionType>> {
    if !db.get_emmyrc().gmod.enabled || param_idx != 2 {
        return None;
    }

    let call_path = call_expr.get_access_path()?;
    if !matches_call_path(&call_path, "hook.Add") {
        return None;
    }

    let hook_name = extract_hook_name(call_expr)?;
    let member_key = LuaMemberKey::Name(hook_name.into());
    let call_realm = db
        .get_gmod_infer_index()
        .get_realm_at_offset(&call_file_id, call_expr.get_range().start());

    let mut candidates = Vec::new();
    for owner_name in iter_hook_owner_names(db) {
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global(&owner_name));
        let Some(item) = db.get_member_index().get_member_item(&owner, &member_key) else {
            continue;
        };

        for member_id in member_ids_from_item(item) {
            let Some(type_cache) = db.get_type_index().get_type_cache(&member_id.into()) else {
                continue;
            };
            let Some(function_types) =
                filter_signature_type(db, type_cache.as_type(), origin_signature_id.as_ref(), true)
            else {
                continue;
            };
            let member_realm = db
                .get_gmod_infer_index()
                .get_realm_at_offset(&member_id.file_id, member_id.get_position());
            let is_compatible = is_realm_compatible(call_realm, member_realm);

            for func in function_types {
                candidates.push((is_compatible, func));
            }
        }
    }

    candidates.sort_by(
        |(left_compatible, left_func), (right_compatible, right_func)| {
            right_compatible.cmp(left_compatible).then_with(|| {
                right_func
                    .get_params()
                    .len()
                    .cmp(&left_func.get_params().len())
            })
        },
    );
    candidates.into_iter().map(|(_, func)| func).next()
}

fn iter_hook_owner_names(db: &DbIndex) -> Vec<String> {
    let mut names = vec![
        "GM".to_string(),
        "GAMEMODE".to_string(),
        "SANDBOX".to_string(),
        "PLUGIN".to_string(),
    ];
    for configured_prefix in &db.get_emmyrc().gmod.hook_mappings.method_prefixes {
        let normalized = configured_prefix
            .trim()
            .trim_end_matches([':', '.'])
            .to_string();
        if normalized.is_empty() {
            continue;
        }

        if !names
            .iter()
            .any(|existing| existing.eq_ignore_ascii_case(&normalized))
        {
            names.push(normalized);
        }
    }

    names
}

fn member_ids_from_item(item: &LuaMemberIndexItem) -> Vec<crate::LuaMemberId> {
    match item {
        LuaMemberIndexItem::One(id) => vec![*id],
        LuaMemberIndexItem::Many(ids) => ids.clone(),
    }
}

pub fn extract_hook_name(call_expr: &LuaCallExpr) -> Option<String> {
    let args = call_expr.get_args_list()?;
    let first_arg = args.get_args().next()?;
    let LuaExpr::LiteralExpr(literal_expr) = first_arg else {
        return None;
    };
    let LuaLiteralToken::String(string_token) = literal_expr.get_literal()? else {
        return None;
    };

    let hook_name = string_token.get_value();
    let trimmed = hook_name.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

fn matches_call_path(path: &str, target: &str) -> bool {
    // All call sites pass a fully-qualified target (e.g. "hook.Add").
    // get_access_path() also returns the full qualified path, so an exact equality check
    // is both necessary and sufficient. A suffix check would produce false positives for
    // paths like "mylib.hook.Add" when the target is "hook.Add".
    path == target
}

fn is_realm_compatible(call_realm: crate::GmodRealm, item_realm: crate::GmodRealm) -> bool {
    !matches!(
        (call_realm, item_realm),
        (crate::GmodRealm::Client, crate::GmodRealm::Server)
            | (crate::GmodRealm::Server, crate::GmodRealm::Client)
    )
}

pub fn try_resolve_closure_parent_params(
    db: &mut DbIndex,
    cache: &mut LuaInferCache,
    closure_params: &mut UnResolveParentClosureParams,
) -> ResolveResult {
    let origin_sig_id = closure_params.signature_id;
    let signature = db
        .get_signature_index()
        .get(&closure_params.signature_id)
        .ok_or(InferFailReason::None)?;
    if !signature.param_docs.is_empty() {
        return Ok(());
    }
    let self_type;
    let member_type = match &closure_params.parent_ast {
        UnResolveParentAst::LuaFuncStat(func_stat) => {
            let func_name = func_stat.get_func_name().ok_or(InferFailReason::None)?;
            match func_name {
                LuaVarExpr::IndexExpr(index_expr) => {
                    let prefix_expr = index_expr.get_prefix_expr().ok_or(InferFailReason::None)?;
                    let prefix_type = infer_expr(db, cache, prefix_expr)?;
                    self_type = Some(prefix_type.clone());
                    find_best_function_type(
                        db,
                        cache,
                        &prefix_type,
                        LuaIndexMemberExpr::IndexExpr(index_expr),
                        signature,
                        Some(&origin_sig_id),
                    )
                    .ok_or(InferFailReason::None)?
                }
                _ => return Ok(()),
            }
        }
        UnResolveParentAst::LuaTableField(table_field) => {
            let parnet_table_expr = table_field
                .get_parent::<LuaTableExpr>()
                .ok_or(InferFailReason::None)?;
            let parent_table_type = infer_table_should_be(db, cache, parnet_table_expr)?;
            self_type = Some(parent_table_type.clone());
            find_best_function_type(
                db,
                cache,
                &parent_table_type,
                LuaIndexMemberExpr::TableField(table_field.clone()),
                signature,
                Some(&origin_sig_id),
            )
            .ok_or(InferFailReason::None)?
        }
        UnResolveParentAst::LuaAssignStat(assign) => {
            let (vars, exprs) = assign.get_var_and_expr_list();
            let position = closure_params.signature_id.get_position();
            let idx = exprs
                .iter()
                .position(|expr| expr.get_position() == position)
                .ok_or(InferFailReason::None)?;
            let var = vars.get(idx).ok_or(InferFailReason::None)?;
            match var {
                LuaVarExpr::IndexExpr(index_expr) => {
                    let prefix_expr = index_expr.get_prefix_expr().ok_or(InferFailReason::None)?;
                    let prefix_expr_type = infer_expr(db, cache, prefix_expr)?;
                    self_type = Some(prefix_expr_type.clone());
                    find_best_function_type(
                        db,
                        cache,
                        &prefix_expr_type,
                        LuaIndexMemberExpr::IndexExpr(index_expr.clone()),
                        signature,
                        Some(&origin_sig_id),
                    )
                    .ok_or(InferFailReason::None)?
                }
                _ => return Ok(()),
            }
        }
    };

    resolve_closure_member_type(
        db,
        closure_params,
        &member_type,
        self_type,
        &InferGuard::new(),
    )
}

fn resolve_closure_member_type(
    db: &mut DbIndex,
    closure_params: &UnResolveParentClosureParams,
    member_type: &LuaType,
    self_type: Option<LuaType>,
    infer_guard: &InferGuardRef,
) -> ResolveResult {
    match &member_type {
        LuaType::DocFunction(doc_func) => {
            resolve_doc_function(db, closure_params, doc_func, self_type)
        }
        LuaType::Signature(id) => {
            if id == &closure_params.signature_id {
                return Ok(());
            }
            let signature = db.get_signature_index().get(id);

            if let Some(signature) = signature {
                let fake_doc_function = signature.to_doc_func_type();
                resolve_doc_function(db, closure_params, &fake_doc_function, self_type)
            } else {
                Ok(())
            }
        }
        LuaType::Union(union_types) => {
            let signature = db
                .get_signature_index()
                .get(&closure_params.signature_id)
                .ok_or(InferFailReason::None)?;
            let mut final_params = signature.get_type_params().to_vec();
            let mut final_ret = LuaType::Unknown;

            let mut multi_function_type = Vec::new();
            for typ in union_types.into_vec() {
                match typ {
                    LuaType::DocFunction(func) => {
                        multi_function_type.push(func.clone());
                    }
                    LuaType::Ref(ref_id) => {
                        if infer_guard.check(&ref_id).is_err() {
                            continue;
                        }
                        let type_decl = db
                            .get_type_index()
                            .get_type_decl(&ref_id)
                            .ok_or(InferFailReason::None)?;

                        if let Some(origin) = type_decl.get_alias_origin(db, None)
                            && let LuaType::DocFunction(f) = origin
                        {
                            multi_function_type.push(f);
                        }
                    }
                    _ => {}
                };
            }

            let mut variadic_type = LuaType::Unknown;
            for doc_func in multi_function_type {
                let mut doc_params = doc_func.get_params().to_vec();
                match (doc_func.is_colon_define(), signature.is_colon_define) {
                    (true, false) => {
                        // 原始签名是冒号定义, 但未解析的签名不是冒号定义, 即要插入第一个参数
                        doc_params.insert(0, ("self".to_string(), Some(LuaType::SelfInfer)));
                    }
                    (false, true) => {
                        // 原始签名不是冒号定义, 但未解析的签名是冒号定义, 即要删除第一个参数
                        if !doc_params.is_empty() {
                            doc_params.remove(0);
                        }
                    }
                    _ => {}
                }

                for (idx, param) in doc_params.iter().enumerate() {
                    if let Some(final_param) = final_params.get(idx) {
                        if final_param.0 == "..." {
                            // 如果`doc_params`当前与之后的参数的类型不一致, 那么`variadic_type`为`Any`
                            for i in idx..doc_params.len() {
                                if let Some(param) = doc_params.get(i)
                                    && let Some(typ) = &param.1
                                {
                                    if variadic_type == LuaType::Unknown {
                                        variadic_type = typ.clone();
                                    } else if variadic_type != *typ {
                                        variadic_type = LuaType::Any;
                                    }
                                }
                            }

                            break;
                        }
                        let new_type = TypeOps::Union.apply(
                            db,
                            final_param.1.as_ref().unwrap_or(&LuaType::Unknown),
                            param.1.as_ref().unwrap_or(&LuaType::Unknown),
                        );
                        final_params[idx] = (final_param.0.clone(), Some(new_type));
                    } else {
                        final_params.push((param.0.clone(), param.1.clone()));
                    }
                }

                final_ret = TypeOps::Union.apply(db, &final_ret, doc_func.get_ret());
            }

            if !variadic_type.is_unknown()
                && let Some(param) = final_params.last_mut()
            {
                param.1 = Some(variadic_type);
            }

            resolve_doc_function(
                db,
                closure_params,
                &LuaFunctionType::new(
                    signature.async_state,
                    signature.is_colon_define,
                    signature.is_vararg,
                    final_params,
                    final_ret,
                ),
                self_type,
            )
        }
        LuaType::Ref(ref_id) => {
            infer_guard.check(ref_id)?;
            let type_decl = db
                .get_type_index()
                .get_type_decl(ref_id)
                .ok_or(InferFailReason::None)?;

            if type_decl.is_alias()
                && let Some(origin) = type_decl.get_alias_origin(db, None)
            {
                return resolve_closure_member_type(
                    db,
                    closure_params,
                    &origin,
                    self_type,
                    infer_guard,
                );
            }
            Ok(())
        }
        _ => Ok(()),
    }
}

fn resolve_doc_function(
    db: &mut DbIndex,
    closure_params: &UnResolveParentClosureParams,
    doc_func: &LuaFunctionType,
    self_type: Option<LuaType>,
) -> ResolveResult {
    let signature = db
        .get_signature_index_mut()
        .get_mut(&closure_params.signature_id)
        .ok_or(InferFailReason::None)?;

    signature.async_state = doc_func.get_async_state();

    let mut doc_params = doc_func.get_params().to_vec();
    // doc_func 是往上追溯的有效签名, signature 是未解析的签名
    match (doc_func.is_colon_define(), signature.is_colon_define) {
        (true, false) => {
            // 原始签名是冒号定义, 但未解析的签名不是冒号定义, 即要插入第一个参数
            doc_params.insert(0, ("self".to_string(), Some(LuaType::SelfInfer)));
        }
        (false, true) => {
            if !doc_params.is_empty() {
                doc_params.remove(0);
            }
        }
        _ => {}
    }

    if let Some(self_type) = self_type
        && let Some((_, Some(typ))) = doc_params.first()
        && typ.is_self_infer()
    {
        doc_params[0].1 = Some(self_type);
    }

    for (index, param) in doc_params.iter().enumerate() {
        let name = signature.params.get(index).unwrap_or(&param.0);
        signature.param_docs.insert(
            index,
            LuaDocParamInfo {
                name: name.clone(),
                type_ref: param.1.clone().unwrap_or(LuaType::Any),
                default_value: None,
                description: None,
                nullable: false,
                attributes: None,
            },
        );
    }

    let doc_return = doc_func.get_ret();
    let should_apply_return = match signature.resolve_return {
        SignatureReturnStatus::UnResolve => true,
        SignatureReturnStatus::InferResolve => {
            should_doc_return_override_inferred(doc_return, &signature.get_return_type())
        }
        SignatureReturnStatus::DocResolve => false,
    };

    if should_apply_return {
        signature.resolve_return = SignatureReturnStatus::DocResolve;
        signature.return_docs.clear();
        signature.return_docs.push(LuaDocReturnInfo {
            name: None,
            type_ref: doc_return.clone(),
            default_value: None,
            description: None,
            attributes: None,
            return_kind: ReturnTypeKind::default(),
        });
    }

    Ok(())
}

fn should_doc_return_override_inferred(doc_return: &LuaType, inferred_return: &LuaType) -> bool {
    !(doc_return.is_any() || doc_return.is_unknown())
        || inferred_return.is_any()
        || inferred_return.is_unknown()
}

fn filter_signature_type(
    db: &DbIndex,
    typ: &LuaType,
    origin_signature_id: Option<&LuaSignatureId>,
    preserve_returns: bool,
) -> Option<Vec<Arc<LuaFunctionType>>> {
    let mut result: Vec<Arc<LuaFunctionType>> = Vec::new();
    let mut stack = Vec::new();
    stack.push(typ.clone());
    let guard = InferGuard::new();
    while let Some(typ) = stack.pop() {
        match typ {
            LuaType::DocFunction(func) => {
                result.push(func.clone());
            }
            LuaType::Signature(sig_id) => {
                // Skip the current closure's own signature to avoid self-reference
                if origin_signature_id != Some(&sig_id) {
                    if let Some(sig) = db.get_signature_index().get(&sig_id) {
                        // Convert annotated signature to DocFunction for param propagation.
                        // When preserve_returns is false (monkey-patch path), only emit a
                        // DocFunction when params are annotated — we want param propagation but
                        // must NOT force a return constraint (could break os.exit-style patches).
                        // When preserve_returns is true (hook hover path), emit even for return-
                        // only hooks so that `@return`-annotated hooks with no params display
                        // correctly as `function() -> boolean` rather than silently degrading.
                        let has_useful_info = if preserve_returns {
                            !sig.param_docs.is_empty() || !sig.get_return_type().is_nil()
                        } else {
                            !sig.param_docs.is_empty()
                        };
                        if has_useful_info {
                            let params = sig.get_type_params();
                            let ret = if preserve_returns {
                                sig.get_return_type()
                            } else {
                                LuaType::Nil
                            };
                            let doc_func = LuaFunctionType::new(
                                sig.async_state,
                                sig.is_colon_define,
                                sig.is_vararg,
                                params,
                                ret,
                            )
                            .with_optional_params(sig.get_param_optional_flags());
                            result.push(Arc::new(doc_func));
                        }
                    }
                }
            }
            LuaType::Union(union) => {
                let types = union.into_vec();
                for typ in types.into_iter().rev() {
                    stack.push(typ);
                }
            }
            LuaType::Ref(type_ref_id) => {
                // On cycle, skip this type but continue accumulating other results.
                if guard.check(&type_ref_id).is_err() {
                    continue;
                }
                let Some(type_decl) = db.get_type_index().get_type_decl(&type_ref_id) else {
                    continue;
                };
                if let Some(func) = type_decl.get_alias_origin(db, None) {
                    match func {
                        LuaType::DocFunction(f) => {
                            result.push(f.clone());
                        }
                        LuaType::Union(union) => {
                            let types = union.into_vec();
                            for typ in types.into_iter().rev() {
                                stack.push(typ);
                            }
                        }
                        _ => {}
                    }
                }
            }
            _ => {}
        }
    }
    if result.is_empty() {
        None
    } else {
        Some(result)
    }
}

fn find_best_function_type(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    prefix_type: &LuaType,
    index_member_expr: LuaIndexMemberExpr,
    origin_signature: &LuaSignature,
    origin_sig_id: Option<&LuaSignatureId>,
) -> Option<LuaType> {
    // 寻找非自身定义的签名
    if let Ok(result) = find_decl_function_type(db, cache, prefix_type, index_member_expr) {
        if result.is_current_owner {
            // 对应当前类型下的声明, 我们需要过滤掉所有`signature`类型
            if let Some(filtered_types) =
                filter_signature_type(db, &result.typ, origin_sig_id, false)
            {
                match filtered_types.len() {
                    0 => {}
                    1 => return Some(LuaType::DocFunction(filtered_types[0].clone())),
                    _ => {
                        return Some(LuaType::from_vec(
                            filtered_types
                                .into_iter()
                                .map(|func| LuaType::DocFunction(func.clone()))
                                .collect(),
                        ));
                    }
                }
            }
        } else {
            return Some(result.typ);
        }
    }

    match origin_signature.overloads.len() {
        0 => None,
        1 => origin_signature
            .overloads
            .clone()
            .into_iter()
            .next()
            .map(LuaType::DocFunction),
        _ => Some(LuaType::from_vec(
            origin_signature
                .overloads
                .clone()
                .into_iter()
                .map(LuaType::DocFunction)
                .collect(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use glua_parser::{LuaAstNode, LuaCallExpr, LuaClosureExpr, LuaParser, ParserConfig};

    use super::resolve_call_param_doc_function;
    use crate::{
        AsyncState, DbIndex, FileId, LuaFunctionType, LuaSignatureId, LuaType, LuaUnionType,
    };

    fn parse_call_and_closure(code: &str) -> (LuaCallExpr, LuaClosureExpr) {
        let tree = LuaParser::parse(code, ParserConfig::default());
        let root = tree.get_chunk_node();
        let call_expr = root
            .descendants::<LuaCallExpr>()
            .next()
            .expect("expected call expression");
        let closure_expr = root
            .descendants::<LuaClosureExpr>()
            .next()
            .expect("expected closure expression");
        (call_expr, closure_expr)
    }

    fn make_doc_func(
        param_name: &str,
        param_type: LuaType,
        return_type: LuaType,
    ) -> Arc<LuaFunctionType> {
        Arc::new(LuaFunctionType::new(
            AsyncState::None,
            false,
            false,
            vec![(param_name.to_string(), Some(param_type))],
            return_type,
        ))
    }

    fn make_call_sig_with_callback_union(union: LuaUnionType) -> LuaFunctionType {
        LuaFunctionType::new(
            AsyncState::None,
            false,
            false,
            vec![(
                "callback".to_string(),
                Some(LuaType::Union(Arc::new(union))),
            )],
            LuaType::Nil,
        )
    }

    #[test]
    fn resolve_call_param_doc_function_union_doc_function_order_is_deterministic() {
        let db = DbIndex::new();
        let file_id = FileId::new(1);
        let (call_expr, closure_expr) = parse_call_and_closure("register(function() end)");
        let origin_signature_id = LuaSignatureId::from_closure(file_id, &closure_expr);

        let alpha_doc_func = make_doc_func("alpha", LuaType::String, LuaType::Boolean);
        let beta_doc_func = make_doc_func("beta", LuaType::Integer, LuaType::Number);

        let alpha_first_sig = make_call_sig_with_callback_union(LuaUnionType::from_vec(vec![
            LuaType::DocFunction(alpha_doc_func.clone()),
            LuaType::DocFunction(beta_doc_func.clone()),
        ]));
        let beta_first_sig = make_call_sig_with_callback_union(LuaUnionType::from_vec(vec![
            LuaType::DocFunction(beta_doc_func),
            LuaType::DocFunction(alpha_doc_func),
        ]));

        let selected_alpha_first = resolve_call_param_doc_function(
            &db,
            &call_expr,
            &alpha_first_sig,
            0,
            origin_signature_id,
            file_id,
        )
        .expect("expected callback doc function to be selected from alpha-first union");
        let selected_beta_first = resolve_call_param_doc_function(
            &db,
            &call_expr,
            &beta_first_sig,
            0,
            origin_signature_id,
            file_id,
        )
        .expect("expected callback doc function to be selected from beta-first union");

        assert_eq!(
            selected_alpha_first.as_ref(),
            selected_beta_first.as_ref(),
            "doc function selection should be independent of union member order"
        );
    }
}
