#[cfg(test)]
mod test {
    use glua_parser::{LuaAst, LuaAstNode, LuaAstToken, LuaLocalName, LuaNameExpr};
    use googletest::prelude::*;
    use lsp_types::NumberOrString;
    use tokio_util::sync::CancellationToken;

    use crate::{
        DiagnosticCode, Emmyrc, EmmyrcGmodScriptedClassScopeEntry, GlobalId, GmodClassCallLiteral,
        LuaMemberId, LuaMemberKey, LuaMemberOwner, LuaType, LuaTypeDeclId, VirtualWorkspace,
    };

    fn legacy_scope(pattern: &str) -> EmmyrcGmodScriptedClassScopeEntry {
        EmmyrcGmodScriptedClassScopeEntry::LegacyGlob(pattern.to_string())
    }

    fn legacy_scopes(patterns: &[&str]) -> Vec<EmmyrcGmodScriptedClassScopeEntry> {
        patterns
            .iter()
            .map(|pattern| legacy_scope(pattern))
            .collect()
    }

    fn index_expr_type(
        ws: &mut VirtualWorkspace,
        file_id: crate::FileId,
        expr_text: &str,
    ) -> LuaType {
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        semantic_model
            .get_root()
            .descendants::<LuaAst>()
            .find_map(|node| match node {
                LuaAst::LuaIndexExpr(index_expr) if index_expr.syntax().text() == expr_text => {
                    semantic_model
                        .get_semantic_info(index_expr.syntax().clone().into())
                        .map(|info| info.typ)
                }
                _ => None,
            })
            .expect("expected semantic info for index expr")
    }

    fn index_expr_prefix_type(
        ws: &mut VirtualWorkspace,
        file_id: crate::FileId,
        expr_text: &str,
    ) -> LuaType {
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let index_expr = semantic_model
            .get_root()
            .descendants::<glua_parser::LuaIndexExpr>()
            .find(|index_expr| index_expr.syntax().text() == expr_text)
            .expect("expected index expr");
        let prefix_expr = index_expr.get_prefix_expr().expect("expected prefix expr");

        semantic_model
            .infer_expr(prefix_expr)
            .expect("expected inferred prefix expr type")
    }

    fn assign_value_type(
        ws: &mut VirtualWorkspace,
        file_id: crate::FileId,
        lhs_text: &str,
    ) -> LuaType {
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let assign_stat = semantic_model
            .get_root()
            .descendants::<glua_parser::LuaAssignStat>()
            .find(|assign_stat| {
                assign_stat
                    .get_var_and_expr_list()
                    .0
                    .into_iter()
                    .any(|var| var.syntax().text() == lhs_text)
            })
            .expect("expected assignment stat");
        let (vars, exprs) = assign_stat.get_var_and_expr_list();
        let index = vars
            .iter()
            .position(|var| var.syntax().text() == lhs_text)
            .expect("expected matching LHS variable");
        let expr = exprs
            .get(index)
            .cloned()
            .expect("expected corresponding assignment value");

        semantic_model
            .infer_expr(expr)
            .expect("expected inferred assignment value type")
    }

    fn local_name_type(ws: &mut VirtualWorkspace, file_id: crate::FileId, name: &str) -> LuaType {
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let local_name = semantic_model
            .get_root()
            .descendants::<LuaLocalName>()
            .find(|local_name| {
                local_name
                    .get_name_token()
                    .is_some_and(|token| token.get_name_text() == name)
            })
            .expect("expected local name");
        let token = local_name
            .get_name_token()
            .expect("expected local name token");

        semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .map(|info| info.typ)
            .expect("expected semantic info for local name")
    }

    fn local_assignment_value_type(
        ws: &mut VirtualWorkspace,
        file_id: crate::FileId,
        name: &str,
    ) -> LuaType {
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let local_stat = semantic_model
            .get_root()
            .descendants::<glua_parser::LuaLocalStat>()
            .find(|local_stat| {
                local_stat.get_local_name_list().any(|local_name| {
                    local_name
                        .get_name_token()
                        .is_some_and(|token| token.get_name_text() == name)
                })
            })
            .expect("expected local assignment");
        let name_index = local_stat
            .get_local_name_list()
            .position(|local_name| {
                local_name
                    .get_name_token()
                    .is_some_and(|token| token.get_name_text() == name)
            })
            .expect("expected local name in assignment");
        let expr = local_stat
            .get_value_exprs()
            .nth(name_index)
            .expect("expected local assignment value");

        semantic_model
            .infer_expr(expr)
            .expect("expected inferred local assignment value type")
    }

    fn super_types_of(ws: &mut VirtualWorkspace, class_name: &str) -> Vec<LuaType> {
        let db = ws.get_db_mut();
        db.get_type_index()
            .get_super_types_iter(&LuaTypeDeclId::global(class_name))
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default()
    }

    fn add_gamemode_gm_library(ws: &mut VirtualWorkspace) {
        let library_root = ws.virtual_url_generator.new_path("__test_library_gm");
        ws.analysis.add_library_workspace(library_root.clone());
        let library_uri =
            lsp_types::Uri::parse_from_file_path(&library_root.join("gm.lua")).unwrap();
        ws.analysis.update_file_by_uri(
            &library_uri,
            Some(
                r#"
                ---@class GM
                function GM:SetupMove(ply, mv, cmd) end
                "#
                .to_string(),
            ),
        );
    }

    #[gtest]
    fn test_extracts_scripted_class_call_literals() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        let file_id = ws.def_file(
            "lua/entities/entities_test.lua",
            r#"
            DEFINE_BASECLASS("base_anim")
            AccessorFunc(ENT, "m_iHealth", "Health", true)
            ENT:NetworkVar("Float", 0, "MoveSpeed", nil)
        "#,
        );

        let metadata = ws
            .get_db_mut()
            .get_gmod_class_metadata_index()
            .get_file_metadata(&file_id)
            .cloned()
            .expect("expected scripted class metadata");

        assert_eq!(metadata.define_baseclass_calls.len(), 1);
        assert_eq!(
            metadata.define_baseclass_calls[0].literal_args,
            vec![Some(GmodClassCallLiteral::String("base_anim".to_string()))]
        );

        assert_eq!(metadata.accessor_func_calls.len(), 1);
        assert_eq!(
            metadata.accessor_func_calls[0].literal_args,
            vec![
                Some(GmodClassCallLiteral::NameRef("ENT".to_string())),
                Some(GmodClassCallLiteral::String("m_iHealth".to_string())),
                Some(GmodClassCallLiteral::String("Health".to_string())),
                Some(GmodClassCallLiteral::Boolean(true))
            ]
        );

        assert_eq!(metadata.network_var_calls.len(), 1);
        assert_eq!(
            metadata.network_var_calls[0].literal_args,
            vec![
                Some(GmodClassCallLiteral::String("Float".to_string())),
                Some(GmodClassCallLiteral::Integer(0)),
                Some(GmodClassCallLiteral::String("MoveSpeed".to_string())),
                Some(GmodClassCallLiteral::Nil)
            ]
        );
    }

    #[gtest]
    fn test_scripted_class_index_clears_on_reparse_without_patterns() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        let file_name = "lua/entities/scripted.lua";

        let file_id = ws.def_file(file_name, r#"DEFINE_BASECLASS("base_anim")"#);
        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_some()
        );

        ws.def_file(file_name, "local value = 1");
        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_none()
        );
    }

    #[gtest]
    fn test_scripted_class_metadata_disabled_when_gmod_disabled() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = false;
        ws.update_emmyrc(emmyrc);
        let file_id = ws.def(r#"DEFINE_BASECLASS("base_anim")"#);
        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_none()
        );
    }

    #[gtest]
    fn test_scripted_class_scope_filters_metadata_collection() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")];
        ws.update_emmyrc(emmyrc);

        let allowed_file_id = ws.def_file(
            "lua/entities/entities_test.lua",
            r#"
            DEFINE_BASECLASS("base_anim")
            AccessorFunc(ENT, "m_iHealth", "Health", true)
        "#,
        );
        let denied_file_id = ws.def_file(
            "lua/autorun/ignored.lua",
            r#"
            DEFINE_BASECLASS("base_anim")
            AccessorFunc(ENT, "m_iHealth", "Health", true)
        "#,
        );

        let allowed_metadata = ws
            .get_db_mut()
            .get_gmod_class_metadata_index()
            .get_file_metadata(&allowed_file_id)
            .cloned()
            .expect("expected scoped metadata for allowed scripted-class file");
        assert_eq!(allowed_metadata.define_baseclass_calls.len(), 1);
        assert_eq!(allowed_metadata.accessor_func_calls.len(), 1);

        let denied_metadata = ws
            .get_db_mut()
            .get_gmod_class_metadata_index()
            .get_file_metadata(&denied_file_id)
            .cloned()
            .expect("expected DEFINE_BASECLASS metadata for out-of-scope file");
        assert_eq!(denied_metadata.define_baseclass_calls.len(), 1);
        // AccessorFunc is always collected regardless of scripted class scope,
        // since it's used by VGUI panels and other non-entity code.
        assert_eq!(denied_metadata.accessor_func_calls.len(), 1);
    }

    #[gtest]
    fn test_scripted_class_scope_matches_nested_entities_folder_anywhere() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "addons/mygamemode/gamemode/entities/entities_test/shared.lua",
            r#"DEFINE_BASECLASS("base_anim")"#,
        );

        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_some()
        );
    }

    #[gtest]
    fn test_plugin_scope_binds_plugin_decl_to_scoped_class() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("plugins/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "plugins/vehicles/sh_plugin.lua",
            r#"
            local PLUGIN = {}

            function PLUGIN:PlayerSpawn(client)
            end
        "#,
        );

        let plugin_decl_id = {
            let db = ws.get_db_mut();
            let decl_tree = db
                .get_decl_index()
                .get_decl_tree(&file_id)
                .expect("expected decl tree");
            decl_tree
                .get_decls()
                .values()
                .find(|decl| decl.get_name() == "PLUGIN" && decl.is_local())
                .expect("expected local PLUGIN declaration")
                .get_id()
        };

        {
            let db = ws.get_db_mut();
            let type_cache = db
                .get_type_index()
                .get_type_cache(&plugin_decl_id.into())
                .expect("expected PLUGIN declaration type cache");
            assert_eq!(
                type_cache.as_type(),
                &LuaType::Def(LuaTypeDeclId::global("vehicles"))
            );

            let plugin_class_id = LuaTypeDeclId::global("vehicles");
            let plugin_class_decl = db
                .get_type_index()
                .get_type_decl(&plugin_class_id)
                .expect("expected inferred plugin class declaration");
            assert!(plugin_class_decl.is_class());

            let super_types = db
                .get_type_index()
                .get_super_types(&plugin_class_id)
                .expect("expected inferred plugin super types");
            assert!(
                super_types
                    .iter()
                    .any(|ty| ty == &LuaType::Ref(LuaTypeDeclId::global("PLUGIN")))
            );
            assert!(
                super_types
                    .iter()
                    .any(|ty| ty == &LuaType::Ref(LuaTypeDeclId::global("GM")))
            );
        }
    }

    #[gtest]
    fn test_plugin_scope_binds_plugin_decl_with_self_reference_initializer() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("plugins/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "plugins/vehicles/sh_plugin.lua",
            r#"
            local PLUGIN = PLUGIN ---@diagnostic disable-line: undefined-global

            function PLUGIN:PlayerSpawn(client)
            end
        "#,
        );

        let plugin_decl_id = {
            let db = ws.get_db_mut();
            let decl_tree = db
                .get_decl_index()
                .get_decl_tree(&file_id)
                .expect("expected decl tree");
            decl_tree
                .get_decls()
                .values()
                .find(|decl| decl.get_name() == "PLUGIN" && decl.is_local())
                .expect("expected local PLUGIN declaration")
                .get_id()
        };

        let db = ws.get_db_mut();
        let type_cache = db
            .get_type_index()
            .get_type_cache(&plugin_decl_id.into())
            .expect("expected PLUGIN declaration type cache");
        assert_eq!(
            type_cache.as_type(),
            &LuaType::Def(LuaTypeDeclId::global("vehicles"))
        );

        let local_name = ws.get_node::<LuaLocalName>(file_id);
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let token = local_name
            .get_name_token()
            .expect("expected local PLUGIN name token");
        let semantic_info = semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .expect("expected semantic info for local PLUGIN name");
        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("vehicles"))
        );
    }

    #[gtest]
    fn test_plugin_scope_binding_respects_scope_filters() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "plugins/vehicles/sh_plugin.lua",
            r#"
            local PLUGIN = {}
        "#,
        );

        let plugin_decl_id = {
            let db = ws.get_db_mut();
            let decl_tree = db
                .get_decl_index()
                .get_decl_tree(&file_id)
                .expect("expected decl tree");
            decl_tree
                .get_decls()
                .values()
                .find(|decl| decl.get_name() == "PLUGIN" && decl.is_local())
                .expect("expected local PLUGIN declaration")
                .get_id()
        };

        let db = ws.get_db_mut();
        let type_cache = db
            .get_type_index()
            .get_type_cache(&plugin_decl_id.into())
            .expect("expected PLUGIN declaration type cache");
        assert_ne!(
            type_cache.as_type(),
            &LuaType::Def(LuaTypeDeclId::global("vehicles"))
        );
    }

    #[gtest]
    fn test_entity_scope_infers_ent_reference_to_scoped_class_without_local_decl() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "plugins/vehicles/entities/entities/vehicles_money/sh_init.lua",
            r#"
            ENT.Type = "anim"
            ENT.Base = "base_gmodentity"
        "#,
        );

        let name_expr = ws.get_node::<LuaNameExpr>(file_id);
        let token = name_expr.get_name_token().expect("expected ENT name token");
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let semantic_info = semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .expect("expected semantic info for ENT");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("vehicles_money"))
        );
    }

    #[gtest]
    fn test_entity_scope_ent_decl_is_local_and_not_indexed_globally() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "entities/entities/cityrp_money/sh_init.lua",
            r#"
            ENT = ENT or {}
            ENT.Type = "anim"
            ENT.Base = "base_gmodentity"
        "#,
        );

        let db = ws.get_db_mut();
        let (ent_decl_id, ent_decl_is_local) = {
            let decl_tree = db
                .get_decl_index()
                .get_decl_tree(&file_id)
                .expect("expected decl tree");
            let ent_decl = decl_tree
                .get_decls()
                .values()
                .find(|decl| decl.get_name() == "ENT")
                .expect("expected ENT declaration");
            (ent_decl.get_id(), ent_decl.is_local())
        };

        assert!(ent_decl_is_local, "expected ENT declaration to be local");

        let has_ent_global_decl = db
            .get_global_index()
            .get_global_decl_ids("ENT")
            .is_some_and(|decl_ids| decl_ids.contains(&ent_decl_id));
        assert!(
            !has_ent_global_decl,
            "expected ENT declaration to be excluded from global index"
        );

        let has_ent_global_refs_in_file = db
            .get_reference_index()
            .get_global_file_references("ENT", file_id)
            .is_some_and(|refs| !refs.is_empty());
        assert!(
            !has_ent_global_refs_in_file,
            "expected no global ENT references for scripted-scope file"
        );
    }

    #[gtest]
    fn test_entity_scope_shadowed_ent_assignment_does_not_bind_scoped_class_member() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "entities/entities/shadowed_ent/init.lua",
            r#"
            local ENT = {}
            ENT.LocalOnly = 1
        "#,
        );

        let db = ws.get_db_mut();
        let member_names: Vec<_> = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("shadowed_ent")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect()
            })
            .unwrap_or_default();

        assert!(
            !member_names.contains(&"LocalOnly".to_string()),
            "shadowed local ENT assignment should not bind to scripted class members: {member_names:?}"
        );
    }

    #[gtest]
    fn test_entity_scope_shadowed_ent_method_self_does_not_bind_scoped_class_member() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "entities/entities/shadowed_method_ent/init.lua",
            r#"
            local ENT = {}
            function ENT:Build()
                self.LocalOnly = 1
            end
        "#,
        );

        let db = ws.get_db_mut();
        let member_names: Vec<_> = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global(
                "shadowed_method_ent",
            )))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect()
            })
            .unwrap_or_default();

        assert!(
            !member_names.contains(&"Build".to_string())
                && !member_names.contains(&"LocalOnly".to_string()),
            "shadowed local ENT method should not bind to scripted class members: {member_names:?}"
        );
    }

    #[gtest]
    fn test_entity_scope_creates_class_decl_without_ent_base_assignment() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "entities/entities/cityrp_inventory/init.lua",
            r#"
            function ENT:Initialize()
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("cityrp_inventory");
        let class_decl = db.get_type_index().get_type_decl(&class_id);
        assert!(class_decl.is_some(), "expected inferred class declaration");

        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();
        assert!(
            super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("ENT"))),
            "expected ENT as super type, got {super_types:?}"
        );

        let name_expr = ws.get_node::<LuaNameExpr>(file_id);
        let token = name_expr.get_name_token().expect("expected ENT name token");
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let semantic_info = semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .expect("expected semantic info for ENT");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("cityrp_inventory"))
        );
    }

    #[gtest]
    fn test_entity_method_self_infers_scoped_class_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "entities/entities/cityrp_inventory/init.lua",
            r#"
            function ENT:Initialize()
                local self_copy = self
            end
        "#,
        );

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let self_name_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|name_expr| {
                name_expr
                    .get_name_token()
                    .is_some_and(|token| token.get_name_text() == "self")
            })
            .expect("expected self name expr in method body");
        let self_token = self_name_expr
            .get_name_token()
            .expect("expected self token");
        let semantic_info = semantic_model
            .get_semantic_info(self_token.syntax().clone().into())
            .expect("expected semantic info for self");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("cityrp_inventory"))
        );
    }

    #[gtest]
    fn test_accessor_func_synthesizes_get_set_members() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/my_entity/init.lua",
            r#"
            AccessorFunc(ENT, "m_iHealth", "Health", FORCE_NUMBER)
            AccessorFunc(ENT, "m_sName", "Name", FORCE_STRING)
            AccessorFunc(ENT, "m_bActive", "Active", true)
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("my_entity");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on my_entity class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        // Should have Get/Set for Health, Name, Active + backing fields
        assert!(
            member_names.contains(&"GetHealth".to_string()),
            "missing GetHealth in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetHealth".to_string()),
            "missing SetHealth in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetName".to_string()),
            "missing GetName in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetName".to_string()),
            "missing SetName in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetActive".to_string()),
            "missing GetActive in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetActive".to_string()),
            "missing SetActive in {member_names:?}"
        );
        assert!(
            member_names.contains(&"m_iHealth".to_string()),
            "missing m_iHealth backing field in {member_names:?}"
        );
    }

    #[gtest]
    fn test_accessor_func_force_type_resolves_correctly() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/typed_ent/init.lua",
            r#"
            AccessorFunc(ENT, "m_vPos", "Position", FORCE_VECTOR)
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("typed_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on typed_ent class");

        let getter = members
            .iter()
            .find(|m| m.get_key().get_name() == Some("GetPosition"))
            .expect("expected GetPosition member");

        let getter_type = db.get_type_index().get_type_cache(&getter.get_id().into());
        assert!(
            getter_type.is_some(),
            "GetPosition should have a type bound"
        );
    }

    #[gtest]
    fn test_network_var_synthesizes_get_set_members() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/nw_entity/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:NetworkVar("Float", 0, "Speed")
                self:NetworkVar("String", 0, "Label")
                self:NetworkVar("Entity", 0, "Owner")
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("nw_entity");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on nw_entity class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetSpeed".to_string()),
            "missing GetSpeed in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetSpeed".to_string()),
            "missing SetSpeed in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetLabel".to_string()),
            "missing GetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetLabel".to_string()),
            "missing SetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetOwner".to_string()),
            "missing GetOwner in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetOwner".to_string()),
            "missing SetOwner in {member_names:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_sets_super_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/derived_ent/init.lua",
            r#"
            DEFINE_BASECLASS("base_anim")
            ENT.Type = "anim"
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("derived_ent");
        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();

        assert!(
            super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("base_anim"))),
            "expected base_anim super type, got {super_types:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_infers_baseclass_local_and_no_undefined_global() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedGlobal);

        let file_id = ws.def_file(
            "lua/entities/fl_dodge_charger/shared.lua",
            r#"
            DEFINE_BASECLASS("base_glide")
            local _ = BaseClass
        "#,
        );

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let baseclass_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|name_expr| name_expr.get_name_text().as_deref() == Some("BaseClass"))
            .expect("expected BaseClass name expression");
        let baseclass_token = baseclass_expr
            .get_name_token()
            .expect("expected BaseClass token");
        let semantic_info = semantic_model
            .get_semantic_info(baseclass_token.syntax().clone().into())
            .expect("expected semantic info for BaseClass");

        assert_eq!(
            semantic_info.typ,
            LuaType::Ref(LuaTypeDeclId::global("base_glide")),
            "expected BaseClass to resolve to base_glide"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let undefined_global_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedGlobal.get_name().to_string(),
        ));

        assert!(
            diagnostics.iter().all(|diag| {
                diag.code != undefined_global_code || !diag.message.contains("BaseClass")
            }),
            "unexpected undefined-global diagnostic for BaseClass: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_infers_baseclass_outside_scripted_scope_and_no_undefined_global() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedGlobal);

        let file_id = ws.def_file(
            "lua/vgui/test_panel.lua",
            r#"
            DEFINE_BASECLASS("base_panel")
            local _ = BaseClass
        "#,
        );

        {
            let metadata = ws
                .get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .cloned()
                .expect("expected scripted class metadata for out-of-scope DEFINE_BASECLASS");
            assert_eq!(metadata.define_baseclass_calls.len(), 1);
            assert_eq!(metadata.accessor_func_calls.len(), 0);
        }

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let baseclass_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|name_expr| name_expr.get_name_text().as_deref() == Some("BaseClass"))
            .expect("expected BaseClass name expression");
        let baseclass_token = baseclass_expr
            .get_name_token()
            .expect("expected BaseClass token");
        let semantic_info = semantic_model
            .get_semantic_info(baseclass_token.syntax().clone().into())
            .expect("expected semantic info for BaseClass");

        assert_eq!(
            semantic_info.typ,
            LuaType::Ref(LuaTypeDeclId::global("base_panel")),
            "expected BaseClass to resolve to base_panel"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let undefined_global_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedGlobal.get_name().to_string(),
        ));

        assert!(
            diagnostics.iter().all(|diag| {
                diag.code != undefined_global_code || !diag.message.contains("BaseClass")
            }),
            "unexpected undefined-global diagnostic for BaseClass: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_derma_define_control_baseclass_member_uses_base_arg_syntax_id() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/vgui/baseclass_source_anchor.lua",
            r#"
            local PANEL = {}
            derma.DefineControl("AnchorPanel", "", PANEL, "EditablePanel")
        "#,
        );

        let db = ws.get_db_mut();
        let metadata = db
            .get_gmod_class_metadata_index()
            .get_file_metadata(&file_id)
            .expect("expected gmod metadata");
        let call = metadata
            .derma_define_control_calls
            .first()
            .expect("expected derma.DefineControl call metadata");
        let expected_member_id = LuaMemberId::new(
            call.args
                .get(3)
                .expect("expected base panel arg metadata")
                .syntax_id,
            file_id,
        );

        let member_item = db
            .get_member_index()
            .get_member_item(
                &LuaMemberOwner::Type(LuaTypeDeclId::global("AnchorPanel")),
                &LuaMemberKey::Name("BaseClass".into()),
            )
            .expect("expected synthesized BaseClass member");

        assert_eq!(
            *member_item,
            crate::LuaMemberIndexItem::One(expected_member_id),
            "BaseClass member should be anchored to the base-panel arg syntax id"
        );
    }

    #[gtest]
    fn test_ent_base_from_shared_file_sets_folder_class_super_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "entities/entities/cityrp_money/sh_init.lua",
            r#"
            ENT.Base = "cityrp_base"
        "#,
        );

        let file_id = ws.def_file(
            "entities/entities/cityrp_money/init.lua",
            r#"
            function ENT:Initialize()
            end
        "#,
        );

        {
            let db = ws.get_db_mut();
            let class_id = LuaTypeDeclId::global("cityrp_money");
            let super_types: Vec<_> = db
                .get_type_index()
                .get_super_types_iter(&class_id)
                .map(|iter| iter.cloned().collect())
                .unwrap_or_default();

            assert!(
                super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("cityrp_base"))),
                "expected cityrp_base super type, got {super_types:?}"
            );
        }

        let name_expr = ws.get_node::<LuaNameExpr>(file_id);
        let token = name_expr.get_name_token().expect("expected ENT name token");
        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let semantic_info = semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .expect("expected semantic info for ENT");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("cityrp_money"))
        );
    }

    #[gtest]
    fn test_ent_base_known_gmod_base_maps_to_ent_super_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/mapped_ent/shared.lua",
            r#"
            ENT.Base = "base_gmodentity"
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("mapped_ent");
        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();

        assert!(
            super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("ENT"))),
            "expected ENT super type, got {super_types:?}"
        );
        assert!(
            !super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("base_gmodentity"))),
            "base_gmodentity should map to ENT super type"
        );
    }

    #[gtest]
    fn test_doc_param_resolves_scoped_entity_type_without_type_not_found() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::TypeNotFound);

        ws.def_files(vec![
            (
                "lua/entities/base_glide_car/shared.lua",
                r#"
                function ENT:Initialize()
                end
            "#,
            ),
            (
                "lua/autorun/use_base.lua",
                r#"
                ---@param ent base_glide_car
                local function consume(ent)
                end
            "#,
            ),
        ]);

        let param_uri = ws.virtual_url_generator.new_uri("lua/autorun/use_base.lua");
        let param_file_id = ws
            .analysis
            .get_file_id(&param_uri)
            .expect("expected test file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(param_file_id, CancellationToken::new())
            .unwrap_or_default();

        let type_not_found_code = Some(NumberOrString::String(
            DiagnosticCode::TypeNotFound.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != type_not_found_code),
            "unexpected type-not-found diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_creates_panel_class() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/my_panel.lua",
            r#"
            local PANEL = {}

            function PANEL:Init()
            end

            function PANEL:Paint(w, h)
            end

            vgui.Register("MyPanel", PANEL, "DPanel")
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("MyPanel");

        // Class should exist
        let decl = db.get_type_index().get_type_decl(&class_id);
        assert!(decl.is_some(), "MyPanel class should be created");

        // Should have DPanel as super type
        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();

        assert!(
            super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("DPanel"))),
            "expected DPanel super type, got {super_types:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_multiple_panels_bind_nearest_panel_decl() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/multi_panel.lua",
            r#"
            local PANEL = {}

            function PANEL:Init()
            end

            vgui.Register("MyPanelOne", PANEL, "DFrame")

            local PANEL = {}

            function PANEL:Paint(w, h)
            end

            vgui.Register("MyPanelTwo", PANEL, "EditablePanel")
        "#,
        );

        let db = ws.get_db_mut();

        let first_class_id = LuaTypeDeclId::global("MyPanelOne");
        let second_class_id = LuaTypeDeclId::global("MyPanelTwo");

        assert!(
            db.get_type_index().get_type_decl(&first_class_id).is_some(),
            "MyPanelOne class should be created"
        );
        assert!(
            db.get_type_index()
                .get_type_decl(&second_class_id)
                .is_some(),
            "MyPanelTwo class should be created"
        );

        let first_supers: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&first_class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();
        assert!(
            first_supers.contains(&LuaType::Ref(LuaTypeDeclId::global("DFrame"))),
            "expected DFrame super type for MyPanelOne, got {first_supers:?}"
        );

        let second_supers: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&second_class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();
        assert!(
            second_supers.contains(&LuaType::Ref(LuaTypeDeclId::global("EditablePanel"))),
            "expected EditablePanel super type for MyPanelTwo, got {second_supers:?}"
        );

        let first_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(first_class_id.clone()))
            .expect("expected members on MyPanelOne");
        let first_member_names: Vec<_> = first_members
            .iter()
            .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
            .collect();

        assert!(
            first_member_names.contains(&"Init".to_string()),
            "expected Init on MyPanelOne, got {first_member_names:?}"
        );
        assert!(
            !first_member_names.contains(&"Paint".to_string()),
            "MyPanelOne should not inherit PANEL:Paint from second panel, got {first_member_names:?}"
        );

        let second_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(second_class_id.clone()))
            .expect("expected members on MyPanelTwo");
        let second_member_names: Vec<_> = second_members
            .iter()
            .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
            .collect();

        assert!(
            second_member_names.contains(&"Paint".to_string()),
            "expected Paint on MyPanelTwo, got {second_member_names:?}"
        );
        assert!(
            !second_member_names.contains(&"Init".to_string()),
            "MyPanelTwo should not inherit PANEL:Init from first panel, got {second_member_names:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_reassigned_local_panel_transfers_members_per_register() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/reassigned_panel.lua",
            r#"
            local PANEL = {}
            function PANEL:Init() end
            vgui.Register("PanelA", PANEL, "DFrame")

            PANEL = {}
            function PANEL:Paint(w, h) end
            vgui.Register("PanelB", PANEL, "EditablePanel")
        "#,
        );

        let db = ws.get_db_mut();
        let panel_a_id = LuaTypeDeclId::global("PanelA");
        let panel_b_id = LuaTypeDeclId::global("PanelB");

        let panel_a_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(panel_a_id.clone()))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            panel_a_members.contains(&"Init".to_string()),
            "expected Init on PanelA, got {panel_a_members:?}"
        );
        assert!(
            !panel_a_members.contains(&"Paint".to_string()),
            "PanelA should not inherit Paint from reassigned PANEL, got {panel_a_members:?}"
        );

        let panel_b_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(panel_b_id.clone()))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            panel_b_members.contains(&"Paint".to_string()),
            "expected Paint on PanelB, got {panel_b_members:?}"
        );
        assert!(
            !panel_b_members.contains(&"Init".to_string()),
            "PanelB should not inherit Init from first PANEL table, got {panel_b_members:?}"
        );
    }

    #[gtest]
    fn test_vgui_reassigned_panel_accessor_func_stays_in_own_region() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/reassigned_panel_accessors.lua",
            r#"
            local PANEL = {}
            AccessorFunc(PANEL, "m_a", "A")
            vgui.Register("PanelA", PANEL, "DPanel")

            PANEL = {}
            AccessorFunc(PANEL, "m_b", "B")
            vgui.Register("PanelB", PANEL, "DPanel")
        "#,
        );

        let db = ws.get_db_mut();
        let panel_a_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("PanelA")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let panel_b_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("PanelB")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            panel_a_members.contains(&"GetA".to_string())
                && panel_a_members.contains(&"SetA".to_string()),
            "PanelA should contain only A accessors, got {panel_a_members:?}"
        );
        assert!(
            !panel_a_members.contains(&"GetB".to_string())
                && !panel_a_members.contains(&"SetB".to_string()),
            "PanelA should not inherit B accessors, got {panel_a_members:?}"
        );

        assert!(
            panel_b_members.contains(&"GetB".to_string())
                && panel_b_members.contains(&"SetB".to_string()),
            "PanelB should contain only B accessors, got {panel_b_members:?}"
        );
        assert!(
            !panel_b_members.contains(&"GetA".to_string())
                && !panel_b_members.contains(&"SetA".to_string()),
            "PanelB should not inherit A accessors, got {panel_b_members:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_reassigned_local_panel_stress_three_panels() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/reassigned_panel_stress.lua",
            r#"
            local PANEL = {}
            function PANEL:Alpha() end
            vgui.Register("PA", PANEL, "Panel")

            PANEL = {}
            function PANEL:Beta() end
            vgui.Register("PB", PANEL, "DFrame")

            PANEL = {}
            function PANEL:Gamma() end
            vgui.Register("PC", PANEL, "EditablePanel")
        "#,
        );

        let db = ws.get_db_mut();
        let pa_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("PA")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let pb_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("PB")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let pc_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("PC")))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            pa_members.contains(&"Alpha".to_string()),
            "expected Alpha on PA, got {pa_members:?}"
        );
        assert!(
            !pa_members.contains(&"Beta".to_string()) && !pa_members.contains(&"Gamma".to_string()),
            "PA should only contain Alpha, got {pa_members:?}"
        );

        assert!(
            pb_members.contains(&"Beta".to_string()),
            "expected Beta on PB, got {pb_members:?}"
        );
        assert!(
            !pb_members.contains(&"Alpha".to_string())
                && !pb_members.contains(&"Gamma".to_string()),
            "PB should only contain Beta, got {pb_members:?}"
        );

        assert!(
            pc_members.contains(&"Gamma".to_string()),
            "expected Gamma on PC, got {pc_members:?}"
        );
        assert!(
            !pc_members.contains(&"Alpha".to_string()) && !pc_members.contains(&"Beta".to_string()),
            "PC should only contain Gamma, got {pc_members:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_panel_decl_inside_closed_do_block_does_not_leak_to_outer_register() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/panel_do_scope.lua",
            r#"
            do
                local PANEL = {}
                function PANEL:Init() end
                vgui.Register("InnerPanel", PANEL, "DFrame")
            end

            local PANEL = {}
            function PANEL:Paint(w, h) end
            vgui.Register("OuterPanel", PANEL, "EditablePanel")
        "#,
        );

        let db = ws.get_db_mut();
        let inner_class_id = LuaTypeDeclId::global("InnerPanel");
        let outer_class_id = LuaTypeDeclId::global("OuterPanel");

        assert!(
            db.get_type_index().get_type_decl(&inner_class_id).is_some(),
            "InnerPanel class should be created"
        );
        assert!(
            db.get_type_index().get_type_decl(&outer_class_id).is_some(),
            "OuterPanel class should be created"
        );

        let inner_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(inner_class_id.clone()))
            .expect("expected members on InnerPanel");
        let inner_member_names: Vec<_> = inner_members
            .iter()
            .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
            .collect();

        assert!(
            inner_member_names.contains(&"Init".to_string()),
            "expected Init on InnerPanel, got {inner_member_names:?}"
        );
        assert!(
            !inner_member_names.contains(&"Paint".to_string()),
            "InnerPanel should not inherit outer PANEL:Paint, got {inner_member_names:?}"
        );

        let outer_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(outer_class_id.clone()))
            .expect("expected members on OuterPanel");
        let outer_member_names: Vec<_> = outer_members
            .iter()
            .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
            .collect();

        assert!(
            outer_member_names.contains(&"Paint".to_string()),
            "expected Paint on OuterPanel, got {outer_member_names:?}"
        );
        assert!(
            !outer_member_names.contains(&"Init".to_string()),
            "OuterPanel should not inherit inner PANEL:Init, got {outer_member_names:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_outside_closed_do_block_ignores_inner_panel_decl() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/panel_orphan_scope.lua",
            r#"
            do
                local PANEL = {}
                function PANEL:Init() end
            end

            vgui.Register("OrphanPanel", PANEL, "DFrame")
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("OrphanPanel");

        assert!(
            db.get_type_index().get_type_decl(&class_id).is_some(),
            "OrphanPanel class should be created"
        );

        let member_names: Vec<_> = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(class_id))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            !member_names.contains(&"Init".to_string()),
            "OrphanPanel should not inherit members from out-of-scope PANEL, got {member_names:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_panel_local_name_resolves_to_correct_panel_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/vgui/panel_hover_type.lua",
            r#"
            local PANEL = {}
            vgui.Register("HoverPanelOne", PANEL, "DPanel")

            local PANEL = {}
            vgui.Register("HoverPanelTwo", PANEL, "EditablePanel")
        "#,
        );

        let first_class_id = LuaTypeDeclId::global("HoverPanelOne");
        let second_class_id = LuaTypeDeclId::global("HoverPanelTwo");

        {
            let db = ws.get_db_mut();
            let decl_tree = db
                .get_decl_index()
                .get_decl_tree(&file_id)
                .expect("expected decl tree");
            let mut panel_decl_ids = decl_tree
                .get_decls()
                .values()
                .filter(|decl| decl.get_name() == "PANEL" && decl.is_local())
                .map(|decl| (decl.get_position(), decl.get_id()))
                .collect::<Vec<_>>();
            panel_decl_ids.sort_by_key(|(position, _)| *position);

            assert_eq!(
                panel_decl_ids.len(),
                2,
                "expected two local PANEL declarations, got {panel_decl_ids:?}"
            );

            let first_panel_type = db
                .get_type_index()
                .get_type_cache(&panel_decl_ids[0].1.into())
                .expect("expected first PANEL type cache")
                .as_type()
                .clone();
            assert_eq!(first_panel_type, LuaType::Def(first_class_id.clone()));

            let second_panel_type = db
                .get_type_index()
                .get_type_cache(&panel_decl_ids[1].1.into())
                .expect("expected second PANEL type cache")
                .as_type()
                .clone();
            assert_eq!(second_panel_type, LuaType::Def(second_class_id.clone()));
        }

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let mut panel_local_types = semantic_model
            .get_root()
            .descendants::<LuaLocalName>()
            .filter_map(|local_name| {
                let token = local_name.get_name_token()?;
                if token.get_name_text() != "PANEL" {
                    return None;
                }

                let semantic_info =
                    semantic_model.get_semantic_info(token.syntax().clone().into())?;
                Some((token.get_position(), semantic_info.typ))
            })
            .collect::<Vec<_>>();
        panel_local_types.sort_by_key(|(position, _)| *position);

        assert_eq!(
            panel_local_types.len(),
            2,
            "expected semantic info for two local PANEL names, got {panel_local_types:?}"
        );
        assert_eq!(panel_local_types[0].1, LuaType::Def(first_class_id));
        assert_eq!(panel_local_types[1].1, LuaType::Def(second_class_id));
    }

    /// Regression: PANEL reassignment (`PANEL = {}`) should create distinct classes
    /// per region. Today all PANEL references bind to a single class, so the set of
    /// resolved class names contains only one entry instead of three.
    #[gtest]
    fn test_vgui_reassigned_panel_name_expr_resolves_per_region() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/vgui/reassigned_panel_regions.lua",
            r#"
            local PANEL = {}
            function PANEL:Init() end
            vgui.Register("ReFrame", PANEL, "DFrame")

            PANEL = {}
            function PANEL:Paint() end
            vgui.Register("ReButton", PANEL, "DButton")

            PANEL = {}
            function PANEL:Layout() end
            derma.DefineControl("ReTree", "", PANEL, "DTree")
        "#,
        );

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let mut resolved_classes: Vec<_> = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .filter_map(|name_expr| {
                let token = name_expr.get_name_token()?;
                if token.get_name_text() != "PANEL" {
                    return None;
                }
                let info = semantic_model.get_semantic_info(token.syntax().clone().into())?;
                match &info.typ {
                    LuaType::Def(id) => {
                        Some((name_expr.get_position(), id.get_simple_name().to_string()))
                    }
                    _ => None,
                }
            })
            .collect();
        resolved_classes.sort_by_key(|(pos, _)| *pos);

        let class_names: std::collections::HashSet<&str> =
            resolved_classes.iter().map(|(_, n)| n.as_str()).collect();

        assert!(
            class_names.contains("ReFrame"),
            "expected PANEL references to include ReFrame, got {class_names:?}"
        );
        assert!(
            class_names.contains("ReButton"),
            "expected PANEL references to include ReButton, got {class_names:?}"
        );
        assert!(
            class_names.contains("ReTree"),
            "expected PANEL references to include ReTree, got {class_names:?}"
        );
    }

    /// Regression: the `local PANEL = {}` declaration should resolve to the FIRST
    /// region's class ("ReFrame"), not a later region. Today it resolves to the
    /// wrong (usually last) class.
    #[gtest]
    fn test_vgui_reassigned_panel_decl_token_first_region_class() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/vgui/reassigned_panel_decl_type.lua",
            r#"
            local PANEL = {}
            function PANEL:Init() end
            vgui.Register("ReFrame", PANEL, "DFrame")

            PANEL = {}
            function PANEL:Paint() end
            vgui.Register("ReButton", PANEL, "DButton")

            PANEL = {}
            function PANEL:Layout() end
            derma.DefineControl("ReTree", "", PANEL, "DTree")
        "#,
        );

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");

        let local_panel = semantic_model
            .get_root()
            .descendants::<LuaLocalName>()
            .find(|local_name| {
                local_name
                    .get_name_token()
                    .is_some_and(|t| t.get_name_text() == "PANEL")
            })
            .expect("expected one local PANEL declaration");

        let token = local_panel.get_name_token().expect("expected PANEL token");
        let semantic_info = semantic_model
            .get_semantic_info(token.syntax().clone().into())
            .expect("expected semantic info for local PANEL");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("ReFrame")),
            "local PANEL should resolve to first region class ReFrame"
        );
    }

    /// Regression: `self:ReloadTree()` inside the FIRST PANEL region should not
    /// produce a false undefined-field diagnostic. Under the bug, all PANEL
    /// references collapse to the LAST registered class ("BrowserPanel"), so
    /// `self` in the early region resolves to BrowserPanel which lacks
    /// ReloadTree → false positive. After the fix, each region resolves to its
    /// own class: self in TreePanel's region finds ReloadTree → no diagnostic.
    #[gtest]
    fn test_vgui_reassigned_panel_self_method_no_undefined_field() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = false;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        let file_id = ws.def_file(
            "lua/vgui/reassigned_panel_self_method.lua",
            r#"
            local PANEL = {}
            function PANEL:ReloadTree() end
            function PANEL:Refresh()
                self:ReloadTree()
            end
            vgui.Register("TreePanel", PANEL, "DTree")

            PANEL = {}
            function PANEL:Paint() end
            vgui.Register("BrowserPanel", PANEL, "DPanel")
        "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "self:ReloadTree() in TreePanel region should not trigger undefined-field, got {diagnostics:?}"
        );
    }

    /// Negative control: calling a method that exists on NO region should still
    /// produce an undefined-field diagnostic. Guards against an over-broad fix
    /// that suppresses all field checks on reassigned PANEL.
    #[gtest]
    fn test_vgui_reassigned_panel_wrong_method_still_flags_undefined_field() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = false;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        let file_id = ws.def_file(
            "lua/vgui/reassigned_panel_wrong_method.lua",
            r#"
            local PANEL = {}
            function PANEL:Init() end
            vgui.Register("AlphaPanel", PANEL, "DFrame")

            PANEL = {}
            function PANEL:Refresh()
                self:DoesNotExistAnywhere()
            end
            vgui.Register("BetaPanel", PANEL, "DPanel")
        "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .any(|diag| diag.code == undefined_field_code),
            "self:DoesNotExistAnywhere() should produce an undefined-field diagnostic, got {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_panel_self_dynamic_field_no_undefined_field_diagnostic() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        let file_id = ws.def_file(
            "lua/vgui/self_field_panel.lua",
            r#"
            local PANEL = {}

            function PANEL:Init()
                self.buttons = {}
                local _ = self.buttons
            end

            vgui.Register("SelfFieldPanel", PANEL, "EditablePanel")
        "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_panel_dynamic_field_method_call_no_undefined_field_diagnostic() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def(
            r#"
            ---@class Panel
            local Panel = {}

            ---@param name string
            ---@return Panel
            function Panel:Add(name)
            end

            ---@param dir number
            function Panel:Dock(dir)
            end

            ---@class EditablePanel : Panel
            "#,
        );

        let file_id = ws.def_file(
            "lua/vgui/test_dynamic_field_panel.lua",
            r#"
            local TOP = 1
            local PANEL = {}

            function PANEL:Init()
                self.buttons = self:Add("Panel")
                self.buttons:Dock(TOP)
            end

            vgui.Register("TestDynamicFieldPanel", PANEL, "EditablePanel")
        "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_panel_cross_method_dynamic_field_no_undefined_field_diagnostic() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def(
            r#"
            ---@class EditablePanel
            "#,
        );

        let file_id = ws.def_file(
            "lua/vgui/cross_method_panel.lua",
            r#"
            local PANEL = {}

            function PANEL:Init()
                self.header = {}
            end

            function PANEL:PerformLayout()
                local _ = self.header
            end

            vgui.Register("CrossMethodPanel", PANEL, "EditablePanel")
        "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_panel_baseclass_calls_no_false_positive_diagnostics() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);
        ws.enable_check(DiagnosticCode::NeedCheckNil);
        ws.enable_check(DiagnosticCode::MissingParameter);

        ws.def(
            r#"
            ---@class DProgress
            local DProgress = {}

            function DProgress:Init()
            end

            ---@param width number
            ---@param height number
            function DProgress:PerformLayout(width, height)
            end
            "#,
        );

        let file_id = ws.def_file(
            "lua/vgui/baseclass_panel.lua",
            r#"
            local PROGRESS = {}

            function PROGRESS:Init()
                self.BaseClass.Init(self)
            end

            function PROGRESS:PerformLayout(width, height)
                self.BaseClass.PerformLayout(self, width, height)
            end

            vgui.Register("BaseClassProgress", PROGRESS, "DProgress")
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let false_positive_codes = [
            DiagnosticCode::UndefinedField,
            DiagnosticCode::NeedCheckNil,
            DiagnosticCode::MissingParameter,
        ]
        .map(|code| Some(NumberOrString::String(code.get_name().to_string())));

        assert!(
            diagnostics
                .iter()
                .all(|diag| !false_positive_codes.contains(&diag.code)),
            "unexpected BaseClass diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_vgui_register_not_captured_when_gmod_disabled() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = false;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/disabled_panel.lua",
            r#"
            local PANEL = {}
            vgui.Register("DisabledPanel", PANEL, "DPanel")
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("DisabledPanel");
        let decl = db.get_type_index().get_type_decl(&class_id);
        assert!(
            decl.is_none(),
            "Panel class should not be created when gmod disabled"
        );
    }

    #[gtest]
    fn test_network_var_two_arg_form_synthesizes_get_set() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/two_arg_nw/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:NetworkVar("String", "Label")
                self:NetworkVar("Bool", "Active")
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("two_arg_nw");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on two_arg_nw class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetLabel".to_string()),
            "missing GetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetLabel".to_string()),
            "missing SetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetActive".to_string()),
            "missing GetActive in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetActive".to_string()),
            "missing SetActive in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_element_collected_and_synthesized() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/entities/nve_entity/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:NetworkVarElement("Float", 0, "x", "VehiclePos")
                self:NetworkVarElement("Float", 1, "y", "VehicleAng")
            end
        "#,
        );

        // Verify collection
        let metadata = ws
            .get_db_mut()
            .get_gmod_class_metadata_index()
            .get_file_metadata(&file_id)
            .cloned()
            .expect("expected scripted class metadata");
        assert_eq!(
            metadata.network_var_element_calls.len(),
            2,
            "expected 2 NetworkVarElement calls, got {}",
            metadata.network_var_element_calls.len()
        );

        // Verify synthesis
        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("nve_entity");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on nve_entity class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetVehiclePos".to_string()),
            "missing GetVehiclePos in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetVehiclePos".to_string()),
            "missing SetVehiclePos in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetVehicleAng".to_string()),
            "missing GetVehicleAng in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetVehicleAng".to_string()),
            "missing SetVehicleAng in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_element_three_arg_form() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/nve_short/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:NetworkVarElement("Float", 0, "Offset")
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("nve_short");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on nve_short class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetOffset".to_string()),
            "missing GetOffset in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetOffset".to_string()),
            "missing SetOffset in {member_names:?}"
        );
    }

    #[gtest]
    fn test_inherited_network_vars_accessible_on_derived_entity() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/entities/base_glide_car/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Vector", 0, "HeadlightColor")
                    self:NetworkVar("Float", 0, "SteerConeChangeRate")
                end
            "#,
            ),
            (
                "lua/entities/fl_dodge_charger/shared.lua",
                r#"
                ENT.Base = "base_glide_car"
            "#,
            ),
        ]);

        // Verify base entity has the NetworkVar members
        {
            let db = ws.get_db_mut();
            let base_class_id = LuaTypeDeclId::global("base_glide_car");
            let base_owner = LuaMemberOwner::Type(base_class_id);
            let base_members = db
                .get_member_index()
                .get_members(&base_owner)
                .expect("expected members on base_glide_car");
            let base_member_names: Vec<_> = base_members
                .iter()
                .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                .collect();
            assert!(
                base_member_names.contains(&"GetHeadlightColor".to_string()),
                "missing GetHeadlightColor on base: {base_member_names:?}"
            );
            assert!(
                base_member_names.contains(&"SetSteerConeChangeRate".to_string()),
                "missing SetSteerConeChangeRate on base: {base_member_names:?}"
            );
        }

        // Verify derived entity inherits from base
        {
            let db = ws.get_db_mut();
            let derived_class_id = LuaTypeDeclId::global("fl_dodge_charger");
            let super_types: Vec<_> = db
                .get_type_index()
                .get_super_types_iter(&derived_class_id)
                .map(|iter| iter.cloned().collect())
                .unwrap_or_default();
            assert!(
                super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("base_glide_car"))),
                "expected base_glide_car super type on derived, \
                 got {super_types:?}"
            );
        }
    }

    #[gtest]
    fn test_network_var_mixed_forms_in_same_entity() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/mixed_nw/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:NetworkVar("Float", 0, "Speed")
                self:NetworkVar("String", "Label")
                self:NetworkVarElement("Float", 0, "x", "Position")
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("mixed_nw");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on mixed_nw class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        // 3-arg NetworkVar
        assert!(
            member_names.contains(&"GetSpeed".to_string()),
            "missing GetSpeed in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetSpeed".to_string()),
            "missing SetSpeed in {member_names:?}"
        );
        // 2-arg NetworkVar
        assert!(
            member_names.contains(&"GetLabel".to_string()),
            "missing GetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetLabel".to_string()),
            "missing SetLabel in {member_names:?}"
        );
        // NetworkVarElement
        assert!(
            member_names.contains(&"GetPosition".to_string()),
            "missing GetPosition in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetPosition".to_string()),
            "missing SetPosition in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_element_metadata_collection() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/entities/nve_meta_test.lua",
            r#"
            self:NetworkVarElement("Float", 0, "x", "Position")
        "#,
        );

        let metadata = ws
            .get_db_mut()
            .get_gmod_class_metadata_index()
            .get_file_metadata(&file_id)
            .cloned()
            .expect("expected metadata for NetworkVarElement");

        assert_eq!(metadata.network_var_element_calls.len(), 1);
        assert_eq!(
            metadata.network_var_element_calls[0].literal_args,
            vec![
                Some(GmodClassCallLiteral::String("Float".to_string())),
                Some(GmodClassCallLiteral::Integer(0)),
                Some(GmodClassCallLiteral::String("x".to_string())),
                Some(GmodClassCallLiteral::String("Position".to_string())),
            ]
        );
    }

    #[gtest]
    fn test_network_var_wrapper_function_synthesizes_get_set() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/wrapper_ent/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:RegisterNW("String", "Label")
                self:RegisterNW("Float", "Health")
            end

            function ENT:RegisterNW(type, name)
                self:NetworkVar(type, 0, name)
            end
        "#,
        );

        let class_id = LuaTypeDeclId::global("wrapper_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let member_names: Vec<String> = ws
            .get_db_mut()
            .get_member_index()
            .get_members(&owner)
            .map(|members| {
                members
                    .iter()
                    .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        assert!(
            member_names.contains(&"GetLabel".to_string()),
            "missing GetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetLabel".to_string()),
            "missing SetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetHealth".to_string()),
            "missing GetHealth in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetHealth".to_string()),
            "missing SetHealth in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_wrapper_with_fixed_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/wrapper_fixed/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:AddBoolNW("Active")
                self:AddBoolNW("Visible")
            end

            function ENT:AddBoolNW(name)
                self:NetworkVar("Bool", 0, name)
            end
        "#,
        );

        let class_id = LuaTypeDeclId::global("wrapper_fixed");
        let owner = LuaMemberOwner::Type(class_id);
        let member_names: Vec<String> = ws
            .get_db_mut()
            .get_member_index()
            .get_members(&owner)
            .map(|members| {
                members
                    .iter()
                    .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        assert!(
            member_names.contains(&"GetActive".to_string()),
            "missing GetActive in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetActive".to_string()),
            "missing SetActive in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetVisible".to_string()),
            "missing GetVisible in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetVisible".to_string()),
            "missing SetVisible in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_element_wrapper_synthesizes_number_type() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/elem_wrap/init.lua",
            r#"
            function ENT:SetupDataTables()
                self:AddPosElement("PosX")
            end

            function ENT:AddPosElement(name)
                self:NetworkVarElement("Float", 0, "x", name)
            end
        "#,
        );

        let class_id = LuaTypeDeclId::global("elem_wrap");
        let owner = LuaMemberOwner::Type(class_id);
        let member_names: Vec<String> = ws
            .get_db_mut()
            .get_member_index()
            .get_members(&owner)
            .map(|members| {
                members
                    .iter()
                    .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        assert!(
            member_names.contains(&"GetPosX".to_string()),
            "missing GetPosX in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetPosX".to_string()),
            "missing SetPosX in {member_names:?}"
        );
    }

    #[gtest]
    fn test_network_var_no_undefined_field_across_entity_files() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/base_glide_car/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Bool", "IsRedlining")
                    self:NetworkVar("Float", 0, "Speed")
                end
            "#,
            ),
            (
                "lua/entities/base_glide_car/cl_init.lua",
                r#"
                function ENT:Think()
                    local x = self:GetIsRedlining()
                    local y = self:GetSpeed()
                end
            "#,
            ),
        ]);

        let target_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/base_glide_car/cl_init.lua");
        let target_file_id = ws
            .analysis
            .get_file_id(&target_uri)
            .expect("expected file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(target_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_self_resolves_to_scoped_class_in_secondary_entity_file() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/entities/multi_file_ent/shared.lua",
                r#"
                ENT.Type = "anim"
            "#,
            ),
            (
                "lua/entities/multi_file_ent/cl_init.lua",
                r#"
                function ENT:Draw()
                    local self_copy = self
                end
            "#,
            ),
        ]);

        let target_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/multi_file_ent/cl_init.lua");
        let file_id = ws
            .analysis
            .get_file_id(&target_uri)
            .expect("expected file id");

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let self_name_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|name_expr| {
                name_expr
                    .get_name_token()
                    .is_some_and(|token| token.get_name_text() == "self")
            })
            .expect("expected self name expr");
        let self_token = self_name_expr
            .get_name_token()
            .expect("expected self token");
        let semantic_info = semantic_model
            .get_semantic_info(self_token.syntax().clone().into())
            .expect("expected semantic info for self");

        assert_eq!(
            semantic_info.typ,
            LuaType::Def(LuaTypeDeclId::global("multi_file_ent"))
        );
    }

    #[gtest]
    fn test_inherited_network_var_no_undefined_field_diagnostic() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/base_vehicle/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Vector", 0, "HeadlightColor")
                    self:NetworkVar("Float", 0, "SteerRate")
                end
            "#,
            ),
            (
                "lua/entities/derived_vehicle/shared.lua",
                r#"
                ENT.Base = "base_vehicle"
            "#,
            ),
            (
                "lua/entities/derived_vehicle/cl_init.lua",
                r#"
                function ENT:Think()
                    self:SetHeadlightColor(Vector(0, 0, 0))
                    local rate = self:GetSteerRate()
                end
            "#,
            ),
        ]);

        let target_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/derived_vehicle/cl_init.lua");
        let target_file_id = ws
            .analysis
            .get_file_id(&target_uri)
            .expect("expected file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(target_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_local_function_wrapper_inside_setup_data_tables() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/local_wrap_ent/init.lua",
            r#"
            function ENT:SetupDataTables()
                local function AddFloatVar(key, min, max)
                    self:NetworkVar("Float", key)
                end

                AddFloatVar("Speed", 0, 100)
                AddFloatVar("Health", 0, 500)
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("local_wrap_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetSpeed".to_string()),
            "missing GetSpeed in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetSpeed".to_string()),
            "missing SetSpeed in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetHealth".to_string()),
            "missing GetHealth in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetHealth".to_string()),
            "missing SetHealth in {member_names:?}"
        );
    }

    #[gtest]
    fn test_two_arg_network_var_no_undefined_field_diagnostic() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/two_arg_diag/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Bool", "IsRedlining")
                    self:NetworkVar("String", "Label")
                end
            "#,
            ),
            (
                "lua/entities/two_arg_diag/cl_init.lua",
                r#"
                function ENT:Think()
                    local x = self:GetIsRedlining()
                    local y = self:GetLabel()
                end
            "#,
            ),
        ]);

        let target_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/two_arg_diag/cl_init.lua");
        let target_file_id = ws
            .analysis
            .get_file_id(&target_uri)
            .expect("expected file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(target_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_realistic_mixed_networkvar_and_wrapper_calls_in_same_entity() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/base_glide_car_realistic/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Bool", "IsRedlining")
                    self:NetworkVar("Int", "Gear")
                    self:NetworkVar("Float", "Steering")

                    self:NetworkVar("Vector", "TireSmokeColor",
                        { KeyName = "TireSmokeColor" })
                    self:NetworkVar("Vector", "HeadlightColor",
                        { KeyName = "HeadlightColor" })

                    local order = 0
                    local function AddFloatVar(key, min, max, category)
                        order = order + 1
                        self:NetworkVar("Float", key)
                    end

                    local function AddIntVar(key, min, max, category)
                        order = order + 1
                        self:NetworkVar("Int", key)
                    end

                    local function AddBoolVar(key, category)
                        order = order + 1
                        self:NetworkVar("Bool", key)
                    end

                    AddFloatVar("MaxSteerAngle", 10, 80, "steering")
                    AddFloatVar("SteerConeChangeRate", 0.1, 10, "steering")
                    AddBoolVar("HasHeadlights", "lights")
                    AddIntVar("MaxHealth", 100, 10000, "health")
                end
            "#,
            ),
            (
                "lua/entities/base_glide_car_realistic/cl_init.lua",
                r#"
                function ENT:OnUpdateSounds()
                    local redlining = self:GetIsRedlining()
                    local gear = self:GetGear()
                    local steer = self:GetSteering()
                end
            "#,
            ),
        ]);

        {
            let db = ws.get_db_mut();
            let class_id = LuaTypeDeclId::global("base_glide_car_realistic");
            let owner = LuaMemberOwner::Type(class_id);
            let members = db
                .get_member_index()
                .get_members(&owner)
                .expect("expected members on base_glide_car_realistic");
            let member_names: Vec<_> = members
                .iter()
                .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                .collect();

            assert!(
                member_names.contains(&"GetIsRedlining".to_string()),
                "missing GetIsRedlining in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetIsRedlining".to_string()),
                "missing SetIsRedlining in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetGear".to_string()),
                "missing GetGear in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetGear".to_string()),
                "missing SetGear in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetSteering".to_string()),
                "missing GetSteering in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetSteering".to_string()),
                "missing SetSteering in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetTireSmokeColor".to_string()),
                "missing GetTireSmokeColor in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetTireSmokeColor".to_string()),
                "missing SetTireSmokeColor in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetHeadlightColor".to_string()),
                "missing GetHeadlightColor in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetHeadlightColor".to_string()),
                "missing SetHeadlightColor in {member_names:?}"
            );
        }

        let cl_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/base_glide_car_realistic/cl_init.lua");
        let cl_file_id = ws
            .analysis
            .get_file_id(&cl_uri)
            .expect("expected cl_init file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(cl_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics on cl_init.lua accessing direct NetworkVar getters: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_network_var_sequential_file_analysis() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        // First batch: shared.lua analyzed alone
        ws.def_file(
            "lua/entities/seq_entity/shared.lua",
            r#"
        function ENT:SetupDataTables()
            self:NetworkVar("Bool", "IsRedlining")
            self:NetworkVar("Float", 0, "Speed")
        end
    "#,
        );

        // Second batch: cl_init.lua analyzed separately (incremental)
        let cl_file_id = ws.def_file(
            "lua/entities/seq_entity/cl_init.lua",
            r#"
        function ENT:Think()
            local x = self:GetIsRedlining()
            local y = self:GetSpeed()
        end
    "#,
        );

        // Check: cl_init.lua should have no UndefinedField diagnostics
        let diagnostics = ws
            .analysis
            .diagnose_file(cl_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));

        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics when files analyzed sequentially: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_network_var_with_std_lib_initialized() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/stdlib_ent/shared.lua",
                r#"
            function ENT:SetupDataTables()
                self:NetworkVar("Bool", "IsRedlining")
                self:NetworkVar("Float", 0, "Speed")
            end
        "#,
            ),
            (
                "lua/entities/stdlib_ent/cl_init.lua",
                r#"
            function ENT:Think()
                local x = self:GetIsRedlining()
                local y = self:GetSpeed()
            end
        "#,
            ),
        ]);

        let cl_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/stdlib_ent/cl_init.lua");
        let cl_file_id = ws
            .analysis
            .get_file_id(&cl_uri)
            .expect("expected cl_init file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(cl_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));

        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics with std lib loaded: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_network_var_same_function_usage_with_real_config() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = legacy_scopes(&[
            "entities/**",
            "weapons/**",
            "effects/**",
            "weapons/gmod_tool/stools/**",
            "plugins/**",
        ]);
        emmyrc.gmod.scripted_class_scopes.legacy_exclude = vec![
            "**/tests/**".to_string(),
            "**/test/**".to_string(),
            "**/docs/**".to_string(),
        ];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        // This exactly mirrors the real base_glide/shared.lua:
        // - ENT.Type and ENT.Base at top level
        // - Many ENT field assignments
        // - SetupDataTables with NetworkVar calls followed by setter usage
        // - CLIENT/SERVER conditional blocks
        // - NetworkVarNotify calls
        let file_id = ws.def_file(
            "addons/cityrp-vehicle-base/lua/entities/base_glide/shared.lua",
            r##"
        ENT.Type = "anim"
        ENT.Base = "base_anim"

        ENT.PrintName = "Glide Base Vehicle"
        ENT.IsGlideVehicle = true
        ENT.MaxChassisHealth = 1000
        ENT.CanSwitchHeadlights = false

        function ENT:SetupDataTables()
            self:NetworkVar("Entity", "Driver")
            self:NetworkVar("Int", "EngineState")
            self:NetworkVar("Bool", "IsEngineOnFire")
            self:NetworkVar("Bool", "IsLocked")
            self:NetworkVar("Int", "LockOnState")
            self:NetworkVar("Entity", "LockOnTarget")
            self:NetworkVar("Float", "ChassisHealth")
            self:NetworkVar("Float", "EngineHealth")
            self:NetworkVar("Float", "BrakeValue")
            self:NetworkVar("Bool", "HandbrakeActive")
            self:NetworkVar("Int", "HeadlightState")
            self:NetworkVar("Int", "TurnSignalState")
            self:NetworkVar("Int", "ConnectedReceptacleCount")
            self:NetworkVar("Bool", "ReducedThrottle")
            self:NetworkVar("Int", "WaterState")

            if CLIENT then
                self:NetworkVarNotify("WaterState", self.OnWaterStateChange)
            end

            local editData = nil
            if self.CanSwitchHeadlights then
                editData = {
                    KeyName = "HeadlightColor",
                    Edit = { type = "VectorColor", order = 0, category = "#glide.settings" },
                }
            end
            self:NetworkVar("Vector", "HeadlightColor", editData)

            self:SetDriver(NULL)
            self:SetEngineState(0)
            self:SetIsEngineOnFire(false)
            self:SetLockOnState(0)
            self:SetLockOnTarget(NULL)
            self:SetBrakeValue(0)
            self:SetHandbrakeActive(false)
            self:SetHeadlightState(0)
            self:SetTurnSignalState(0)

            local maxChassis = self.MaxChassisHealth or 1000
            if maxChassis < 1 then maxChassis = 1 end
            self:SetChassisHealth(maxChassis)
            self:SetEngineHealth(1.0)

            self:NetworkVarNotify("EngineState", self.OnEngineStateChange)

            if SERVER then
                self:NetworkVarNotify("EngineState", self.OnEngineStateChangePhoton)
                self:NetworkVarNotify("HeadlightState", self.OnHeadlightStateChangePhoton)
                self:NetworkVarNotify("TurnSignalState", self.OnTurnSignalStateChangePhoton)
            end
        end

        function ENT:IsEngineOn(selfTbl)
            selfTbl = selfTbl or self:GetTable()
            local cached = selfTbl._cachedEngineState
            if cached ~= nil then
                return cached > 1
            end
            return self:GetEngineState() > 1
        end

        function ENT:IsBraking(selfTbl)
            selfTbl = selfTbl or self:GetTable()
            local cached = selfTbl._cachedBrakeValue
            if cached ~= nil then
                return cached > 0.1
            end
            return self:GetBrakeValue() > 0.1
        end

        function ENT:OnPostInitialize() end
        function ENT:OnTurnOn() end
        function ENT:OnTurnOff() end

        if CLIENT then
            ENT.Spawnable = false
            ENT.CameraOffset = Vector(-200, 0, 50)

            function ENT:OnActivateSounds() end
            function ENT:OnDeactivateSounds() end
            function ENT:OnUpdateSounds() end
        end

        if SERVER then
            ENT.Spawnable = true
            ENT.ChassisMass = 700

            function ENT:CreateFeatures() end
            function ENT:OnDriverEnter() end
            function ENT:OnDriverExit() end
        end
    "##,
        );

        // First check: members should exist on base_glide class
        let member_names: Vec<String> = {
            let db = ws.get_db_mut();
            let class_id = LuaTypeDeclId::global("base_glide");
            let owner = LuaMemberOwner::Type(class_id);
            let members = db
                .get_member_index()
                .get_members(&owner)
                .expect("expected members on base_glide class");
            let member_names: Vec<String> = members
                .iter()
                .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                .collect();

            assert!(
                member_names.contains(&"GetDriver".to_string()),
                "missing GetDriver in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetDriver".to_string()),
                "missing SetDriver in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetEngineState".to_string()),
                "missing GetEngineState in {member_names:?}"
            );
            assert!(
                member_names.contains(&"SetEngineState".to_string()),
                "missing SetEngineState in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetIsEngineOnFire".to_string()),
                "missing GetIsEngineOnFire in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetHeadlightColor".to_string()),
                "missing GetHeadlightColor in {member_names:?}"
            );

            member_names
        };

        // Second check: no UndefinedField diagnostics within the same file
        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let param_type_mismatch_code = Some(NumberOrString::String(
            DiagnosticCode::ParamTypeMismatch.get_name().to_string(),
        ));
        let missing_parameter_code = Some(NumberOrString::String(
            DiagnosticCode::MissingParameter.get_name().to_string(),
        ));
        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));

        let param_diagnostics: Vec<_> = diagnostics
            .iter()
            .filter(|diag| {
                diag.code == param_type_mismatch_code || diag.code == missing_parameter_code
            })
            .collect();

        assert!(
            param_diagnostics.is_empty(),
            "unexpected param diagnostics for synthesized NetworkVar accessors: {param_diagnostics:?}"
        );

        let undefined_field_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == undefined_field_code)
            .collect();

        let extract_undefined_field_name = |message: &str| {
            let prefix = "Undefined field `";
            message
                .strip_prefix(prefix)
                .and_then(|rest| rest.split_once('`'))
                .map(|(name, _)| name.to_string())
        };

        let mut undefined_field_names: Vec<String> = undefined_field_diags
            .iter()
            .filter_map(|diag| extract_undefined_field_name(&diag.message))
            .collect();
        undefined_field_names.sort();

        let allowed_undefined_field_names = [
            "NetworkVar",
            "NetworkVarNotify",
            "OnWaterStateChange",
            "OnEngineStateChange",
            "OnEngineStateChangePhoton",
            "OnHeadlightStateChangePhoton",
            "OnTurnSignalStateChangePhoton",
            "GetTable",
        ];

        assert!(
            undefined_field_names
                .iter()
                .all(|name| allowed_undefined_field_names.contains(&name.as_str())),
            "unexpected undefined-field diagnostics in same file: {undefined_field_diags:?}; undefined_field_names={undefined_field_names:?}; member_names={member_names:?}"
        );

        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "NetworkVar")
                .count(),
            16,
            "expected 16 undefined NetworkVar members"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "NetworkVarNotify")
                .count(),
            5,
            "expected 5 undefined NetworkVarNotify members"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "GetTable")
                .count(),
            2,
            "expected 2 undefined GetTable members"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "OnWaterStateChange")
                .count(),
            1,
            "expected one undefined callback member for OnWaterStateChange"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "OnEngineStateChange")
                .count(),
            1,
            "expected one undefined callback member for OnEngineStateChange"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "OnEngineStateChangePhoton")
                .count(),
            1,
            "expected one undefined callback member for OnEngineStateChangePhoton"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "OnHeadlightStateChangePhoton")
                .count(),
            1,
            "expected one undefined callback member for OnHeadlightStateChangePhoton"
        );
        assert_eq!(
            undefined_field_names
                .iter()
                .filter(|name| name.as_str() == "OnTurnSignalStateChangePhoton")
                .count(),
            1,
            "expected one undefined callback member for OnTurnSignalStateChangePhoton"
        );
    }

    #[gtest]
    fn test_network_var_with_entity_type_definitions() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/includes/entity_defs.lua",
                r#"
            ---@class Entity
            ---@field NetworkVar fun(self: Entity, type: string, name: string)
            ---@field NetworkVarNotify fun(self: Entity, name: string, callback: function)
            ---@field GetTable fun(self: Entity): table
            local Entity = {}

            ---@class ENT : Entity
            local ENT = {}
        "#,
            ),
            (
                "lua/entities/base_glide/shared.lua",
                r#"
            ENT.Type = "anim"
            ENT.Base = "base_anim"
            ENT.PrintName = "Glide Base Vehicle"
            ENT.MaxChassisHealth = 1000

            function ENT:SetupDataTables()
                self:NetworkVar("Entity", "Driver")
                self:NetworkVar("Int", "EngineState")
                self:NetworkVar("Bool", "IsEngineOnFire")
                self:NetworkVar("Bool", "IsLocked")
                self:NetworkVar("Float", "ChassisHealth")
                self:NetworkVar("Float", "EngineHealth")
                self:NetworkVar("Float", "BrakeValue")
                self:NetworkVar("Bool", "HandbrakeActive")
                self:NetworkVar("Int", "HeadlightState")

                if CLIENT then
                    self:NetworkVarNotify("HeadlightState", self.OnHeadlightStateChange)
                end

                self:SetDriver(NULL)
                self:SetEngineState(0)
                self:SetIsEngineOnFire(false)
                self:SetBrakeValue(0)
                self:SetHandbrakeActive(false)
                self:SetHeadlightState(0)

                local maxChassis = self.MaxChassisHealth or 1000
                self:SetChassisHealth(maxChassis)
                self:SetEngineHealth(1.0)

                self:NetworkVarNotify("EngineState", self.OnEngineStateChange)
            end

            function ENT:IsEngineOn()
                return self:GetEngineState() > 1
            end

            function ENT:IsBraking()
                return self:GetBrakeValue() > 0.1
            end
        "#,
            ),
        ]);

        let member_names: Vec<String> = {
            let db = ws.get_db_mut();
            let class_id = LuaTypeDeclId::global("base_glide");
            let owner = LuaMemberOwner::Type(class_id);
            let members = db
                .get_member_index()
                .get_members(&owner)
                .expect("expected members on base_glide");
            let member_names: Vec<_> = members
                .iter()
                .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                .collect();

            assert!(
                member_names.contains(&"SetDriver".to_string()),
                "missing SetDriver in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetDriver".to_string()),
                "missing GetDriver in {member_names:?}"
            );
            assert!(
                member_names.contains(&"GetEngineState".to_string()),
                "missing GetEngineState in {member_names:?}"
            );

            member_names
        };

        let shared_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/base_glide/shared.lua");
        let shared_file_id = ws
            .analysis
            .get_file_id(&shared_uri)
            .expect("expected shared file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(shared_file_id, CancellationToken::new())
            .unwrap_or_default();

        let param_type_mismatch_code = Some(NumberOrString::String(
            DiagnosticCode::ParamTypeMismatch.get_name().to_string(),
        ));
        let missing_parameter_code = Some(NumberOrString::String(
            DiagnosticCode::MissingParameter.get_name().to_string(),
        ));
        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));

        let param_diagnostics: Vec<_> = diagnostics
            .iter()
            .filter(|diag| {
                diag.code == param_type_mismatch_code || diag.code == missing_parameter_code
            })
            .collect();

        assert!(
            param_diagnostics.is_empty(),
            "unexpected param diagnostics for synthesized NetworkVar accessors: {param_diagnostics:?}"
        );

        let undefined_field_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == undefined_field_code)
            .collect();

        let extract_undefined_field_name = |message: &str| {
            let prefix = "Undefined field `";
            message
                .strip_prefix(prefix)
                .and_then(|rest| rest.split_once('`'))
                .map(|(name, _)| name.to_string())
        };

        let mut undefined_field_names: Vec<String> = undefined_field_diags
            .iter()
            .filter_map(|diag| extract_undefined_field_name(&diag.message))
            .collect();
        undefined_field_names.sort();

        let mut expected_undefined_field_names = vec![
            "OnEngineStateChange".to_string(),
            "OnHeadlightStateChange".to_string(),
        ];
        expected_undefined_field_names.sort();

        assert!(
            undefined_field_names == expected_undefined_field_names,
            "unexpected undefined-field diagnostics with Entity type defined: {undefined_field_diags:?}; undefined_field_names={undefined_field_names:?}; member_names={member_names:?}"
        );
    }

    #[gtest]
    fn test_stool_methods_resolve_from_tool_super_type() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_file(
            "lua/includes/custom_classes.lua",
            r#"
                ---@class Tool
                ---@class TOOL : Tool
            "#,
        );

        ws.def_file(
            "lua/includes/tool_meta.lua",
            r#"
                ---@class Player
                ---@param command string
                function Player:ConCommand(command) end

                ---@return Player
                function Tool:GetOwner() end

                ---@param name string
                ---@return number
                function Tool:GetClientNumber(name) end

                ---@return table
                function Tool:BuildConVarList() end

                ---@class DNumSlider
                ---@param value number
                function DNumSlider:SetValue(value) end

                ---@class ControlPanel
                ---@param text string
                function ControlPanel:Help(text) end

                ---@param controlType string
                ---@param info table
                function ControlPanel:AddControl(controlType, info) end

                ---@return DNumSlider
                function ControlPanel:NumSlider(...) end

                ---@param panel ControlPanel
                function TOOL.BuildCPanel(panel) end
            "#,
        );

        let file_id = ws.def_file(
            "lua/weapons/gmod_tool/stools/my_tool.lua",
            r#"
                TOOL.Category = "Test"
                TOOL.Name = "My Tool"

                function TOOL:LeftClick(trace)
                    local owner = self:GetOwner()
                    local speed = self:GetClientNumber("my_speed")
                    local defaults = self:BuildConVarList()
                    owner:ConCommand("say test")
                    return IsValid(owner) and speed >= 0 and defaults ~= nil
                end

                function TOOL.BuildCPanel(panel)
                    panel:Help("help")
                    panel:AddControl("slider", {})
                    local slider = panel:NumSlider("Label", "tool_speed", 0, 10, 0)
                    slider:SetValue(5)
                end
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        let undefined_field_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == undefined_field_code)
            .collect();

        let extract_name = |message: &str| {
            message
                .strip_prefix("Undefined field `")
                .and_then(|rest| rest.split_once('`'))
                .map(|(name, _)| name.to_string())
        };

        let undefined_field_names: Vec<String> = undefined_field_diags
            .iter()
            .filter_map(|diag| extract_name(&diag.message))
            .collect();

        assert!(
            !undefined_field_names.iter().any(|name| {
                matches!(
                    name.as_str(),
                    "GetOwner"
                        | "GetClientNumber"
                        | "BuildConVarList"
                        | "ConCommand"
                        | "Help"
                        | "AddControl"
                        | "NumSlider"
                        | "SetValue"
                )
            }),
            "unexpected undefined-field diagnostics for TOOL base methods: {undefined_field_diags:?}"
        );
    }

    #[gtest]
    fn test_stool_and_entity_same_name_do_not_share_type_identity() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/includes/custom_classes.lua",
            r#"
                ---@class Entity

                ---@class ENT : Entity
                ENT = {}

                ---@class Tool

                ---@class TOOL : Tool
                TOOL = {}
            "#,
        );
        ws.def_file(
            "lua/entities/shared_name.lua",
            r#"
                ENT.Base = "base_anim"
            "#,
        );
        ws.def_file(
            "lua/weapons/gmod_tool/stools/shared_name.lua",
            r#"
                TOOL.Name = "Shared Name"
            "#,
        );

        let db = ws.get_db_mut();
        let entity_class_id = LuaTypeDeclId::global("shared_name");
        let stool_class_id = LuaTypeDeclId::global("TOOL.shared_name");

        assert!(
            db.get_type_index()
                .get_type_decl(&entity_class_id)
                .is_some(),
            "expected the entity class to keep the runtime class name"
        );
        assert!(
            db.get_type_index().get_type_decl(&stool_class_id).is_some(),
            "expected the STool class to use a scoped type id"
        );

        let entity_supers = db
            .get_type_index()
            .get_super_types(&entity_class_id)
            .unwrap_or_default();
        assert!(
            entity_supers
                .iter()
                .any(|ty| matches!(ty, LuaType::Ref(id) if id == &LuaTypeDeclId::global("Entity"))),
            "entity class should inherit Entity, got {entity_supers:?}"
        );
        assert!(
            !entity_supers.iter().any(|ty| {
                matches!(ty, LuaType::Ref(id) if id == &LuaTypeDeclId::global("TOOL") || id == &LuaTypeDeclId::global("Tool"))
            }),
            "entity class must not inherit TOOL/Tool from the same-named STool, got {entity_supers:?}"
        );

        let stool_supers = db
            .get_type_index()
            .get_super_types(&stool_class_id)
            .unwrap_or_default();
        assert!(
            stool_supers
                .iter()
                .any(|ty| matches!(ty, LuaType::Ref(id) if id == &LuaTypeDeclId::global("TOOL"))),
            "STool class should inherit TOOL, got {stool_supers:?}"
        );
    }

    #[gtest]
    fn test_stool_buildcpanel_from_field_annotation_only() {
        // Reproduces the real scenario with exact annotation structure from glua-api-snippets.
        // @field BuildCPanel fun(panel: ControlPanel) is on Tool class (not TOOL).
        // ControlPanel : DForm, and DForm:Help is inherited.
        // Methods are split across separate files.
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        // class.TOOL.lua - exact real annotation structure
        ws.def_file(
            "lua/annotations/class.TOOL.lua",
            r#"
                ---@class Tool
                ---@field BuildCPanel fun(panel: ControlPanel) Called to populate the tool's control panel.
                Tool = Tool or {}

                ---@class TOOL : Tool
                TOOL = {}
            "#,
        );

        // tool.lua - Tool methods
        ws.def_file(
            "lua/annotations/tool.lua",
            r#"
                ---@return Player
                function Tool:GetOwner() end
            "#,
        );

        // player.lua
        ws.def_file(
            "lua/annotations/player.lua",
            r#"
                ---@class Player
                ---@param command string
                function Player:ConCommand(command) end
            "#,
        );

        // class.ControlPanel.lua (separate from controlpanel.lua methods)
        ws.def_file(
            "lua/annotations/class.ControlPanel.lua",
            r#"
                ---@class ControlPanel : DForm
                local ControlPanel = {}
            "#,
        );

        // controlpanel.lua (methods, separate from class declaration)
        ws.def_file(
            "lua/annotations/controlpanel.lua",
            r#"
                ---@param type string
                ---@param controlinfo table
                ---@return Panel
                function ControlPanel:AddControl(type, controlinfo) end

                ---@return DNumSlider
                function ControlPanel:NumSlider(...) end
            "#,
        );

        // dform.lua (DForm methods, inherited by ControlPanel)
        ws.def_file(
            "lua/annotations/dform.lua",
            r#"
                ---@class DForm : DCollapsibleCategory
                local DForm = {}

                ---@param help string
                ---@return DLabel
                function DForm:Help(help) end

                ---@param label string
                ---@param convar string
                ---@param min number
                ---@param max number
                ---@param decimals? number
                ---@return DNumSlider
                function DForm:NumSlider(label, convar, min, max, decimals) end
            "#,
        );

        // dcollapsiblecategory.lua
        ws.def_file(
            "lua/annotations/dcollapsiblecategory.lua",
            r#"
                ---@class DCollapsibleCategory
                ---@class DNumSlider
                ---@class DLabel
                ---@class Panel
            "#,
        );

        // Stool file: NO @param annotations, types come from @field on Tool
        let file_id = ws.def_file(
            "lua/weapons/gmod_tool/stools/glide_test.lua",
            r#"
                TOOL.Category = "Glide"
                TOOL.Name = "Projectile Launcher"

                function TOOL:RightClick(trace)
                    local ply = self:GetOwner()
                    ply:ConCommand("say test")
                end

                function TOOL.BuildCPanel(panel)
                    panel:Help("help text")
                    panel:AddControl("slider", {})
                    panel:NumSlider("Speed", "tool_speed", 0, 100)
                end
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        let undefined_field_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == undefined_field_code)
            .collect();

        let extract_name = |message: &str| {
            message
                .strip_prefix("Undefined field `")
                .and_then(|rest| rest.split_once('`'))
                .map(|(name, _)| name.to_string())
        };
        let undefined_field_names: Vec<String> = undefined_field_diags
            .iter()
            .filter_map(|diag| extract_name(&diag.message))
            .collect();

        assert!(
            !undefined_field_names.iter().any(|name| matches!(
                name.as_str(),
                "Help" | "AddControl" | "NumSlider" | "ConCommand" | "GetOwner"
            )),
            "unexpected undefined-field diagnostics for TOOL methods (type from @field): {undefined_field_diags:?}"
        );
    }

    #[gtest]
    fn test_vector_member_access_after_arithmetic_chain() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        let file_id = ws.def_file(
            "lua/glide/server/vector_chain.lua",
            r#"
                local function compute(steerDir, fixedLen)
                    local dirLen = steerDir:Length()
                    local dirNorm = steerDir / dirLen
                    local dirVec = dirNorm * fixedLen
                    dirVec:Normalize()
                    local len = dirVec:Length()
                    local cross = dirVec:Cross(Vector(1, 0, 0))
                    return len + cross.x + dirVec.y
                end
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        let undefined_field_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == undefined_field_code)
            .collect();

        let extract_name = |message: &str| {
            message
                .strip_prefix("Undefined field `")
                .and_then(|rest| rest.split_once('`'))
                .map(|(name, _)| name.to_string())
        };
        let undefined_field_names: Vec<String> = undefined_field_diags
            .iter()
            .filter_map(|diag| extract_name(&diag.message))
            .collect();

        assert!(
            !undefined_field_names
                .iter()
                .any(|name| matches!(name.as_str(), "Normalize" | "Length" | "Cross" | "x" | "y")),
            "unexpected vector undefined-field diagnostics after arithmetic chain: {undefined_field_diags:?}"
        );
    }

    #[test]
    fn test_dynamic_field_from_typed_variable() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        // Mimics the real pattern: function param with @type re-annotation
        let file_id = ws.def(
            r#"
                ---@class base_glide_car
                local ENT = {}

                ---@return table
                function ENT:GetTable() return {} end

                function ENT:OnPostThink(dt, selfTbl)
                    ---@type base_glide_car
                    selfTbl = selfTbl or self:GetTable()

                    selfTbl.throttleRamp = 0.5
                    local x = selfTbl.throttleRamp * selfTbl.throttleRamp * selfTbl.throttleRamp
                end
            "#,
        );

        let result = ws.analysis.diagnose_file(file_id, CancellationToken::new());
        let diags: Vec<_> = result
            .unwrap_or_default()
            .into_iter()
            .filter(|d| {
                d.code
                    == Some(lsp_types::NumberOrString::String(
                        "undefined-field".to_string(),
                    ))
            })
            .collect();

        let field_names: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();

        assert!(
            !field_names.iter().any(|m| m.contains("throttleRamp")),
            "throttleRamp should not trigger undefined-field after dynamic assignment: {field_names:?}"
        );
    }

    #[test]
    fn test_dynamic_field_multifile() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        // Class defined in file A
        ws.def_file(
            "lua/entities/base_glide_car/shared.lua",
            r#"
                ---@class base_glide_car
                local ENT = {}

                ---@return table
                function ENT:GetTable() return {} end
            "#,
        );

        // Dynamic field used in file B
        let file_id = ws.def_file(
            "lua/entities/base_glide_car/init.lua",
            r#"
                ---@class base_glide_car
                local ENT = {}

                local getTable = FindMetaTable("Entity").GetTable

                function ENT:OnPostThink(dt, selfTbl)
                    ---@type base_glide_car
                    selfTbl = selfTbl or getTable(self)

                    selfTbl.throttleRamp = 0.5
                    local x = selfTbl.throttleRamp * selfTbl.throttleRamp * selfTbl.throttleRamp
                end
            "#,
        );

        let result = ws.analysis.diagnose_file(file_id, CancellationToken::new());
        let diags: Vec<_> = result
            .unwrap_or_default()
            .into_iter()
            .filter(|d| {
                d.code
                    == Some(lsp_types::NumberOrString::String(
                        "undefined-field".to_string(),
                    ))
            })
            .collect();

        let field_names: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();

        assert!(
            !field_names.iter().any(|m| m.contains("throttleRamp")),
            "throttleRamp should not trigger undefined-field in multi-file setup: {field_names:?}"
        );
    }

    #[gtest]
    fn test_gettable_alias_preserves_typed_nested_state_fields_in_scripted_class() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        let file_id = ws.def_file(
            "cityrp/entities/entities/glide_wheel/init.lua",
            r#"
                ---@class Entity
                local Entity = {}

                ---@return tableof<self>
                function Entity:GetTable() end

                ---@generic T : table
                ---@param metaName `T`
                ---@return T
                function FindMetaTable(metaName) end

                ---@class GlideTraceData
                ---@field start number

                ---@class GlideWheelState
                ---@field traceData GlideTraceData

                local getTable = FindMetaTable("Entity").GetTable

                function ENT:Initialize()
                    ---@type GlideWheelState
                    self.state = {
                        traceData = {
                            start = 1,
                        },
                    }
                end

                function ENT:Think(selfTbl)
                    selfTbl = selfTbl or getTable(self)
                    local traceData = selfTbl.state.traceData
                    return traceData.start
                end
            "#,
        );

        let expected_state = ws.ty("GlideWheelState");
        let expected_trace = ws.ty("GlideTraceData");
        assert_eq!(
            index_expr_type(&mut ws, file_id, "selfTbl.state"),
            expected_state
        );
        assert_eq!(
            index_expr_type(&mut ws, file_id, "selfTbl.state.traceData"),
            expected_trace
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let undefined_field = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        let messages: Vec<_> = diagnostics
            .iter()
            .filter(|diagnostic| diagnostic.code == undefined_field)
            .map(|diagnostic| diagnostic.message.clone())
            .collect();

        assert!(
            !messages.iter().any(|message| {
                message.contains("state")
                    || message.contains("traceData")
                    || message.contains("start")
            }),
            "unexpected UndefinedField diagnostics for nested typed state access: {messages:?}"
        );
    }

    #[gtest]
    fn test_gettable_alias_uses_shadowed_self_argument_type_instead_of_receiver() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "cityrp/entities/entities/glide_wheel/init.lua",
            r#"
                ---@class Entity
                local Entity = {}

                ---@return tableof<self>
                function Entity:GetTable() end

                ---@generic T : table
                ---@param metaName `T`
                ---@return T
                function FindMetaTable(metaName) end

                ---@class GlideWheelState
                ---@field traceData table

                ---@class ShadowedSelfTable
                ---@field other number

                local getTable = FindMetaTable("Entity").GetTable

                function ENT:Initialize()
                    ---@type GlideWheelState
                    self.state = {
                        traceData = {},
                    }
                end

                function ENT:Think()
                    ---@type ShadowedSelfTable
                    local self = {
                        other = 1,
                    }

                    local shadowTbl = getTable(self)
                    local keep = shadowTbl.other
                    local notReceiverState = shadowTbl.state
                end
            "#,
        );

        assert_eq!(
            index_expr_type(&mut ws, file_id, "shadowTbl.other"),
            ws.ty("number")
        );
        assert_eq!(
            index_expr_type(&mut ws, file_id, "shadowTbl.state"),
            LuaType::Nil
        );
    }

    #[gtest]
    fn test_gettable_alias_or_assignment_preserves_tableof_self_for_nullable_table_param() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UncheckedNilAccess);

        let file_id = ws.def_file(
            "cityrp/entities/entities/base_glide/sv_input.lua",
            r#"
                ---@class Entity
                local Entity = {}

                ---Returns a table containing all key-value pairs stored on this entity's Lua table.
                ---@return tableof<self>
                function Entity:GetTable() end

                ---@generic T : table
                ---@param metaName `T`
                ---@return T
                function FindMetaTable(metaName) end

                ---@class base_glide : Entity
                local ENT = {}

                local getTable = FindMetaTable("Entity").GetTable

                function ENT:Initialize()
                    self.inputFloats = {
                        {
                            1.0,
                        },
                    }
                end

                ---@param seatIndex number
                ---@param action string
                ---@param entTbl table|nil
                function ENT:GetInputFloat(seatIndex, action, entTbl)
                    entTbl = entTbl or getTable(self)

                    local floats = entTbl.inputFloats[seatIndex]
                    return floats[action]
                end
            "#,
        );

        let ent_tbl_assignment_type = assign_value_type(&mut ws, file_id, "entTbl");
        let ent_tbl_type = index_expr_prefix_type(&mut ws, file_id, "entTbl.inputFloats");
        let input_floats_type = index_expr_type(&mut ws, file_id, "entTbl.inputFloats");

        assert_that!(
            ws.humanize_type_detailed(ent_tbl_assignment_type).as_str(),
            eq("(table|tableof<base_glide>)"),
            "the raw `entTbl or getTable(self)` assignment should still include the tableof<self> branch before assignment-flow narrowing"
        );
        assert_that!(
            ws.humanize_type_detailed(ent_tbl_type).as_str(),
            eq("tableof<base_glide>"),
            "the local should keep the GetTable(self) tableof<self> type after `entTbl = entTbl or getTable(self)`"
        );
        // The shaped sequential literal `{ { 1.0 } }` is modeled as a dynamic
        // table (TableConst) with an integer member `[1]` holding the inner row,
        // rather than the old immutable nested-tuple `((1.0))`. The detailed
        // rendering preserves that rich shape instead of degrading to a generic
        // `table`, which is the property this test guards.
        assert_that!(
            ws.humanize_type_detailed(input_floats_type).as_str(),
            eq("{\n    [1]: (1.0),\n}"),
            "entTbl.inputFloats should keep the scripted-class field shape instead of degrading to generic table access"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let unchecked_nil_access = diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code == Some(NumberOrString::String("unchecked-nil-access".to_string()))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();

        assert_that!(
            unchecked_nil_access,
            is_empty(),
            "entTbl.inputFloats[seatIndex] should not report unchecked-nil-access after the GetTable(self) fallback"
        );
    }

    #[gtest]
    fn test_gettable_alias_or_assignment_does_not_collapse_when_using_different_expression() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "cityrp/entities/entities/base_glide/sv_input.lua",
            r#"
                ---@class Entity
                local Entity = {}

                ---Returns a table containing all key-value pairs stored on this entity's Lua table.
                ---@return tableof<self>
                function Entity:GetTable() end

                ---@generic T : table
                ---@param metaName `T`
                ---@return T
                function FindMetaTable(metaName) end

                ---@class base_glide : Entity
                local ENT = {}

                local getTable = FindMetaTable("Entity").GetTable

                function ENT:Initialize()
                    self.inputFloats = {
                        {
                            1.0,
                        },
                    }
                end

                ---@return table|nil
                local function getPlainTable() end

                ---@param seatIndex number
                ---@param action string
                ---@param entTbl table|nil
                function ENT:GetInputFloat(seatIndex, action, entTbl)
                    entTbl = getPlainTable() or getTable(self)

                    local floats = entTbl.inputFloats[seatIndex]
                    return floats[action]
                end
            "#,
        );

        let ent_tbl_type = index_expr_prefix_type(&mut ws, file_id, "entTbl.inputFloats");

        assert_that!(
            ws.humanize_type_detailed(ent_tbl_type).as_str(),
            eq("(table|tableof<base_glide>)"),
            "should NOT collapse table|tableof<base_glide> since getPlainTable() is not the same variable"
        );
    }

    #[test]
    fn test_dynamic_field_table_typed_in_class_file() {
        // When selfTbl is typed as `table` (e.g. from Entity:GetTable()),
        // dynamic field assignments in gmod class files should still be indexed
        // under the class type.
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_file(
            "lua/entities/base_glide_car/shared.lua",
            r#"
                ---@class base_glide_car : Entity
                local ENT = {}
            "#,
        );

        // selfTbl is typed as `table` (no @type annotation)
        let file_id = ws.def_file(
            "lua/entities/base_glide_car/sv_braking.lua",
            r#"
                ---@class base_glide_car
                local ENT = {}

                ---@return tableof<self>
                local function getTable(e) return {} end

                function ENT:BrakeInit()
                    local selfTbl = getTable(self)
                    selfTbl.frontBrake = 0
                    selfTbl.rearBrake = 0
                end

                function ENT:GetBrakes()
                    return self.frontBrake + self.rearBrake
                end
            "#,
        );

        let result = ws.analysis.diagnose_file(file_id, CancellationToken::new());
        let diags: Vec<_> = result
            .unwrap_or_default()
            .into_iter()
            .filter(|d| {
                d.code
                    == Some(lsp_types::NumberOrString::String(
                        "undefined-field".to_string(),
                    ))
            })
            .collect();

        let field_names: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();

        assert!(
            !field_names
                .iter()
                .any(|m| m.contains("frontBrake") || m.contains("rearBrake")),
            "frontBrake/rearBrake should not trigger undefined-field when assigned via table-typed selfTbl in class file: {field_names:?}"
        );
    }

    #[gtest]
    fn test_numeric_for_index_expr_on_inferred_ent_weapons_table() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);
        ws.enable_check(DiagnosticCode::UncheckedNilAccess);

        ws.def_file(
            "lua/entities/base_glide/shared.lua",
            r#"
                ---@class base_glide : Entity
                local ENT = {}
            "#,
        );

        let file_id = ws.def_file(
            "lua/entities/base_glide/sv_weapons.lua",
            r#"
                ---@class base_glide
                local ENT = {}

                local registry = {
                    base = {
                        Initialize = function(self) end,
                        OnRemove = function(self) end,
                    }
                }

                local function CreateVehicleWeapon(className, data)
                    local class = registry[className]
                    return setmetatable(data or {}, { __index = class })
                end

                function ENT:CreateWeapon(className, data)
                    local weapon = CreateVehicleWeapon(className, data)
                    local index = (self.weaponCount or 0) + 1

                    self.weaponCount = index
                    self.weapons = self.weapons or {}
                    self.weapons[index] = weapon
                    weapon:Initialize()
                end

                function ENT:ClearWeapons()
                    local myWeapons = self.weapons
                    if not myWeapons then return end

                    for i = #myWeapons, 1, -1 do
                        myWeapons[i]:OnRemove()
                        myWeapons[i] = nil
                    end
                end
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();

        let field_names: Vec<String> = diagnostics
            .iter()
            .filter(|d| d.code == Some(NumberOrString::String("undefined-field".to_string())))
            .map(|d| d.message.clone())
            .collect();

        assert!(
            !field_names.iter().any(|m| m.contains("`[i]`")),
            "numeric for index into inferred ENT weapons table should not trigger undefined-field: {field_names:?}"
        );

        let unchecked_nil_accesses: Vec<String> = diagnostics
            .iter()
            .filter(|d| {
                d.code
                    == Some(NumberOrString::String(
                        DiagnosticCode::UncheckedNilAccess.get_name().to_string(),
                    ))
            })
            .map(|d| d.message.clone())
            .collect();

        assert!(
            unchecked_nil_accesses.is_empty(),
            "numeric for index into inferred ENT weapons table should not trigger unchecked-nil-access: {unchecked_nil_accesses:?}"
        );
    }

    #[gtest]
    fn test_gmod_vehicle_seat_table_index_is_deterministic_for_self_and_gettable_alias() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.strict.array_index = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "annotations/vehicle.lua",
            r#"
                ---@class Entity
                local Entity = {}

                ---@return tableof<self>
                function Entity:GetTable() end

                ---@class Vehicle : Entity
                local Vehicle = {}

                ---@return Entity
                function Vehicle:GetDriver() end

                ---@class prop_vehicle_prisoner_pod : Vehicle

                ---@class NotEntity
                local NotEntity = {}

                ---@return table
                function NotEntity:GetTable() end
            "#,
        );

        ws.def_file(
            "lua/entities/base_glide/shared.lua",
            r#"
                ---@class base_glide : Entity
                local ENT = {}
            "#,
        );

        let init_code = r#"
            ---@class base_glide
            local ENT = {}

            ---@type Entity
            local EntityMeta = {}
            local getTable = EntityMeta.GetTable
            ---@type NotEntity
            local NotEntityMeta = {}
            local nonEntityGetTable = NotEntityMeta.GetTable
            ---@return table
            local function makePlainTable() end

            function ENT:Initialize()
                self.seats = {}
            end

            function ENT:Use()
                local earlyDriverSeatSelf = self.seats[1]
            end

            ---@param index integer
            function ENT:CreateSeat(index)
                ---@type prop_vehicle_prisoner_pod
                local seat
                self.seats[index] = seat
            end

            function ENT:Think()
                local selfTbl = getTable(self)
                ---@type table
                local unrelated = {}
                local unrelatedSeats = unrelated.seats
                if #selfTbl.seats > 0 then
                    local driverSeatAlias = selfTbl.seats[1]
                    local driverSeatSelf = self.seats[1]
                    local driver = IsValid(driverSeatAlias) and driverSeatAlias:GetDriver() or NULL
                end
                if #selfTbl.seats > 0 then
                    local driverSeatSelfOnly = self.seats[1]
                    local driver = IsValid(driverSeatSelfOnly) and driverSeatSelfOnly:GetDriver() or NULL
                end
                local nonEntityTbl = nonEntityGetTable(self)
                local nonEntitySeats = nonEntityTbl.seats
                local reassignedTbl = getTable(self)
                reassignedTbl = makePlainTable()
                local reassignedSeats = reassignedTbl.seats
                do
                    ---@type table
                    local selfTbl = {}
                    local namedSelfTblSeats = selfTbl.seats
                end
            end
        "#;

        let file_id = ws.def_file("lua/entities/base_glide/init.lua", init_code);
        let expected = ws.ty("prop_vehicle_prisoner_pod");
        let expected_seats = ws.ty("prop_vehicle_prisoner_pod[]");

        let alias_seats_type = index_expr_type(&mut ws, file_id, "selfTbl.seats");
        let self_seats_type = index_expr_type(&mut ws, file_id, "self.seats");
        let expected_seats_display = "{\n    prop_vehicle_prisoner_pod,\n}";

        assert_that!(
            ws.check_type(&alias_seats_type, &expected_seats),
            eq(true),
            "selfTbl.seats should be a prop_vehicle_prisoner_pod array, got {}",
            ws.humanize_type_detailed(alias_seats_type.clone())
        );
        let alias_seats_display = ws.humanize_type_detailed(alias_seats_type.clone());
        assert_that!(alias_seats_display.as_str(), eq(expected_seats_display));
        assert_that!(
            ws.check_type(&self_seats_type, &expected_seats),
            eq(true),
            "self.seats should be a prop_vehicle_prisoner_pod array, got {}",
            ws.humanize_type_detailed(self_seats_type.clone())
        );
        let self_seats_display = ws.humanize_type_detailed(self_seats_type.clone());
        assert_that!(self_seats_display.as_str(), eq(expected_seats_display));

        let alias_type = local_name_type(&mut ws, file_id, "driverSeatAlias");
        let self_type = local_name_type(&mut ws, file_id, "driverSeatSelf");
        let alias_expr_type = local_assignment_value_type(&mut ws, file_id, "driverSeatAlias");
        let self_expr_type = local_assignment_value_type(&mut ws, file_id, "driverSeatSelf");
        let self_only_type = local_name_type(&mut ws, file_id, "driverSeatSelfOnly");
        let self_only_expr_type =
            local_assignment_value_type(&mut ws, file_id, "driverSeatSelfOnly");
        let early_self_type = local_name_type(&mut ws, file_id, "earlyDriverSeatSelf");
        let unrelated_type = local_name_type(&mut ws, file_id, "unrelatedSeats");
        let named_self_tbl_type = local_name_type(&mut ws, file_id, "namedSelfTblSeats");
        let non_entity_get_table_type = local_name_type(&mut ws, file_id, "nonEntitySeats");
        let reassigned_get_table_type = local_name_type(&mut ws, file_id, "reassignedSeats");
        let expected_driver_seat_display = "prop_vehicle_prisoner_pod";

        assert_that!(
            ws.check_type(&alias_type, &expected),
            eq(true),
            "selfTbl.seats[1] should be prop_vehicle_prisoner_pod after the length guard, got {}",
            ws.humanize_type_detailed(alias_type.clone())
        );
        let alias_display = ws.humanize_type(alias_type.clone());
        assert_that!(alias_display.as_str(), eq(expected_driver_seat_display));
        assert_that!(
            ws.humanize_type(alias_expr_type).as_str(),
            eq(expected_driver_seat_display),
            "selfTbl.seats[1] expression should match hover RHS inference"
        );
        assert_that!(
            ws.check_type(&expected, &alias_type),
            eq(true),
            "selfTbl.seats[1] should not include stale any/table members, got {}",
            ws.humanize_type_detailed(alias_type)
        );
        assert_that!(
            ws.check_type(&self_type, &expected),
            eq(true),
            "self.seats[1] should be prop_vehicle_prisoner_pod after the length guard, got {}",
            ws.humanize_type_detailed(self_type.clone())
        );
        let self_display = ws.humanize_type(self_type.clone());
        assert_that!(self_display.as_str(), eq(expected_driver_seat_display));
        assert_that!(
            ws.humanize_type(self_expr_type).as_str(),
            eq(expected_driver_seat_display),
            "self.seats[1] expression should match hover RHS inference"
        );
        assert_that!(
            ws.check_type(&expected, &self_type),
            eq(true),
            "self.seats[1] should not collapse to nil, got {}",
            ws.humanize_type_detailed(self_type)
        );
        assert_that!(
            ws.humanize_type(self_only_type).as_str(),
            eq(expected_driver_seat_display),
            "self.seats[1] should work when it is the only driverSeat assignment"
        );
        assert_that!(
            ws.humanize_type(self_only_expr_type).as_str(),
            eq(expected_driver_seat_display),
            "self.seats[1] RHS hover type should work without a sibling selfTbl.seats[1] expression"
        );
        assert_that!(
            ws.humanize_type(early_self_type).as_str(),
            eq(expected_driver_seat_display),
            "an early self.seats[1] read must not remain cached as nil before later seat writes are analyzed"
        );
        let unrelated_display = ws.humanize_type(unrelated_type);
        assert_that!(
            unrelated_display.as_str(),
            eq("any?"),
            "plain tables unrelated to Entity:GetTable(self) must not inherit scripted-class fields"
        );
        let named_self_tbl_display = ws.humanize_type(named_self_tbl_type);
        assert_that!(
            named_self_tbl_display.as_str(),
            eq("any?"),
            "a table named selfTbl must not inherit scripted-class fields unless it comes from Entity:GetTable(self)"
        );
        let non_entity_get_table_display = ws.humanize_type(non_entity_get_table_type);
        assert_that!(
            non_entity_get_table_display.as_str(),
            eq("any?"),
            "a non-Entity GetTable(self) alias must not inherit scripted-class fields"
        );
        let reassigned_get_table_display = ws.humanize_type_detailed(reassigned_get_table_type);
        assert_that!(
            reassigned_get_table_display.as_str(),
            eq("any?"),
            "a reassigned GetTable(self) alias must not keep scripted-class field provenance"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let unchecked_nil_access = diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code == Some(NumberOrString::String("unchecked-nil-access".to_string()))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();

        assert_that!(
            unchecked_nil_access,
            is_empty(),
            "IsValid(driverSeatAlias) should narrow the indexed seat receiver"
        );
    }

    #[gtest]
    fn test_gmod_vehicle_seat_table_index_is_same_after_batch_load_and_touch() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.strict.array_index = true;
        ws.update_emmyrc(emmyrc);

        let annotations_code = r#"
            ---@class Entity
            local Entity = {}

            ---@return tableof<self>
            function Entity:GetTable() end
            ---@generic T : table
            ---@param metaName `T`
            ---@return (definition) T
            function FindMetaTable(metaName) end

            ---@class Vehicle : Entity
            local Vehicle = {}

            ---@return Entity
            function Vehicle:GetDriver() end

            ---@class prop_vehicle_prisoner_pod : Vehicle

            ---@return table
            function Entity:GetTable() end
        "#;

        let shared_code = r#"
            ---@class base_glide : Entity
            local ENT = {}
        "#;

        let cl_init_code = r#"
            function ENT:RefreshSeats()
                local seats = {}
                self.seats = table.Reverse(seats)
            end

            function ENT:OnRemove()
                self.seats = nil
            end
        "#;

        let init_path = "lua/entities/base_glide/init.lua";
        let init_code = r#"
            ---@class base_glide
            local ENT = {}

            local EntityMeta = FindMetaTable("Entity")
            local getTable = EntityMeta.GetTable

            function ENT:Initialize()
                self.seats = self.seats or {}
            end

            function ENT:Use()
                local earlyDriverSeatSelf = self.seats[1]
            end

            ---@param index integer
            function ENT:CreateSeat(index)
                ---@type prop_vehicle_prisoner_pod
                local seat
                self.seats[index] = seat
            end

            function ENT:Think()
                local selfTbl = getTable(self)
                if #selfTbl.seats > 0 then
                    local driverSeatAlias = selfTbl.seats[1]
                    local driverSeatSelf = self.seats[1]
                    local driver = IsValid(driverSeatAlias) and driverSeatAlias:GetDriver() or NULL
                end
            end
        "#;

        ws.def_files(vec![
            ("annotations/vehicle.lua", annotations_code),
            ("lua/entities/base_glide/cl_init.lua", cl_init_code),
            ("lua/entities/base_glide/shared.lua", shared_code),
            (init_path, init_code),
        ]);
        let init_uri = ws.virtual_url_generator.new_uri(init_path);
        let file_id = ws
            .analysis
            .compilation
            .get_db()
            .get_vfs()
            .get_file_id(&init_uri)
            .expect("expected init file id");

        let expected = ws.ty("prop_vehicle_prisoner_pod");
        let expected_seats_display = "{\n    prop_vehicle_prisoner_pod,\n}";

        let alias_seats_initial = index_expr_type(&mut ws, file_id, "selfTbl.seats");
        let alias_initial = local_name_type(&mut ws, file_id, "driverSeatAlias");
        let self_initial = local_name_type(&mut ws, file_id, "driverSeatSelf");
        let self_expr_initial = local_assignment_value_type(&mut ws, file_id, "driverSeatSelf");
        let early_self_initial = local_name_type(&mut ws, file_id, "earlyDriverSeatSelf");

        assert_that!(
            ws.humanize_type_detailed(alias_seats_initial).as_str(),
            eq(expected_seats_display),
            "cold batch load should not produce stale table unions for selfTbl.seats"
        );
        assert_that!(
            ws.check_type(&alias_initial, &expected),
            eq(true),
            "cold batch load should type selfTbl.seats[1] as the vehicle seat"
        );
        assert_that!(
            ws.check_type(&self_initial, &expected),
            eq(true),
            "cold batch load should type self.seats[1] as the vehicle seat"
        );
        assert_that!(
            ws.humanize_type(self_expr_initial).as_str(),
            eq("prop_vehicle_prisoner_pod"),
            "cold batch load should type self.seats[1] RHS for hover"
        );
        assert_that!(
            ws.humanize_type(early_self_initial).as_str(),
            eq("prop_vehicle_prisoner_pod"),
            "an early self.seats[1] read must be revisited after later seat writes are analyzed"
        );

        ws.analysis
            .update_file_by_uri(&init_uri, Some(format!("{init_code}\n")))
            .expect("expected touched init file id");

        let alias_seats_after_touch = index_expr_type(&mut ws, file_id, "selfTbl.seats");
        let alias_after_touch = local_name_type(&mut ws, file_id, "driverSeatAlias");
        let self_after_touch = local_name_type(&mut ws, file_id, "driverSeatSelf");
        let self_expr_after_touch = local_assignment_value_type(&mut ws, file_id, "driverSeatSelf");
        let early_self_after_touch = local_name_type(&mut ws, file_id, "earlyDriverSeatSelf");

        assert_that!(
            ws.humanize_type_detailed(alias_seats_after_touch).as_str(),
            eq(expected_seats_display),
            "touching the file should not change selfTbl.seats type"
        );
        assert_that!(ws.check_type(&alias_after_touch, &expected), eq(true));
        assert_that!(ws.check_type(&self_after_touch, &expected), eq(true));
        assert_that!(
            ws.humanize_type(self_expr_after_touch).as_str(),
            eq("prop_vehicle_prisoner_pod"),
            "touching the file should not change self.seats[1] RHS hover type"
        );
        assert_that!(
            ws.humanize_type(early_self_after_touch).as_str(),
            eq("prop_vehicle_prisoner_pod")
        );
    }

    #[gtest]
    fn test_gmod_vehicle_sound_alias_guards_are_same_after_batch_load_and_touch() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.analysis
            .diagnostic
            .enable_only(DiagnosticCode::UndefinedField);

        let annotations_code = r#"
            ---@class Entity
            local Entity = {}

            ---@class CSoundPatch
            local CSoundPatch = {}

            function CSoundPatch:Stop() end
            function CSoundPatch:SetSoundLevel(level) end
            function CSoundPatch:PlayEx(volume, pitch) end
            function CSoundPatch:ChangeVolume(volume) end
            ---@return number
            function CSoundPatch:GetVolume() end

            ---@return CSoundPatch
            function CreateSound(parent, path) end
        "#;

        let base_shared_code = r#"
            ENT.Type = "anim"
        "#;

        let base_cl_init_code = r#"
            function ENT:Initialize()
                self.sounds = {}
            end

            function ENT:OnEngineStateChange()
                local snd = self:CreateLoopingSound("start", "start.wav", 70, self)
                snd:PlayEx(1, 100)
            end

            function ENT:CreateLoopingSound(id, path, level, parent)
                local snd = self.sounds[id]

                if not snd then
                    snd = CreateSound(parent or self, path)
                    snd:SetSoundLevel(level)
                    self.sounds[id] = snd
                end

                return snd
            end

            function ENT:InternalDeactivateSounds()
                local sounds = self.sounds

                for k, snd in pairs(sounds) do
                    snd:Stop()
                    sounds[k] = nil
                end
            end
        "#;

        let car_shared_code = r#"
            ENT.Type = "anim"
            ENT.Base = "base_glide"
            ENT.StopStartTailOnInput = false
            ENT.HornSound = "horn.wav"
        "#;

        let car_path = "lua/entities/base_glide_car/cl_init.lua";
        let car_code = r#"
            function ENT:OnUpdateSounds()
                local sounds = self.sounds
                local dt = 0
                local isHonking = false

                if self.StopStartTailOnInput and sounds.startTail then
                    sounds.startTail:Stop()
                    sounds.startTail = nil
                end

                if isHonking and self.HornSound and self.HornSound ~= "" then
                    if sounds.horn then
                        sounds.horn:ChangeVolume(1)
                    else
                        local snd = self:CreateLoopingSound("horn", self.HornSound, 85, self)
                        snd:PlayEx(1, 100)
                    end
                elseif sounds.horn then
                    if sounds.horn:GetVolume() > 0 then
                        sounds.horn:ChangeVolume(sounds.horn:GetVolume() - dt * 8)
                    else
                        sounds.horn:Stop()
                        sounds.horn = nil
                    end
                end
            end
        "#;

        ws.def_files(vec![
            ("annotations/sound.lua", annotations_code),
            ("lua/entities/base_glide/shared.lua", base_shared_code),
            ("lua/entities/base_glide/cl_init.lua", base_cl_init_code),
            ("lua/entities/base_glide_car/shared.lua", car_shared_code),
            (car_path, car_code),
        ]);
        let car_uri = ws.virtual_url_generator.new_uri(car_path);
        let file_id = ws
            .analysis
            .compilation
            .get_db()
            .get_vfs()
            .get_file_id(&car_uri)
            .expect("expected car cl_init file id");

        let initial_sounds_type = local_name_type(&mut ws, file_id, "sounds");
        let initial_sounds_display = ws.humanize_type_detailed(initial_sounds_type);
        assert_that!(
            initial_sounds_display.as_str(),
            contains_substring("CSoundPatch"),
            "cold batch load should type the sounds alias from dynamic sound assignments"
        );
        assert!(
            !initial_sounds_display.contains("horn")
                && !initial_sounds_display.contains("startTail")
                && !initial_sounds_display.contains(": nil"),
            "cleanup-only sound fields should not pollute the cold sounds alias hover type: {initial_sounds_display}"
        );

        let initial_diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let initial_undefined_fields = initial_diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code == Some(NumberOrString::String("undefined-field".to_string()))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();
        assert_that!(
            initial_undefined_fields,
            is_empty(),
            "guarded sound fields should not produce undefined-field diagnostics on cold batch load"
        );

        ws.analysis
            .update_file_by_uri(&car_uri, Some(format!("{car_code}\n")))
            .expect("expected touched car cl_init file id");

        let touched_sounds_type = local_name_type(&mut ws, file_id, "sounds");
        let touched_sounds_display = ws.humanize_type_detailed(touched_sounds_type);
        assert_that!(
            touched_sounds_display.as_str(),
            eq(initial_sounds_display.as_str()),
            "touching the file should not change the sounds alias hover type"
        );

        let touched_diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let touched_undefined_fields = touched_diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code == Some(NumberOrString::String("undefined-field".to_string()))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();
        assert_that!(
            touched_undefined_fields,
            is_empty(),
            "guarded sound fields should not produce undefined-field diagnostics after touching the file"
        );
    }

    #[gtest]
    fn test_gmod_vehicle_sound_guarded_starttail_call_has_no_unchecked_nil_and_non_any_type() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.analysis
            .diagnostic
            .enable_only(DiagnosticCode::UncheckedNilAccess);

        let annotations_code = r#"
            ---@class Entity
            local Entity = {}

            ---@class CSoundPatch
            local CSoundPatch = {}

            function CSoundPatch:Stop() end
            function CSoundPatch:SetSoundLevel(level) end
            function CSoundPatch:PlayEx(volume, pitch) end

            ---@return CSoundPatch
            function CreateSound(parent, path) end

            ---@return string
            function GetDynamicSoundId() end
        "#;

        let base_shared_code = r#"
            ENT.Type = "anim"
        "#;

        let base_cl_init_code = r#"
            function ENT:Initialize()
                self.sounds = {}
            end

            function ENT:OnEngineStateChange()
                local snd = self:CreateLoopingSound("start", "start.wav", 70, self)
                snd:PlayEx(1, 100)

                local dynamicId = GetDynamicSoundId()
                local dynamicSound = self:CreateLoopingSound(dynamicId, "dynamic.wav", 70, self)
                dynamicSound:PlayEx(1, 100)
            end

            function ENT:CreateLoopingSound(id, path, level, parent)
                local snd = self.sounds[id]

                if not snd then
                    snd = CreateSound(parent or self, path)
                    snd:SetSoundLevel(level)
                    self.sounds[id] = snd
                end

                return snd
            end

            function ENT:InternalDeactivateSounds()
                local sounds = self.sounds

                for k, snd in pairs(sounds) do
                    snd:Stop()
                    sounds[k] = nil
                end
            end
        "#;

        let car_shared_code = r#"
            ENT.Type = "anim"
            ENT.Base = "base_glide"
            ENT.StopStartTailOnInput = false
            ENT.HornSound = "horn.wav"
        "#;

        let car_path = "lua/entities/base_glide_car/cl_init.lua";
        let car_code = r#"
            function ENT:OnUpdateSounds()
                local sounds = self.sounds
                local dt = 0
                local isHonking = false
                local startTailPreview = sounds.startTail

                if self.StopStartTailOnInput and sounds.startTail then
                    local guardedStartTail = sounds.startTail
                    guardedStartTail:Stop()
                    sounds.startTail:Stop()
                    sounds["startTail"] = nil
                end

                if isHonking and self.HornSound and self.HornSound ~= "" then
                    if sounds.horn then
                        sounds.horn:ChangeVolume(1)
                    else
                        local snd = self:CreateLoopingSound("horn", self.HornSound, 85, self)
                        snd:PlayEx(1, 100)
                    end
                elseif sounds.horn then
                    if sounds.horn:GetVolume() > 0 then
                        sounds.horn:ChangeVolume(sounds.horn:GetVolume() - dt * 8)
                    else
                        sounds.horn:Stop()
                        sounds.horn = nil
                    end
                end
            end
        "#;

        ws.def_files(vec![
            ("annotations/sound.lua", annotations_code),
            ("lua/entities/base_glide/shared.lua", base_shared_code),
            ("lua/entities/base_glide/cl_init.lua", base_cl_init_code),
            ("lua/entities/base_glide_car/shared.lua", car_shared_code),
            (car_path, car_code),
        ]);

        let car_uri = ws.virtual_url_generator.new_uri(car_path);
        let file_id = ws
            .analysis
            .compilation
            .get_db()
            .get_vfs()
            .get_file_id(&car_uri)
            .expect("expected car cl_init file id");

        let start_tail_type = index_expr_type(&mut ws, file_id, "sounds.startTail");
        let start_tail_display = ws.humanize_type_detailed(start_tail_type);
        assert_that!(
            start_tail_display.as_str(),
            contains_substring("CSoundPatch"),
            "sounds.startTail should keep inferred sound patch type from dynamic map indexing"
        );
        assert_that!(
            start_tail_display.contains("any"),
            eq(false),
            "sounds.startTail should not widen to any"
        );
        let sounds_type = local_name_type(&mut ws, file_id, "sounds");
        let sounds_display = ws.humanize_type_detailed(sounds_type);
        assert_that!(
            sounds_display.as_str(),
            contains_substring("CSoundPatch"),
            "sounds alias should keep inferred CSoundPatch map value type"
        );
        let preview_start_tail_type = local_name_type(&mut ws, file_id, "startTailPreview");
        let preview_start_tail_display = ws.humanize_type_detailed(preview_start_tail_type);
        assert_that!(
            preview_start_tail_display.as_str(),
            contains_substring("CSoundPatch"),
            "unguarded sounds.startTail access should use map value type"
        );
        assert_that!(
            preview_start_tail_display.contains("any"),
            eq(false),
            "unguarded sounds.startTail access should not widen to any"
        );

        let guarded_start_tail_type = local_name_type(&mut ws, file_id, "guardedStartTail");
        let guarded_start_tail_display = ws.humanize_type_detailed(guarded_start_tail_type);
        assert_that!(
            guarded_start_tail_display.as_str(),
            contains_substring("CSoundPatch"),
            "guarded local alias should preserve concrete sound patch type"
        );
        assert_that!(
            guarded_start_tail_display.contains("any"),
            eq(false),
            "guarded local alias should not widen to any"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let unchecked_nil_diagnostics = diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code
                    == Some(NumberOrString::String(
                        DiagnosticCode::UncheckedNilAccess.get_name().to_string(),
                    ))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();

        assert_that!(
            unchecked_nil_diagnostics,
            is_empty(),
            "guarded sounds.startTail call should not report unchecked-nil-access"
        );
    }

    #[test]
    fn test_guarded_dynamic_field_local_alias_matches_rhs_type() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);

        let consumer_path = "lua/weapons/glide_refueler_base.lua";
        let consumer_code = r#"
            ---@class Angle
            local Angle = {}

            ---@class Entity
            local Entity = {}

            ---@return Angle
            function Entity:GetAngles() end

            ---@param ang Angle
            ---@return Angle
            function Entity:LocalToWorldAngles(ang) end

            ---@class Angle
            ---@return Vector
            function Angle:Forward() end

            ---@return Vector
            function Angle:Up() end

            ---@return Vector
            function Angle:Right() end

            ---@class FillPort
            ---@field worldPos Vector
            ---@field ang Angle

            ---@return FillPort
            local function getPort()
                ---@type FillPort
                local port = { ang = Angle }
                port.worldAng = Entity:LocalToWorldAngles(port.ang)
                return port
            end

            local function draw()
                local port = getPort()
                local veh = Entity
                if not port or not port.worldPos or not port.worldAng then return end

                local pos = port.worldPos
                local vehAng = veh:GetAngles()
                local portAng = port.worldAng
                local normal = portAng:Forward()
                local upAxis = vehAng:Up()
                local rightAxis = vehAng:Right()
                print(pos, vehAng, portAng, normal, upAxis, rightAxis)
            end
        "#;
        let consumer_file = ws.def_file(consumer_path, consumer_code);

        let rhs_type = local_assignment_value_type(&mut ws, consumer_file, "portAng");
        let alias_type = local_name_type(&mut ws, consumer_file, "portAng");
        let pos_type = local_name_type(&mut ws, consumer_file, "pos");
        let veh_ang_type = local_name_type(&mut ws, consumer_file, "vehAng");
        let normal_type = local_name_type(&mut ws, consumer_file, "normal");
        let up_axis_type = local_name_type(&mut ws, consumer_file, "upAxis");
        let right_axis_type = local_name_type(&mut ws, consumer_file, "rightAxis");

        assert_that!(
            ws.humanize_type(rhs_type).as_str(),
            eq("Angle"),
            "port.worldAng RHS should infer as Angle"
        );
        assert_that!(ws.humanize_type(pos_type).as_str(), eq("Vector"));
        assert_that!(ws.humanize_type(veh_ang_type).as_str(), eq("Angle"));
        assert_that!(
            ws.humanize_type(alias_type).as_str(),
            eq("Angle"),
            "local portAng should keep the same Angle type as its RHS"
        );
        assert_that!(ws.humanize_type(normal_type).as_str(), eq("Vector"));
        assert_that!(ws.humanize_type(up_axis_type).as_str(), eq("Vector"));
        assert_that!(ws.humanize_type(right_axis_type).as_str(), eq("Vector"));

        let consumer_uri = ws.virtual_url_generator.new_uri(consumer_path);
        ws.analysis
            .update_file_by_uri(&consumer_uri, Some(format!("{consumer_code}\n")))
            .expect("expected touched consumer file id");

        let touched_pos_type = local_name_type(&mut ws, consumer_file, "pos");
        let touched_veh_ang_type = local_name_type(&mut ws, consumer_file, "vehAng");
        let touched_port_ang_type = local_name_type(&mut ws, consumer_file, "portAng");
        let touched_normal_type = local_name_type(&mut ws, consumer_file, "normal");
        let touched_up_axis_type = local_name_type(&mut ws, consumer_file, "upAxis");
        let touched_right_axis_type = local_name_type(&mut ws, consumer_file, "rightAxis");

        assert_that!(
            ws.humanize_type(touched_pos_type).as_str(),
            eq("Vector"),
            "touching the file should not change pos"
        );
        assert_that!(
            ws.humanize_type(touched_veh_ang_type).as_str(),
            eq("Angle"),
            "touching the file should not change vehAng"
        );
        assert_that!(
            ws.humanize_type(touched_port_ang_type).as_str(),
            eq("Angle"),
            "touching the file should not change portAng"
        );
        assert_that!(
            ws.humanize_type(touched_normal_type).as_str(),
            eq("Vector"),
            "touching the file should not change normal"
        );
        assert_that!(
            ws.humanize_type(touched_up_axis_type).as_str(),
            eq("Vector"),
            "touching the file should not change upAxis"
        );
        assert_that!(
            ws.humanize_type(touched_right_axis_type).as_str(),
            eq("Vector"),
            "touching the file should not change rightAxis"
        );
        assert_that!(
            {
                ws.analysis
                    .diagnostic
                    .enable_only(DiagnosticCode::UncheckedNilAccess);
                ws.analysis
                    .diagnose_file(consumer_file, CancellationToken::new())
                    .unwrap_or_default()
                    .iter()
                    .any(|diagnostic| {
                        diagnostic.code
                            == Some(NumberOrString::String(
                                DiagnosticCode::UncheckedNilAccess.get_name().to_string(),
                            ))
                    })
            },
            eq(false),
            "portAng:Forward() should not report unchecked-nil-access"
        );
    }

    #[gtest]
    fn test_scripted_weapon_field_read_unions_assignment_history_in_same_file() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);

        let file_ids = ws.def_files(vec![
            (
                "lua/autorun/sh_glide.lua",
                r#"
                ---@class Glide
                Glide = Glide or {}
                "#,
            ),
            (
                "lua/weapons/glide_refueler_base.lua",
                r#"
            ---@class Entity
            ---@return Entity
            function Entity:GetParent() end

            ---@class Player: Entity
            ---@return TraceResult
            function Player:GetEyeTrace() end

            ---@class TraceResult
            ---@field Entity Entity

            ---@class ENT: Entity
            ---@class base_glide: ENT
            ---@field IsGlideVehicle boolean
            ---@class base_glide_car: base_glide
            ---@class fl_bmw_i3: base_glide_car

            ---@return Player?
            function SWEP:GetOwner() end

            function SWEP:Deploy()
                Glide.RefuelerHUDWeapon = self
            end

            function SWEP:StartFueling()
                if CLIENT then return end
                local vehicle = nil
                vehicle = select(1, self:GetTargetVehicle())
                if not IsValid(vehicle) then return end
                self.activeVehicle = vehicle
            end

            local function resolveVehicle(ent)
                if not IsValid(ent) then return nil end
                if ent.IsGlideVehicle then return ent end

                local parent = ent:GetParent()
                if IsValid(parent) and parent.IsGlideVehicle then
                    return parent
                end

                return nil
            end

            function SWEP:GetTargetVehicle()
                local owner = self:GetOwner()
                if not IsValid(owner) then return nil, nil end

                local trace = owner:GetEyeTrace()
                local vehicle = resolveVehicle(trace.Entity)
                return vehicle, trace
            end

            ---@realm server
            function SWEP:CancelFueling()
                self.activeVehicle = nil
                if Glide.RefuelerHUDWeapon == self then
                    Glide.RefuelerHUDWeapon = nil
                end
            end

            ---@realm shared
            function SWEP:Holster()
                self.activeVehicle = nil
                if Glide.RefuelerHUDWeapon == self then
                    Glide.RefuelerHUDWeapon = nil
                end
            end

            ---@realm client
            if CLIENT then
                local function draw()
                    local wep = Glide.RefuelerHUDWeapon
                    if not IsValid(wep) then return end

                    local veh = wep.activeVehicle
                end
            end
            "#,
            ),
        ]);
        let file_id = file_ids[1];

        let assigned_member_id = {
            let semantic_model = ws
                .analysis
                .compilation
                .get_semantic_model(file_id)
                .expect("expected semantic model");
            let active_vehicle_assignment = semantic_model
                .get_root()
                .descendants::<glua_parser::LuaAssignStat>()
                .find_map(|assign_stat| {
                    let (vars, exprs) = assign_stat.get_var_and_expr_list();
                    vars.iter()
                        .enumerate()
                        .find(|(_, var)| var.syntax().text() == "self.activeVehicle")
                        .and_then(|(idx, var)| {
                            let expr = exprs.get(idx)?;
                            (expr.syntax().text() == "vehicle").then(|| var.clone())
                        })
                })
                .expect("expected self.activeVehicle = vehicle assignment");

            LuaMemberId::new(active_vehicle_assignment.get_syntax_id(), file_id)
        };
        let assigned_type = ws
            .get_db_mut()
            .get_type_index()
            .get_type_cache(&assigned_member_id.into())
            .expect("expected activeVehicle assignment type")
            .as_type()
            .clone();
        let assigned_desc = ws.humanize_type(assigned_type);
        assert_that!(
            assigned_desc.as_str(),
            contains_substring("base_glide"),
            "self.activeVehicle = vehicle cache should keep the resolved guarded vehicle type, got {}",
            assigned_desc
        );
        assert_that!(
            assigned_desc.as_str(),
            not(any!(eq("nil"), eq("any"), eq("unknown"))),
            "self.activeVehicle = vehicle cache should not fossilize a weak early RHS type"
        );

        let rhs_type = local_assignment_value_type(&mut ws, file_id, "veh");
        let rhs_desc = ws.humanize_type(rhs_type);
        assert_that!(
            rhs_desc.as_str(),
            all!(contains_substring("base_glide"), contains_substring("?")),
            "activeVehicle read should union all visible assignments, got {}",
            rhs_desc
        );

        let semantic_decl = {
            let semantic_model = ws
                .analysis
                .compilation
                .get_semantic_model(file_id)
                .expect("expected semantic model");
            let active_vehicle_expr = semantic_model
                .get_root()
                .descendants::<glua_parser::LuaIndexExpr>()
                .find(|index_expr| index_expr.syntax().text() == "wep.activeVehicle")
                .expect("expected activeVehicle index expression");
            semantic_model
                .get_semantic_info(active_vehicle_expr.syntax().clone().into())
                .expect("expected semantic info for activeVehicle")
                .semantic_decl
                .expect("expected activeVehicle semantic declaration")
        };
        let decl_type = match semantic_decl {
            crate::LuaSemanticDeclId::Member(member_id) => ws
                .get_db_mut()
                .get_type_index()
                .get_type_cache(&member_id.into())
                .expect("expected member type")
                .as_type()
                .clone(),
            other => panic!("expected member semantic declaration, got {other:?}"),
        };
        let decl_desc = ws.humanize_type(decl_type);
        assert_that!(
            decl_desc.as_str(),
            not(eq("nil")),
            "activeVehicle semantic declaration should not point at a nil-only assignment"
        );

        let veh_type = local_name_type(&mut ws, file_id, "veh");
        let veh_desc = ws.humanize_type(veh_type);
        assert_that!(
            veh_desc.as_str(),
            all!(contains_substring("base_glide"), contains_substring("?")),
            "local veh should retain the activeVehicle assignment union, got {}",
            veh_desc
        );
    }

    #[gtest]
    fn test_scripted_weapon_same_function_member_read_uses_latest_assignment() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/weapons/glide_refueler_base.lua",
            r#"
            ---@class ENT
            ---@class base_glide: ENT

            function SWEP:Test(vehicle)
                self.activeVehicle = nil
                self.activeVehicle = vehicle
                local veh = self.activeVehicle
            end
            "#,
        );

        let veh_type = local_name_type(&mut ws, file_id, "veh");
        let veh_desc = ws.humanize_type(veh_type);
        assert_that!(
            veh_desc.as_str(),
            not(contains_substring("nil")),
            "same-function field flow should keep the latest assignment precise, got {}",
            veh_desc
        );
    }

    #[gtest]
    fn test_scripted_weapon_field_semantic_decl_prefers_informative_history() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);

        let file_ids = ws.def_files(vec![
            (
                "lua/autorun/sh_glide.lua",
                r#"
                ---@class Glide
                Glide = Glide or {}
                "#,
            ),
            (
                "lua/weapons/glide_refueler_base.lua",
                r#"
            ---@class ENT
            ---@class base_glide: ENT

            function SWEP:Deploy()
                Glide.RefuelerHUDWeapon = self
            end

            ---@realm shared
            function SWEP:Reset()
                self.activeVehicle = nil
            end

            ---@param vehicle base_glide
            function SWEP:StartFueling(vehicle)
                if CLIENT then return end
                if not IsValid(vehicle) then return end
                self.activeVehicle = vehicle
            end

            ---@realm client
            if CLIENT then
                local function draw()
                    local wep = Glide.RefuelerHUDWeapon
                    if not IsValid(wep) then return end

                    local veh = wep.activeVehicle
                end
            end
            "#,
            ),
        ]);
        let file_id = file_ids[1];

        let semantic_decl = {
            let semantic_model = ws
                .analysis
                .compilation
                .get_semantic_model(file_id)
                .expect("expected semantic model");
            let active_vehicle_expr = semantic_model
                .get_root()
                .descendants::<glua_parser::LuaIndexExpr>()
                .find(|index_expr| index_expr.syntax().text() == "wep.activeVehicle")
                .expect("expected activeVehicle index expression");
            semantic_model
                .get_semantic_info(active_vehicle_expr.syntax().clone().into())
                .expect("expected semantic info for activeVehicle")
                .semantic_decl
                .expect("expected activeVehicle semantic declaration")
        };
        let decl_type = match semantic_decl {
            crate::LuaSemanticDeclId::Member(member_id) => ws
                .get_db_mut()
                .get_type_index()
                .get_type_cache(&member_id.into())
                .expect("expected member type")
                .as_type()
                .clone(),
            other => panic!("expected member semantic declaration, got {other:?}"),
        };
        let decl_desc = ws.humanize_type(decl_type);
        assert_that!(
            decl_desc.as_str(),
            contains_substring("base_glide"),
            "semantic declaration should prefer informative assignment history, got {}",
            decl_desc
        );
    }

    #[test]
    fn test_gmod_dynamic_initializer_fallback_respects_explicit_unknown_annotation() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def(
            r#"
            ---@class Angle
            local Angle = {}

            ---@class Entity
            local Entity = {}

            ---@return Angle
            function Entity:GetAngles() end

            local veh = Entity

            ---@type unknown
            local vehAng = veh:GetAngles()
            print(vehAng)
        "#,
        );

        let annotated_type = local_name_type(&mut ws, file_id, "vehAng");
        assert_that!(
            ws.humanize_type_detailed(annotated_type).as_str(),
            eq("unknown"),
            "explicit unknown annotations should not be replaced by dynamic initializer fallback"
        );
    }

    #[test]
    fn test_isvalid_guard_promotes_hard_nil_local_to_any_after_early_return() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def(
            r#"
            local veh = nil
            if not IsValid(veh) then return end

            local narrowed = veh
            print(narrowed)
            "#,
        );

        let narrowed_type = local_name_type(&mut ws, file_id, "narrowed");
        assert_eq!(
            narrowed_type,
            LuaType::Any,
            "a successful IsValid guard proves even a previously hard-nil local is usable"
        );
    }

    #[test]
    fn test_isvalid_alias_guard_promotes_hard_nil_local_to_any_after_early_return() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = ws.get_emmyrc();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def(
            r#"
            local iv = IsValid
            local veh = nil
            if not iv(veh) then return end

            local narrowed = veh
            print(narrowed)
            "#,
        );

        let narrowed_type = local_name_type(&mut ws, file_id, "narrowed");
        assert_eq!(
            narrowed_type,
            LuaType::Any,
            "an aliased successful IsValid guard should also promote a hard-nil local"
        );
    }

    #[gtest]
    fn test_gmod_self_index_local_unknown_reassignment_does_not_fall_back_to_initializer() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")];
        ws.update_emmyrc(emmyrc);

        let code = r#"
            ---@return unknown
            function MakeUnknown() end

            function ENT:Initialize()
                self.sounds = {}
            end

            function ENT:Test()
                local sounds = self.sounds
                sounds = MakeUnknown()
                local after = sounds
            end
        "#;

        let path = "lua/entities/base_glide/cl_init.lua";
        ws.def_file(path, code);
        let uri = ws.virtual_url_generator.new_uri(path);
        let file_id = ws
            .analysis
            .compilation
            .get_db()
            .get_vfs()
            .get_file_id(&uri)
            .expect("expected cl_init file id");

        let reassignment_type = assign_value_type(&mut ws, file_id, "sounds");
        assert_that!(
            ws.humanize_type_detailed(reassignment_type).as_str(),
            eq("unknown"),
            "the reassignment itself should be unknown"
        );

        let after_type = local_name_type(&mut ws, file_id, "after");
        assert_that!(
            ws.humanize_type_detailed(after_type).as_str(),
            eq("unknown"),
            "a later unknown assignment should remain the current flow result"
        );
    }

    #[gtest]
    fn test_gmod_self_field_local_alias_preserves_known_optional_panel_type() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.infer_dynamic_fields = true;
        ws.update_emmyrc(emmyrc);
        ws.analysis
            .diagnostic
            .enable_only(DiagnosticCode::UncheckedNilAccess);

        let engine_code = r#"
            local WebAudio = Glide.WebAudio

            function WebAudio:Enable()
                local panel = self.panel
                if IsValid(panel) then
                    return
                end

                panel = vgui.Create("HTML", GetHUDPanel())
                self.panel = panel
            end

            function WebAudio:Disable()
                self.panel = nil
            end

            function WebAudio:OnHTMLReady()
                local readyPanel = self.panel
                readyPanel:NewObject("webaudio")
            end
            "#;
        ws.def_files(vec![
            ("lua/glide/client/engine_stream_webaudio.lua", engine_code),
            (
                "lua/autorun/sh_glide.lua",
                r#"
                Glide = {}

                if CLIENT then
                    ---@class WebAudio
                    Glide.WebAudio = Glide.WebAudio or {}
                end
                "#,
            ),
            (
                "lua/includes/gmod_vgui.lua",
                r#"
                ---@class Panel
                ---@class HTML : Panel
                local HTML = {}
                function HTML:NewObject(name) end

                vgui = {}
                ---@generic T : Panel
                ---@param className `T`
                ---@return T
                function vgui.Create(className, parent) end

                ---@return Panel
                function GetHUDPanel() end
                "#,
            ),
        ]);
        let uri = ws
            .virtual_url_generator
            .new_uri("lua/glide/client/engine_stream_webaudio.lua");
        let file_id = ws
            .analysis
            .compilation
            .get_db()
            .get_vfs()
            .get_file_id(&uri)
            .expect("expected WebAudio file id");

        let create_panel_type = assign_value_type(&mut ws, file_id, "panel");
        let create_panel_display = ws.humanize_type(create_panel_type);
        let self_panel_assignment_type = assign_value_type(&mut ws, file_id, "self.panel");
        let self_panel_assignment_display = ws.humanize_type(self_panel_assignment_type);
        let panel_rhs_type = local_assignment_value_type(&mut ws, file_id, "readyPanel");
        let panel_rhs_display = ws.humanize_type(panel_rhs_type);
        let panel_type = local_name_type(&mut ws, file_id, "readyPanel");
        let panel_display = ws.humanize_type(panel_type);
        assert_that!(
            panel_display.as_str(),
            contains_substring("HTML"),
            "local panel should preserve the known HTML half of self.panel, got {panel_display}; RHS was {panel_rhs_display}; create call was {create_panel_display}; self.panel assignment was {self_panel_assignment_display}"
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let unchecked_nil_access = diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.code == Some(NumberOrString::String("unchecked-nil-access".to_string()))
            })
            .map(|diagnostic| diagnostic.message.clone())
            .collect::<Vec<_>>();

        assert_that!(
            unchecked_nil_access,
            is_empty(),
            "HTML? should not degrade to opaque any? and produce unchecked-nil-access"
        );
    }

    /// Verify that a method defined in two entity files (init.lua + shared.lua CLIENT block)
    /// is stored as Many with both member IDs in the member index.
    #[gtest]
    fn test_dual_realm_ent_method_stores_both_members() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/dual_realm_ent/init.lua",
            r#"
            function ENT:GetFuelAmountUnits()
                return self.fuelAmount or 0
            end
        "#,
        );

        ws.def_file(
            "lua/entities/dual_realm_ent/shared.lua",
            r#"
            if CLIENT then
                function ENT:GetFuelAmountUnits()
                    return self:GetNWFloat("fuel", 0)
                end
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("dual_realm_ent");
        let owner = LuaMemberOwner::Type(class_id.clone());

        // Get all members of the class
        let members = db.get_member_index().get_members(&owner);
        let fuel_members: Vec<_> = members
            .unwrap_or_default()
            .into_iter()
            .filter(|m| {
                m.get_key()
                    .get_name()
                    .is_some_and(|n| n == "GetFuelAmountUnits")
            })
            .collect();

        // We expect BOTH definitions (init.lua server + shared.lua client)
        assert!(
            fuel_members.len() >= 2,
            "Expected at least 2 member definitions for GetFuelAmountUnits on dual_realm_ent, got {} from files {:?}",
            fuel_members.len(),
            fuel_members
                .iter()
                .map(|m| m.get_file_id())
                .collect::<Vec<_>>()
        );
    }

    #[gtest]
    fn test_derma_define_control_registers_global() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/test_derma_panel.lua",
            r#"
            local PANEL = {}

            function PANEL:Init()
            end

            function PANEL:Paint(w, h)
            end

            derma.DefineControl("TestDermaPanel", "Description", PANEL, "DFrame")

            -- TestPanel should be recognized as a valid global
            local x = TestDermaPanel  -- should not report undefined global
        "#,
        );

        let db = ws.get_db_mut();

        // Verify the class type was created
        let class_id = LuaTypeDeclId::global("TestDermaPanel");
        let class_decl = db.get_type_index().get_type_decl(&class_id);
        assert!(
            class_decl.is_some(),
            "TestDermaPanel class should be created"
        );

        // Verify the global was registered
        let global_decl_ids = db.get_global_index().get_global_decl_ids("TestDermaPanel");
        assert!(
            global_decl_ids.is_some(),
            "TestDermaPanel should be registered as a global"
        );
        assert!(
            !global_decl_ids.unwrap().is_empty(),
            "TestDermaPanel global decl should exist"
        );
    }

    /// Regression test: SWEP field assignments in scope files (without an explicit `local SWEP
    /// = {}` declaration) should be owned by the per-entity class type, not by the shared
    /// `GlobalPath("SWEP")` owner. Before the fix, all weapon files that assigned the same field
    /// via `SWEP.Field = …` would accumulate that field on the global SWEP path, causing
    /// cross-contamination between different weapons' member types (e.g. `number|IMaterial`).
    #[gtest]
    fn test_swep_members_scoped_to_weapon_class_not_global_swep() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/weapons/weapon_a/shared.lua",
                r#"
                SWEP.UniqueFieldA = 1
                "#,
            ),
            (
                "lua/weapons/weapon_b/shared.lua",
                r#"
                SWEP.UniqueFieldB = 2
                "#,
            ),
        ]);

        let db = ws.get_db_mut();

        // Each weapon's class should own only its own field
        let weapon_a_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("weapon_a")))
            .expect("weapon_a should have members");
        let weapon_a_names: Vec<_> = weapon_a_members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            weapon_a_names.contains(&"UniqueFieldA".to_string()),
            "weapon_a should have UniqueFieldA, got {weapon_a_names:?}"
        );
        assert!(
            !weapon_a_names.contains(&"UniqueFieldB".to_string()),
            "weapon_a should NOT contain UniqueFieldB from weapon_b, got {weapon_a_names:?}"
        );

        let weapon_b_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(LuaTypeDeclId::global("weapon_b")))
            .expect("weapon_b should have members");
        let weapon_b_names: Vec<_> = weapon_b_members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            weapon_b_names.contains(&"UniqueFieldB".to_string()),
            "weapon_b should have UniqueFieldB, got {weapon_b_names:?}"
        );
        assert!(
            !weapon_b_names.contains(&"UniqueFieldA".to_string()),
            "weapon_b should NOT contain UniqueFieldA from weapon_a, got {weapon_b_names:?}"
        );

        // Neither field should be left on the global SWEP path after migration
        let global_swep_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::GlobalPath(GlobalId::new("SWEP")));
        if let Some(global_members) = global_swep_members {
            let global_names: Vec<_> = global_members
                .iter()
                .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
                .collect();
            assert!(
                !global_names.contains(&"UniqueFieldA".to_string()),
                "UniqueFieldA should NOT remain on GlobalPath(SWEP), got {global_names:?}"
            );
            assert!(
                !global_names.contains(&"UniqueFieldB".to_string()),
                "UniqueFieldB should NOT remain on GlobalPath(SWEP), got {global_names:?}"
            );
        }
    }

    #[gtest]
    fn test_swep_wepselecticon_does_not_union_with_global_annotation_on_first_analysis() {
        let mut ws = VirtualWorkspace::new_with_init_std_lib();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::ParamTypeMismatch);

        let file_id = ws.def_file(
            "lua/weapons/weapon_mad_deagle/shared.lua",
            r#"
            if ( SERVER ) then return end

            SWEP.WepSelectIcon = Material("swepicons/cityrp_deagle.png")

            function SWEP:DrawWeaponSelection( x, y, w, h, a )
                surface.SetMaterial( self.WepSelectIcon )
            end
            "#,
        );

        let diagnostics = ws
            .analysis
            .diagnose_file(file_id, CancellationToken::new())
            .unwrap_or_default();
        let param_type_mismatch_code = Some(NumberOrString::String(
            DiagnosticCode::ParamTypeMismatch.get_name().to_string(),
        ));
        let mismatch_diags: Vec<_> = diagnostics
            .iter()
            .filter(|diag| diag.code == param_type_mismatch_code)
            .collect();
        assert!(
            mismatch_diags.is_empty(),
            "unexpected param-type-mismatch diagnostics: {mismatch_diags:?}"
        );

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let swep_name_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|name_expr| {
                name_expr
                    .get_name_token()
                    .is_some_and(|token| token.get_name_text() == "SWEP")
            })
            .expect("expected SWEP name expression");
        let swep_token = swep_name_expr
            .get_name_token()
            .expect("expected SWEP token");
        let swep_semantic = semantic_model
            .get_semantic_info(swep_token.syntax().clone().into())
            .expect("expected semantic info for SWEP");
        assert_eq!(
            swep_semantic.typ,
            LuaType::Def(LuaTypeDeclId::global("weapon_mad_deagle")),
            "expected SWEP to resolve to the scoped weapon class"
        );
    }

    #[gtest]
    fn test_scripted_class_cross_file_assignments_survive_single_file_reindex() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let shared_path = "lua/weapons/weapon_mad_deagle/shared.lua";
        let shared_text = r#"
            SWEP.WepSelectIcon = "material"
        "#;

        ws.def_files(vec![
            (shared_path, shared_text),
            (
                "lua/weapons/weapon_mad_deagle/cl_init.lua",
                r#"
                include("shared.lua")
                SWEP.WepSelectIcon = 1
                "#,
            ),
        ]);

        let shared_uri = ws.virtual_url_generator.new_uri(shared_path);
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("weapon_mad_deagle"));
        let key = LuaMemberKey::Name("WepSelectIcon".into());

        let member_item_before = ws
            .analysis
            .compilation
            .get_db()
            .get_member_index()
            .get_member_item(&owner, &key)
            .expect("expected WepSelectIcon member item before edit");
        let member_count_before = match member_item_before {
            crate::LuaMemberIndexItem::One(_) => 1,
            crate::LuaMemberIndexItem::Many(ids) => ids.len(),
        };
        assert_eq!(
            member_count_before, 1,
            "expected latest assignment to replace previous assignment"
        );

        ws.analysis
            .update_file_by_uri(&shared_uri, Some(format!("\n{shared_text}")))
            .expect("shared file should update");

        let member_item_after = ws
            .analysis
            .compilation
            .get_db()
            .get_member_index()
            .get_member_item(&owner, &key)
            .expect("expected WepSelectIcon member item after edit");
        let member_count_after = match member_item_after {
            crate::LuaMemberIndexItem::One(_) => 1,
            crate::LuaMemberIndexItem::Many(ids) => ids.len(),
        };
        assert_eq!(
            member_count_after, 1,
            "single-file reindex should retain the latest assignment"
        );
    }

    // ---- Gamemode scoped-class tests -------------------------------------------------
    //
    // Garry's Mod stores gamemode tables at `_G["gamemode_<folder>"]` at runtime, and
    // `DEFINE_BASECLASS("gamemode_sandbox")` references that runtime name. The built-in
    // `gamemodes` scoped-class scope (with `class_name_prefix = "gamemode_"`) is what
    // gives the `GM` table in a gamemode file a class type.

    #[gtest]
    fn test_gamemode_scope_assigns_prefixed_class_type_to_gm() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "gamemodes/sandbox/gamemode/init.lua",
            r#"
            function GM:SetupMove(ply, mv, cmd) end
        "#,
        );

        // The scoped-class detection must produce a `gamemode_sandbox` class (folder
        // name `sandbox` with the `gamemode_` prefix), not plain `sandbox`.
        let info = ws
            .get_db_mut()
            .get_gmod_infer_index()
            .get_scoped_class_info(&file_id)
            .cloned()
            .expect("expected scoped-class info for gamemode file");
        assert_eq!(info.class_name, "gamemode_sandbox");
        assert_eq!(info.global_name, "GM");

        let class_id = LuaTypeDeclId::global("gamemode_sandbox");
        assert!(
            ws.get_db_mut()
                .get_type_index()
                .get_type_decl(&class_id)
                .is_some(),
            "expected a class decl for gamemode_sandbox"
        );
    }

    #[gtest]
    fn test_gamemode_scope_nested_entities_resolve_to_entity_scope() {
        // A file under `gamemodes/<name>/gamemode/entities/<ent>/...` must be detected
        // as the ENT scope (class name = ent folder), not as the outer gamemode scope.
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "gamemodes/darkrp/gamemode/entities/entities/my_ent/shared.lua",
            r#"
            ENT.Type = "anim"
        "#,
        );

        let info = ws
            .get_db_mut()
            .get_gmod_infer_index()
            .get_scoped_class_info(&file_id)
            .cloned()
            .expect("expected scoped-class info for nested entity file");
        assert_eq!(info.class_name, "my_ent");
        assert_eq!(info.global_name, "ENT");
    }

    #[gtest]
    fn test_derive_gamemode_self_base_field_resolves_without_undefined_field() {
        // End-to-end repro of the user-reported DarkRP scenario:
        //   DEFINE_BASECLASS("gamemode_sandbox")
        //   function GM:SetupMove(...) return self.Sandbox.SetupMove(...) end
        //
        // Two things must hold:
        //   1. `self.Sandbox` resolves to the `gamemode_sandbox` class type
        //      (so hover / completion / goto all work), not to "unknown".
        //   2. No `UndefinedField` diagnostic fires on `self.Sandbox.SetupMove`.
        //
        // The alias is synthesized by the analyzer from `DEFINE_BASECLASS` +
        // the scope's `classNamePrefix` (`gamemode_` → stripped short name
        // `sandbox` → capitalized `Sandbox`), so the user does NOT need to
        // write `GM.Sandbox = BaseClass` themselves for it to work.
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        ws.analysis
            .diagnostic
            .enable_only(DiagnosticCode::UndefinedField);

        // Base gamemode: defines `SetupMove` on its GM (→ class `gamemode_sandbox`).
        ws.def_file(
            "gamemodes/sandbox/gamemode/player.lua",
            r#"
            function GM:SetupMove(ply, mv, cmd) end
        "#,
        );

        // Deriving gamemode init: just declares the base class. Notably it does
        // NOT contain `GM.Sandbox = BaseClass` — the analyzer must synthesize
        // the alias purely from `DEFINE_BASECLASS` metadata.
        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DEFINE_BASECLASS("gamemode_sandbox")
        "#,
        );

        // Method on the deriving gamemode's GM that reaches into `self.Sandbox`.
        let call_file = ws.def_file(
            "gamemodes/darkrp/gamemode/modules/base/sh_gamemode_functions.lua",
            r#"
            function GM:SetupMove(ply, mv, cmd)
                return self.Sandbox.SetupMove(self, ply, mv, cmd)
            end
        "#,
        );

        // Assertion 1: `Sandbox` member was synthesized on `gamemode_darkrp`
        // and points at `LuaType::Ref(gamemode_sandbox)`.
        let derived_id = LuaTypeDeclId::global("gamemode_darkrp");
        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(derived_id);
        let key = LuaMemberKey::Name("Sandbox".into());
        let member_item = db
            .get_member_index()
            .get_member_item(&owner, &key)
            .expect("expected synthesized `Sandbox` alias member on gamemode_darkrp");
        let member_id = match member_item {
            crate::LuaMemberIndexItem::One(id) => *id,
            crate::LuaMemberIndexItem::Many(ids) => *ids
                .first()
                .expect("expected at least one member id for Sandbox alias"),
        };
        let bound = db
            .get_type_index()
            .get_type_cache(&member_id.into())
            .expect("synthesized alias should have a bound type");
        assert!(
            matches!(bound.as_type(), LuaType::Ref(id) if id == &LuaTypeDeclId::global("gamemode_sandbox")),
            "Sandbox alias should be typed as gamemode_sandbox, got {:?}",
            bound.as_type()
        );

        // Assertion 2: no UndefinedField diagnostic on `self.Sandbox.SetupMove`.
        let diagnostics = ws
            .analysis
            .diagnose_file(call_file, CancellationToken::new())
            .unwrap_or_default();
        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        let offending = diagnostics
            .iter()
            .filter(|d| d.code == undefined_field_code)
            .collect::<Vec<_>>();
        assert!(
            offending.is_empty(),
            "unexpected undefined-field diagnostics on self.Sandbox.SetupMove: {offending:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_parent_alias_skipped_when_scope_has_no_prefix() {
        // Scopes without `classNamePrefix` (entities, weapons, etc.) should not
        // get a parent-name alias — the short-name convention is gamemode-
        // specific and synthesizing a bogus field for e.g. ENT would be wrong.
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/entities/base_thing/init.lua",
            r#"
            ENT.Type = "anim"
        "#,
        );

        // Derived entity that declares a base class via DEFINE_BASECLASS. The
        // scope's `classNamePrefix` is absent, so we must not synthesize any
        // alias member (in particular no `Base_thing` / `Basething`).
        ws.def_file(
            "lua/entities/derived_thing/init.lua",
            r#"
            DEFINE_BASECLASS("base_thing")
        "#,
        );

        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("derived_thing"));
        let members = db
            .get_member_index()
            .get_members(&owner)
            .unwrap_or_default();
        let member_names: Vec<String> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(ToString::to_string))
            .collect();
        assert!(
            !member_names.iter().any(
                |n| n.eq_ignore_ascii_case("base_thing") || n.eq_ignore_ascii_case("basething")
            ),
            "no parent-name alias should be synthesized when scope lacks classNamePrefix, got {member_names:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_parent_alias_skipped_when_prefix_mismatches() {
        // If the base class name does not start with the scope's prefix, skip
        // synthesis silently — the class belongs to a different family and
        // fabricating an alias would be misleading.
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "gamemodes/weirdgame/gamemode/init.lua",
            r#"
            DEFINE_BASECLASS("not_a_gamemode_class")
        "#,
        );

        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_weirdgame"));
        let members = db
            .get_member_index()
            .get_members(&owner)
            .unwrap_or_default();
        let member_names: Vec<String> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(ToString::to_string))
            .collect();
        assert!(
            !member_names.iter().any(|n| n == "Not_a_gamemode_class"),
            "no alias should be synthesized when prefix does not match the base name, got {member_names:?}"
        );
    }

    #[gtest]
    fn test_define_baseclass_parent_alias_does_not_shadow_user_assignment() {
        // If the user explicitly writes `GM.Sandbox = BaseClass` themselves,
        // that real assignment must take precedence. We should not add a
        // duplicate synthesized decl that could cause ambiguous hover/goto.
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "gamemodes/sandbox/gamemode/player.lua",
            r#"
            function GM:SetupMove(ply, mv, cmd) end
        "#,
        );

        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DEFINE_BASECLASS("gamemode_sandbox")
            GM.Sandbox = BaseClass
        "#,
        );

        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_darkrp"));
        let key = LuaMemberKey::Name("Sandbox".into());
        let member_item = db
            .get_member_index()
            .get_member_item(&owner, &key)
            .expect("expected a Sandbox member (user-written) on gamemode_darkrp");
        // Exactly one member id — the user's explicit assignment. No synthetic
        // duplicate should have been added on top of it.
        let count = match member_item {
            crate::LuaMemberIndexItem::One(_) => 1,
            crate::LuaMemberIndexItem::Many(ids) => ids.len(),
        };
        assert_eq!(
            count, 1,
            "explicit user-written `GM.Sandbox = BaseClass` should be the only member decl for `Sandbox`, got {count} entries"
        );
    }

    #[gtest]
    fn test_define_baseclass_phantom_gamemode_base_inherits_gm() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DEFINE_BASECLASS("gamemode_sandbox")
            GM.Sandbox = BaseClass
        "#,
        );

        let call_file = ws.def_file(
            "gamemodes/darkrp/gamemode/modules/base/sh_gamemode_functions.lua",
            r#"
            function GM:SetupMove(ply, mv, cmd)
                return self.Sandbox.SetupMove(self, ply, mv, cmd)
            end
        "#,
        );

        let db = ws.get_db_mut();
        let base_class_id = LuaTypeDeclId::global("gamemode_sandbox");
        assert!(db.get_type_index().get_type_decl(&base_class_id).is_some());
        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&base_class_id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();
        assert!(super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("GM"))));

        assert!(!index_expr_type(&mut ws, call_file, "self.Sandbox.SetupMove").is_unknown());
    }

    #[gtest]
    fn test_derive_gamemode_equiv_to_define_baseclass() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DeriveGamemode("sandbox")
            DEFINE_BASECLASS("gamemode_sandbox")
            GM.Sandbox = BaseClass
        "#,
        );

        let super_types = super_types_of(&mut ws, "gamemode_darkrp");
        assert!(super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"))));

        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_darkrp"));
        let key = LuaMemberKey::Name("Sandbox".into());
        let member_item = db.get_member_index().get_member_item(&owner, &key).unwrap();
        let count = match member_item {
            crate::LuaMemberIndexItem::One(_) => 1,
            crate::LuaMemberIndexItem::Many(ids) => ids.len(),
        };
        assert_eq!(count, 1);
    }

    #[gtest]
    fn test_derive_gamemode_and_define_baseclass_together() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DeriveGamemode("sandbox")
            DEFINE_BASECLASS("gamemode_sandbox")
            GM.Sandbox = BaseClass
        "#,
        );

        let super_types = super_types_of(&mut ws, "gamemode_darkrp");
        let sandbox_ref = LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"));
        assert!(super_types.iter().filter(|ty| *ty == &sandbox_ref).count() == 1);

        let db = ws.get_db_mut();
        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_darkrp"));
        let key = LuaMemberKey::Name("Sandbox".into());
        assert!(
            db.get_member_index()
                .get_member_item(&owner, &key)
                .is_some()
        );
    }

    #[gtest]
    fn test_derive_gamemode_ignores_non_literal_arg() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            local x = "sandbox"
            DeriveGamemode(x)
        "#,
        );

        let super_types = super_types_of(&mut ws, "gamemode_darkrp");
        assert!(super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("GM"))));
        assert!(!super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"))));
        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_some()
        );
    }

    #[gtest]
    fn test_derive_gamemode_does_not_double_prefix() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DeriveGamemode("gamemode_sandbox")
        "#,
        );

        let super_types = super_types_of(&mut ws, "gamemode_darkrp");
        assert!(super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"))));
        assert!(!super_types.contains(&LuaType::Ref(LuaTypeDeclId::global(
            "gamemode_gamemode_sandbox"
        ))));
    }

    #[gtest]
    fn test_derive_gamemode_outside_gamemode_scope_is_ignored() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/autorun/test.lua",
            r#"
            DeriveGamemode("sandbox")
        "#,
        );

        assert!(
            ws.get_db_mut()
                .get_type_index()
                .get_type_decl(&LuaTypeDeclId::global("gamemode_sandbox"))
                .is_none()
        );
        assert!(
            ws.get_db_mut()
                .get_gmod_class_metadata_index()
                .get_file_metadata(&file_id)
                .is_some()
        );
    }

    #[gtest]
    fn test_phantom_gamemode_base_collects_locations_from_multiple_files() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        let file_a = ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"DEFINE_BASECLASS("gamemode_sandbox")"#,
        );
        let file_b = ws.def_file(
            "gamemodes/darkrp/gamemode/cl_init.lua",
            r#"DEFINE_BASECLASS("gamemode_sandbox")"#,
        );

        let db = ws.get_db_mut();
        let decl = db
            .get_type_index()
            .get_type_decl(&LuaTypeDeclId::global("gamemode_sandbox"))
            .expect("expected phantom gamemode_sandbox decl");
        let locations: Vec<_> = decl.get_locations().iter().map(|loc| loc.file_id).collect();

        assert!(locations.contains(&file_a));
        assert!(locations.contains(&file_b));

        let super_types: Vec<_> = db
            .get_type_index()
            .get_super_types_iter(&LuaTypeDeclId::global("gamemode_sandbox"))
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default();
        assert!(super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("GM"))));
    }

    #[gtest]
    fn test_derive_wins_for_inheritance_while_define_wins_for_alias() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);
        add_gamemode_gm_library(&mut ws);

        let file_id = ws.def_file(
            "gamemodes/darkrp/gamemode/init.lua",
            r#"
            DeriveGamemode("sandbox")
            DEFINE_BASECLASS("gamemode_other")
            GM.Other = BaseClass
        "#,
        );

        let derived_supers = super_types_of(&mut ws, "gamemode_darkrp");
        assert!(derived_supers.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"))));
        assert!(!derived_supers.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_other"))));

        let semantic_model = ws
            .analysis
            .compilation
            .get_semantic_model(file_id)
            .expect("expected semantic model");
        let baseclass_expr = semantic_model
            .get_root()
            .descendants::<LuaNameExpr>()
            .find(|expr| expr.get_name_text().as_deref() == Some("BaseClass"))
            .expect("expected BaseClass expr");
        let baseclass_token = baseclass_expr
            .get_name_token()
            .expect("expected BaseClass token");
        let baseclass_info = semantic_model
            .get_semantic_info(baseclass_token.syntax().clone().into())
            .expect("expected BaseClass semantic info");
        assert_eq!(
            baseclass_info.typ,
            LuaType::Ref(LuaTypeDeclId::global("gamemode_other"))
        );

        let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_darkrp"));
        let member_item = ws
            .get_db_mut()
            .get_member_index()
            .get_member_item(&owner, &LuaMemberKey::Name("Other".into()))
            .expect("expected Other alias");
        let count = match member_item {
            crate::LuaMemberIndexItem::One(_) => 1,
            crate::LuaMemberIndexItem::Many(ids) => ids.len(),
        };
        assert_eq!(count, 1);
    }

    // --- scripted_ents.GetMember delegation tests ---

    #[gtest]
    fn test_getmember_delegation_copies_network_vars_to_delegating_entity() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        // Simulate the real edit_sky / env_skypaint pattern
        ws.def_files(vec![
            (
                "lua/entities/env_skypaint/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "TopColorR")
                    self:NetworkVar("Float", 0, "TopColorG")
                    self:NetworkVar("Float", 0, "TopColorB")
                end
            "#,
            ),
            (
                "lua/entities/edit_sky/init.lua",
                r#"
                function ENT:SetupDataTables()
                    local SetupDataTables = scripted_ents.GetMember("env_skypaint", "SetupDataTables")
                    SetupDataTables(self)
                end
            "#,
            ),
        ]);

        // Verify the delegating entity has the NetworkVar members from the target
        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("edit_sky");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on edit_sky class");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetTopColorR".to_string()),
            "missing GetTopColorR in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetTopColorR".to_string()),
            "missing SetTopColorR in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetTopColorG".to_string()),
            "missing GetTopColorG in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetTopColorG".to_string()),
            "missing SetTopColorG in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetTopColorB".to_string()),
            "missing GetTopColorB in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetTopColorB".to_string()),
            "missing SetTopColorB in {member_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_no_undefined_field_diagnostics() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);
        ws.enable_check(DiagnosticCode::UndefinedField);

        ws.def_files(vec![
            (
                "lua/entities/env_skypaint/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Int", 0, "SkyTopColor")
                end
            "#,
            ),
            (
                "lua/entities/edit_sky/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    local SetupDataTables = scripted_ents.GetMember("env_skypaint", "SetupDataTables")
                    SetupDataTables(self)
                end
            "#,
            ),
            (
                "lua/entities/edit_sky/cl_init.lua",
                r#"
                function ENT:Think()
                    local color = self:GetSkyTopColor()
                    self:SetSkyTopColor(color + 1)
                end
            "#,
            ),
        ]);

        let target_uri = ws
            .virtual_url_generator
            .new_uri("lua/entities/edit_sky/cl_init.lua");
        let target_file_id = ws
            .analysis
            .get_file_id(&target_uri)
            .expect("expected file id");
        let diagnostics = ws
            .analysis
            .diagnose_file(target_file_id, CancellationToken::new())
            .unwrap_or_default();

        let undefined_field_code = Some(NumberOrString::String(
            DiagnosticCode::UndefinedField.get_name().to_string(),
        ));
        assert!(
            diagnostics
                .iter()
                .all(|diag| diag.code != undefined_field_code),
            "unexpected undefined-field diagnostics: {diagnostics:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_with_direct_call_pattern() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        // Test direct call: scripted_ents.GetMember("class", "method")(self)
        ws.def_files(vec![
            (
                "lua/entities/target_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("String", "Label")
                end
            "#,
            ),
            (
                "lua/entities/delegating_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    scripted_ents.GetMember("target_ent", "SetupDataTables")(self)
                end
            "#,
            ),
        ]);

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("delegating_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetLabel".to_string()),
            "missing GetLabel in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetLabel".to_string()),
            "missing SetLabel in {member_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_preserves_target_entity_network_vars() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        // Verify the target entity still has its own NetworkVar members
        ws.def_files(vec![
            (
                "lua/entities/target_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "Speed")
                end
            "#,
            ),
            (
                "lua/entities/delegating_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    local f = scripted_ents.GetMember("target_ent", "SetupDataTables")
                    f(self)
                end
            "#,
            ),
        ]);

        let db = ws.get_db_mut();

        // Target should still have its members
        let target_class_id = LuaTypeDeclId::global("target_ent");
        let target_owner = LuaMemberOwner::Type(target_class_id);
        let target_members = db
            .get_member_index()
            .get_members(&target_owner)
            .expect("expected members on target_ent");
        let target_names: Vec<_> = target_members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();
        assert!(
            target_names.contains(&"GetSpeed".to_string()),
            "target_ent missing GetSpeed in {target_names:?}"
        );

        // Delegating entity should also have the members
        let deleg_class_id = LuaTypeDeclId::global("delegating_ent");
        let deleg_owner = LuaMemberOwner::Type(deleg_class_id);
        let deleg_members = db
            .get_member_index()
            .get_members(&deleg_owner)
            .expect("expected members on delegating_ent");
        let deleg_names: Vec<_> = deleg_members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();
        assert!(
            deleg_names.contains(&"GetSpeed".to_string()),
            "delegating_ent missing GetSpeed in {deleg_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_ignores_non_setupdatatables_method() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/entities/target_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "Speed")
                end
            "#,
            ),
            (
                "lua/entities/delegating_ent/init.lua",
                r#"
                function ENT:Think()
                    local Think = scripted_ents.GetMember("target_ent", "Think")
                    Think(self)
                end
            "#,
            ),
        ]);

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("delegating_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on delegating_ent");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            !member_names.contains(&"GetSpeed".to_string()),
            "unexpected copied GetSpeed from non-SetupDataTables delegation in {member_names:?}"
        );
        assert!(
            !member_names.contains(&"SetSpeed".to_string()),
            "unexpected copied SetSpeed from non-SetupDataTables delegation in {member_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_copies_network_vars_from_all_class_files() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/entities/target_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "ServerSpeed")
                end
            "#,
            ),
            (
                "lua/entities/target_ent/cl_init.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "ClientOnlySpeed")
                end
            "#,
            ),
            (
                "lua/entities/delegating_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    scripted_ents.GetMember("target_ent", "SetupDataTables")(self)
                end
            "#,
            ),
        ]);

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("delegating_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on delegating_ent");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetServerSpeed".to_string()),
            "missing GetServerSpeed from init.lua in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetClientOnlySpeed".to_string()),
            "missing GetClientOnlySpeed from cl_init.lua in {member_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_copies_network_vars_from_shared_target_file() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        ws.def_files(vec![
            (
                "lua/entities/target_ent/init.lua",
                r#"
                function ENT:Initialize()
                end
            "#,
            ),
            (
                "lua/entities/target_ent/shared.lua",
                r#"
                function ENT:SetupDataTables()
                    self:NetworkVar("Float", 0, "SharedSpeed")
                    self:NetworkVar("Bool", 0, "SharedActive")
                end
            "#,
            ),
            (
                "lua/entities/delegating_ent/init.lua",
                r#"
                function ENT:SetupDataTables()
                    local SetupDataTables = scripted_ents.GetMember("target_ent", "SetupDataTables")
                    SetupDataTables(self)
                end
            "#,
            ),
        ]);

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("delegating_ent");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members on delegating_ent");
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();

        assert!(
            member_names.contains(&"GetSharedSpeed".to_string()),
            "missing GetSharedSpeed from shared.lua in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetSharedSpeed".to_string()),
            "missing SetSharedSpeed from shared.lua in {member_names:?}"
        );
        assert!(
            member_names.contains(&"GetSharedActive".to_string()),
            "missing GetSharedActive from shared.lua in {member_names:?}"
        );
        assert!(
            member_names.contains(&"SetSharedActive".to_string()),
            "missing SetSharedActive from shared.lua in {member_names:?}"
        );
    }

    #[gtest]
    fn test_getmember_delegation_target_not_found_does_not_crash() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")];
        ws.update_emmyrc(emmyrc);

        // Delegation to a nonexistent class should not crash
        ws.def_file(
            "lua/entities/edit_sky/init.lua",
            r#"
            function ENT:SetupDataTables()
                local f = scripted_ents.GetMember("nonexistent_class", "SetupDataTables")
                f(self)
            end
        "#,
        );

        let db = ws.get_db_mut();
        let class_id = LuaTypeDeclId::global("edit_sky");
        let owner = LuaMemberOwner::Type(class_id);
        let members = db
            .get_member_index()
            .get_members(&owner)
            .expect("expected members");
        // Should not have get/set members from the nonexistent class
        let member_names: Vec<_> = members
            .iter()
            .filter_map(|m| m.get_key().get_name().map(|n| n.to_string()))
            .collect();
        assert!(
            !member_names.contains(&"GetSpeed".to_string()),
            "unexpected GetSpeed from nonexistent target in {member_names:?}"
        );
    }

    /// Oracle audit: `self.field` IndexRef root uses the shared receiver decl,
    /// NOT the method-aware self_decl_id. Two methods of the SAME reused
    /// `local PANEL` (regions A and B) share the same receiver `Decl(panel_decl)`.
    /// If the IndexRef key for `self.value` collapses across regions, then
    /// `self.value` typed as string in region A could poison region B's
    /// `self.value` (which should be number), or vice-versa.
    ///
    /// This test assigns DIFFERENT types to `self.value` in each region and
    /// asserts each resolves independently.
    #[gtest]
    fn test_vgui_reassigned_panel_self_field_type_not_collapsed_across_regions() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        let file_id = ws.def_file(
            "lua/vgui/self_field_collapse.lua",
            r#"
            local PANEL = {}
            function PANEL:Init()
                self.value = "stringval"
            end
            function PANEL:UseValue()
                local x = self.value
            end
            vgui.Register("PanelA", PANEL, "DPanel")

            PANEL = {}
            function PANEL:Init()
                self.value = 123
            end
            function PANEL:UseValue()
                local y = self.value
            end
            vgui.Register("PanelB", PANEL, "DPanel")
            "#,
        );

        let x_type = local_name_type(&mut ws, file_id, "x");
        let y_type = local_name_type(&mut ws, file_id, "y");

        let x_display = ws.humanize_type(x_type.clone());
        let y_display = ws.humanize_type(y_type.clone());

        // Region A: self.value = "stringval" → x should resolve to string
        assert!(
            x_display.contains("string"),
            "PanelA region: self.value should resolve to string, got {x_display:?}"
        );

        // Region B: self.value = 123 → y should resolve to number/integer
        assert!(
            y_display.contains("number")
                || y_display.contains("integer")
                || matches!(
                    y_type,
                    LuaType::IntegerConst(_) | LuaType::Number | LuaType::Integer
                ),
            "PanelB region: self.value should resolve to number/integer, got {y_display:?} ({y_type:?})"
        );

        // The two types must be distinct — collapse would make them identical
        assert_ne!(
            x_type, y_type,
            "self.value types should differ across regions (collapse detected): \
             x={x_display:?}, y={y_display:?}"
        );
    }

    /// Regression: when PANEL is aliased (`local OLD = PANEL`), then PANEL is
    /// reassigned (`PANEL = {}`), and a method is added via the OLD alias
    /// (`function OLD:OldOnly() end`), the fallback member transfer must NOT
    /// carry `OldOnly` into the new panel class. The member belongs to the
    /// original initializer table through the OLD alias, not to the reassigned
    /// PANEL table that is being registered.
    ///
    /// The risk: `collect_panel_member_source_ranges` includes the initializer
    /// table range as a fallback source. If `OldOnly`'s position falls within
    /// `[latest_write_position, register_position)` of the PANEL decl, the
    /// position-slicing filter could wrongly transfer it to NewPanel.
    #[gtest]
    fn test_vgui_register_alias_after_reassignment_does_not_transfer_aliased_members() {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/vgui/alias_after_reassign.lua",
            r#"
            local PANEL = {}
            local OLD = PANEL
            PANEL = {}
            function OLD:OldOnly() end
            vgui.Register("NewPanel", PANEL, "DPanel")
        "#,
        );

        let db = ws.get_db_mut();
        let new_panel_class_id = LuaTypeDeclId::global("NewPanel");

        assert!(
            db.get_type_index()
                .get_type_decl(&new_panel_class_id)
                .is_some(),
            "NewPanel class should be created"
        );

        let new_panel_members = db
            .get_member_index()
            .get_members(&LuaMemberOwner::Type(new_panel_class_id.clone()))
            .map(|members| {
                members
                    .iter()
                    .filter_map(|member| member.get_key().get_name().map(ToString::to_string))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        assert!(
            !new_panel_members.contains(&"OldOnly".to_string()),
            "NewPanel should NOT inherit OldOnly from the aliased OLD table, got {new_panel_members:?}"
        );
    }
}
