package com.ruoyi.generator.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.domain.GenProject;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionInputFieldDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
import com.ruoyi.generator.service.GenTableServiceImpl;
import com.ruoyi.generator.service.GenProjectServiceImpl;
import java.lang.reflect.Method;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.junit.Test;

public class QingTemplateSupportTest
{
    @Test
    public void qingTemplateListIncludesRunnableProjectFiles() throws Exception
    {
        List<String> templates = TemplateUtils.getTemplateList("crud", "/qing");

        assertTrue(templates.contains("qing/pom.xml.vm"));
        assertTrue(templates.contains("qing/application.yml.vm"));
        assertTrue(templates.contains("qing/application.java.vm"));
        assertTrue(templates.contains("qing/result.java.vm"));
        assertTrue(templates.contains("qing/mybatisPlusConfig.java.vm"));
        assertTrue(templates.contains("qing/adminUser.java.vm"));
        assertTrue(templates.contains("qing/adminUserMapper.java.vm"));
        assertTrue(templates.contains("qing/portalUser.java.vm"));
        assertTrue(templates.contains("qing/portalUserMapper.java.vm"));
        assertTrue(templates.contains("qing/portalAuthTokenStore.java.vm"));
        assertTrue(templates.contains("qing/portalAuthInterceptor.java.vm"));
        assertTrue(templates.contains("qing/adminAuthController.java.vm"));
        assertTrue(templates.contains("qing/portalAuthController.java.vm"));
        assertTrue(templates.contains("qing/sql.vm"));
        assertTrue(templates.contains("qing/vue-package.json.vm"));
        assertTrue(templates.contains("qing/vue-main.js.vm"));
        assertTrue(templates.contains("qing/vue-request.js.vm"));
        assertTrue(templates.contains("qing/index.vue.vm"));
        assertTrue(templates.contains("qing/vue-login.vue.vm"));

        Path resourceRoot = repoRoot().resolve("ruoyi-generator/src/main/resources");
        for (String template : templates)
        {
            assertTrue("Missing qing template file: " + template, Files.exists(resourceRoot.resolve(template)));
        }
    }

    @Test
    public void qingPackageIncludesEcharts()
    {
        VelocityInitializer.initVelocity();
        String content = render("qing/vue-package.json.vm", VelocityUtils.prepareContextProject(project()));

        assertTrue(content.contains("\"echarts\": \"5.4.0\""));
    }

    @Test
    public void qingTemplatesAllResolveRunnableOutputFileNames()
    {
        GenTable table = table();

        for (String template : TemplateUtils.getTemplateList("crud", "/qing"))
        {
            String fileName = FileNameUtils.getFileName(template, table, "/qing");
            assertFalse("Qing template should resolve output path: " + template, fileName.isEmpty());
        }

        assertEquals("pom.xml", FileNameUtils.getFileName("qing/pom.xml.vm", table, "/qing"));
        assertEquals("src/main/resources/application.yml",
                FileNameUtils.getFileName("qing/application.yml.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/DemoProductApplication.java",
                FileNameUtils.getFileName("qing/application.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/entity/DemoProduct.java",
                FileNameUtils.getFileName("qing/entity.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/entity/AdminUser.java",
                FileNameUtils.getFileName("qing/adminUser.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/mapper/AdminUserMapper.java",
                FileNameUtils.getFileName("qing/adminUserMapper.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/entity/PortalUser.java",
                FileNameUtils.getFileName("qing/portalUser.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/mapper/PortalUserMapper.java",
                FileNameUtils.getFileName("qing/portalUserMapper.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/security/PortalAuthTokenStore.java",
                FileNameUtils.getFileName("qing/portalAuthTokenStore.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/security/PortalAuthInterceptor.java",
                FileNameUtils.getFileName("qing/portalAuthInterceptor.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/controller/AdminAuthController.java",
                FileNameUtils.getFileName("qing/adminAuthController.java.vm", table, "/qing"));
        assertEquals("src/main/java/com/example/demo/controller/PortalAuthController.java",
                FileNameUtils.getFileName("qing/portalAuthController.java.vm", table, "/qing"));
        assertEquals("vue/src/views/demo/product/index.vue",
                FileNameUtils.getFileName("qing/index.vue.vm", table, "/qing"));
        assertEquals("vue/src/views/login/index.vue",
                FileNameUtils.getFileName("qing/vue-login.vue.vm", table, "/qing"));
    }

    @Test
    public void qingDefaultGenerationPathDoesNotNestSrcTwice()
    {
        GenTable table = table();
        table.setGenPath("/");

        String path = GenTableServiceImpl.getGenPath(table, "qing/entity.java.vm", "/qing")
                .replace('\\', '/');

        assertTrue(path.endsWith("/src/main/java/com/example/demo/entity/DemoProduct.java"));
        assertFalse(path.contains("/src/src/main/"));
    }

    @Test
    public void qingControllerUsesGeneratedPackageContracts() throws Exception
    {
        String controllerTemplate = read(repoRoot().resolve(
                "ruoyi-generator/src/main/resources/qing/controller.java.vm"));

        assertFalse(controllerTemplate.contains("com.example.springboot"));
        assertFalse(controllerTemplate.contains(".getId()"));
        assertTrue(controllerTemplate.contains("${packageName}.common.Result"));
        assertTrue(controllerTemplate.contains("@PathVariable ${pkColumn.javaType} ${pkColumn.javaField}"));
        assertTrue(controllerTemplate.contains("${className}Service.saveOrUpdate(${className})"));
    }

    @Test
    public void qingEntityImportsDateTypesForStandaloneAuditColumns()
    {
        GenTable table = renderTable();
        GenTableColumn createTime = column("create_time", "创建时间", "Date", "createTime",
                "0", "1", "0", "1", "EQ", "datetime");
        GenTableColumn updateTime = column("update_time", "更新时间", "Date", "updateTime",
                "0", "1", "0", "1", "EQ", "datetime");
        List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
        columns.add(createTime);
        columns.add(updateTime);
        table.setColumns(columns);

        VelocityInitializer.initVelocity();
        String rendered = render("qing/entity.java.vm", VelocityUtils.prepareQingContext(table));

        assertTrue(rendered.contains("import java.util.Date;"));
        assertTrue(rendered.contains("import com.fasterxml.jackson.annotation.JsonFormat;"));
        assertTrue(rendered.contains("private Date createTime;"));
        assertTrue(rendered.contains("private Date updateTime;"));
    }

    @Test
    public void qingTemplatesRenderExecutableBusinessActions()
    {
        GenTable table = renderTable();
        BusinessActionDesign action = businessAction();
        action.setCurrentUserFields(Arrays.asList("user_id", "username"));
        BusinessActionRuleDesign rule = new BusinessActionRuleDesign();
        rule.setType("NUMBER_GTE");
        rule.setTargetTable("demo_product");
        rule.setTargetField("product_stock");
        rule.setCompareValue("1");
        rule.setConditionFields(Arrays.asList("id"));
        rule.setMessage("Stock is not enough");
        action.setRuleChecks(Arrays.asList(rule));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        context.put("tableBusinessActions", Arrays.asList(action));
        context.put("hasTableBusinessActions", true);

        String controller = render("qing/controller.java.vm", context);
        String service = render("qing/service.java.vm", context);
        String implementation = render("qing/serviceImpl.java.vm", context);

        assertTrue(controller.contains("@PostMapping(\"/library/borrow/borrow\")"));
        assertTrue(controller.contains("actionParams.put(\"__currentUserId\", currentUser.getUserId())"));
        assertTrue(controller.contains(
                "prepareBusinessActionParams(params, request, new String[] {\"user_id\", \"username\"})"));
        assertTrue(controller.contains("\"username\".equals(field)"));
        assertTrue(controller.contains("currentUser.getUsername()"));
        assertTrue(controller.contains("actionParams.put(field, currentUser.getUserId())"));
        assertTrue(service.contains("int borrow_book(Map<String, Object> params)"));
        assertTrue(implementation.contains("@Transactional(rollbackFor = Exception.class)"));
        assertTrue(implementation.contains("resolveBusinessValue"));
        assertTrue(implementation.contains("\"${current_user.id}\""));
        assertTrue(implementation.contains(
                "assertNumberGte(params, \"demo_product\", \"product_stock\", \"1\", new String[] {\"id\"}"));
        assertTrue(implementation.contains("where.append(safeField).append(\" = ?\")"));
        assertFalse(implementation.contains("conditionColumns"));
    }

    @Test
    public void qingBackendResolvesLegacyCurrentUserIdPlaceholder()
    {
        GenTable table = renderTable();
        BusinessActionDesign action = businessAction();
        action.getEffects().get(0).getValues().put("user_id", "${current_user_id}");
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        context.put("tableBusinessActions", Arrays.asList(action));
        context.put("hasTableBusinessActions", true);

        String implementation = render("qing/serviceImpl.java.vm", context);

        assertTrue(implementation.contains(
                "insertValues0.put(\"user_id\", resolveBusinessValue(params, \"${current_user_id}\"))"));
        assertTrue(implementation.contains("\"${current_user_id}\".equals(valueExpression)"));
        assertTrue(implementation.contains("return params.get(\"__currentUserId\")"));
    }

    @Test
    public void qingFrontendUsesOneDialogForMissingBusinessActionFields()
    {
        GenTable table = renderTable();
        BusinessActionDesign action = businessAction();
        action.setButtonLabel("Borrow");
        action.setCurrentUserFields(Arrays.asList("user_id"));
        BusinessActionInputFieldDesign dueDate = new BusinessActionInputFieldDesign();
        dueDate.setField("due_date");
        dueDate.setLabel("Due date");
        dueDate.setHtmlType("datetime");
        dueDate.setDictType("");
        action.setInputFields(Arrays.asList(dueDate));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        context.put("pageDesignBusinessActions", Arrays.asList(action));

        String page = render("qing/index.vue.vm", context);

        assertTrue(page.contains("businessActionDialogVisible"));
        assertTrue(page.contains("businessActionInputFields"));
        assertTrue(page.contains("submitBusinessAction"));
        assertTrue(page.contains("openBusinessActionDialog"));
        assertTrue(page.contains("currentUserFields: [\"user_id\"]"));
        assertTrue(page.contains("field: \"due_date\""));
        assertTrue(page.contains("label: \"Due date\""));
        assertTrue(page.contains("htmlType: \"datetime\""));
        assertTrue(page.contains("v-for=\"field in businessActionInputFields\""));
        assertTrue(page.contains("field.htmlType === 'datetime'"));
        assertTrue(page.contains("field.htmlType === 'textarea'"));
        assertTrue(page.contains("getBusinessActionFieldOptions(field)"));
        assertFalse(page.contains("this.$prompt"));
        assertFalse(page.contains("collectBusinessActionPayload"));
    }

    @Test
    public void qingCrudFormHidesAutoIncrementIdOnCreateAndShowsItReadOnlyOnEdit()
    {
        GenTable table = renderTable();
        GenTableColumn id = column("id", "ID", "Long", "id",
                "1", "1", "1", "1", "EQ", "input");
        id.setIsRequired("1");
        GenTableColumn name = column("name", "Name", "String", "name",
                "0", "1", "1", "1", "LIKE", "input");
        name.setIsRequired("1");
        table.setPkColumn(id);
        table.setColumns(Arrays.asList(id, name));

        VelocityInitializer.initVelocity();
        String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(table));

        assertTrue(rendered.contains("<el-form-item v-if=\"form.id\" label=\"ID\" prop=\"id\">"));
        assertTrue(rendered.contains("<el-input v-model=\"form.id\" disabled"));
        assertFalse(rendered.contains("prop=\"id\" :rules="));
        assertTrue(rendered.contains("<el-form-item label=\"Name\" prop=\"name\" :rules="));
        assertTrue(rendered.contains("<el-input v-model=\"form.name\" placeholder="));
    }

    @Test
    public void qingTemplatesRenderWithSampleCrudTable()
    {
        GenTable table = renderTable();
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);

        for (String templateName : TemplateUtils.getTemplateList("crud", "/qing"))
        {
            Template template = Velocity.getTemplate(templateName, "UTF-8");
            StringWriter writer = new StringWriter();
            template.merge(context, writer);
            String rendered = writer.toString();
            assertFalse("Rendered template should not keep hard-coded demo package: " + templateName,
                    rendered.contains("com.example.springboot"));
            assertFalse("Rendered template should not contain raw Velocity literal marker: " + templateName,
                    rendered.contains("#[["));
            assertFalse("Rendered template should not contain raw Velocity literal marker: " + templateName,
                    rendered.contains("]]#"));

            if ("qing/vue-main.js.vm".equals(templateName))
            {
                assertTrue(rendered.contains("}).$mount(\"#app\")"));
            }
            if ("qing/vue-app.vue.vm".equals(templateName))
            {
                assertTrue(rendered.contains("class=\"portal-shell\""));
                assertTrue(rendered.contains("class=\"hero-banner\""));
                assertTrue(rendered.contains("localStorage.getItem(\"portal-user\")"));
                assertTrue(rendered.contains("localStorage.removeItem(\"portal-token\")"));
            }
            if ("qing/vue-router.js.vm".equals(templateName))
            {
                assertTrue(rendered.contains("path: \"/login\""));
                assertTrue(rendered.contains("meta: { requiresAuth: true }"));
                assertTrue(rendered.contains("localStorage.getItem(\"portal-token\")"));
            }
            if ("qing/vue-request.js.vm".equals(templateName))
            {
                assertTrue(rendered.contains("Authorization"));
                assertTrue(rendered.contains("portal-token"));
                assertFalse(rendered.contains("portalLoggedIn"));
            }
            if ("qing/vue-login.vue.vm".equals(templateName))
            {
                assertTrue(rendered.contains("class=\"portal-login\""));
                assertTrue(rendered.contains("username: \"user\""));
                assertTrue(rendered.contains("password: \"123456\""));
                assertTrue(rendered.contains("this.request.post(\"/auth/login\""));
                assertTrue(rendered.contains("localStorage.setItem(\"portal-token\""));
                assertTrue(rendered.contains("this.$router.push(redirect)"));
            }
            if ("qing/adminUser.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("@TableName(\"t_admin\")"));
                assertTrue(rendered.contains("@TableId(value = \"admin_id\""));
            }
            if ("qing/portalUser.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("@TableName(\"t_user\")"));
                assertTrue(rendered.contains("@TableId(value = \"user_id\""));
            }
            if ("qing/portalAuthTokenStore.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("Map<String, Object>"));
                assertTrue(rendered.contains("getPortalUser"));
                assertTrue(rendered.contains("getAdminUser"));
            }
            if ("qing/adminAuthController.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("@RequestMapping(\"/auth/admin\")"));
                assertTrue(rendered.contains("AdminUserMapper"));
                assertTrue(rendered.contains("tokenStore.issue"));
            }
            if ("qing/portalAuthController.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("@PostMapping(\"/login\")"));
                assertTrue(rendered.contains("PortalUserMapper"));
                assertTrue(rendered.contains("passwordHash"));
                assertTrue(rendered.contains("tokenStore.issue"));
                assertTrue(rendered.contains("tokenStore.getPortalUser"));
            }
            if ("qing/portalAuthInterceptor.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("Authorization"));
                assertTrue(rendered.contains("Bearer "));
                assertTrue(rendered.contains("response.setStatus(401)"));
            }
            if ("qing/corsConfig.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("addInterceptors"));
                assertTrue(rendered.contains("PortalAuthInterceptor"));
                assertTrue(rendered.contains("excludePathPatterns(\"/auth/login\""));
                assertTrue(rendered.contains("\"/auth/admin/login\""));
                assertTrue(rendered.contains("\"/business-blocks/**\""));
            }
            if ("qing/sql.vm".equals(templateName))
            {
                assertTrue(rendered.contains("create table if not exists t_admin"));
                assertTrue(rendered.contains("insert into t_admin"));
                assertTrue(rendered.contains("select 'admin'"));
                assertTrue(rendered.contains("where not exists (select 1 from t_admin where username = 'admin')"));
                assertTrue(rendered.contains("create table if not exists t_user"));
                assertTrue(rendered.contains("insert into t_user"));
                assertTrue(rendered.contains("select 'user'"));
                assertTrue(rendered.contains("where not exists (select 1 from t_user where username = 'user')"));
                assertTrue(rendered.contains("password_hash"));
                assertTrue(rendered.contains("8d969eef6ecad3c29a3a629280e686cf"));
                assertFalse(rendered.contains("portal_user"));
            }
            if ("qing/index.vue.vm".equals(templateName))
            {
                assertTrue(rendered.contains("this.$refs.form.validate"));
                assertTrue(rendered.contains("this.$message.success"));
                assertTrue(rendered.contains("this.$set(this.form, field, res.data.url)"));
            }
        }
    }

    @Test
    public void qingIndexTemplateRendersRemoteSelectConfiguration()
    {
        GenTable table = renderTable();
        GenTableColumn category = column("category_id", "鍒嗙被", "Long", "categoryId",
                "0", "1", "1", "1", "EQ", "select");
        category.setDictType("remote:book_category:id:category_name");
        List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
        columns.add(category);
        table.setColumns(columns);

        VelocityInitializer.initVelocity();
        String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(table));

        assertTrue(rendered.contains("remoteOptionMap: {}"));
        assertTrue(rendered.contains("categoryId: {"));
        assertTrue(rendered.contains("path: \"/demo/bookCategory/page\""));
        assertTrue(rendered.contains("valueField: \"id\""));
        assertTrue(rendered.contains("labelField: \"categoryName\""));
        assertTrue(rendered.contains("v-for=\"item in getSelectOptions('categoryId')\""));
        assertTrue(rendered.contains("{{ getOptionLabel('categoryId', scope.row.categoryId) }}"));
    }

    @Test
    public void qingIndexTemplateResolvesUploadedResourceUrlsForPreviewProxy()
    {
        GenTable table = renderTable();
        GenTableColumn cover = column("cover_url", "Cover", "String", "coverUrl",
                "0", "1", "1", "1", "EQ", "imageUpload");
        List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
        columns.add(cover);
        table.setColumns(columns);

        VelocityInitializer.initVelocity();
        String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(table));

        assertTrue(rendered.contains(":src=\"resolveResourceUrl(scope.row.coverUrl)\""));
        assertTrue(rendered.contains(":preview-src-list=\"[resolveResourceUrl(scope.row.coverUrl)]\""));
        assertTrue(rendered.contains(":src=\"resolveResourceUrl(form.coverUrl)\""));
        assertTrue(rendered.contains("resolveResourceUrl(url)"));
        assertTrue(rendered.contains("return \"/api\" + url"));
    }

    @Test
    public void qingIndexTemplateBuildsConfiguredRoutesWithoutSqlFragileRegexLiteral()
    {
        VelocityInitializer.initVelocity();
        String rendered = render("qing/index.vue.vm", VelocityUtils.prepareContext(renderTable()));

        assertTrue(rendered.contains("hasUnresolvedRouteParam(path)"));
        assertFalse(rendered.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
        assertFalse(rendered.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
    }

    @Test
    public void qingProjectTemplatesRenderWithSampleProject()
    {
        GenProject project = project();
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        for (String templateName : Arrays.asList(
                "qing/pom.xml.vm",
                "qing/application.yml.vm",
                "qing/application.java.vm",
                "qing/readme.md.vm",
                "qing/vue-index.html.vm"))
        {
            Template template = Velocity.getTemplate(templateName, "UTF-8");
            StringWriter writer = new StringWriter();
            template.merge(context, writer);
            String rendered = writer.toString();

            assertFalse("Rendered project template should not keep raw businessName placeholder: " + templateName,
                    rendered.contains("${businessName}"));
            assertFalse("Rendered project template should not keep raw BusinessName placeholder: " + templateName,
                    rendered.contains("${BusinessName}"));
            assertFalse("Rendered project template should not keep raw ClassName placeholder: " + templateName,
                    rendered.contains("${ClassName}"));
            assertFalse("Rendered project template should not keep raw functionName placeholder: " + templateName,
                    rendered.contains("${functionName}"));

            if ("qing/pom.xml.vm".equals(templateName))
            {
                assertTrue(rendered.contains("<artifactId>demo-app-service</artifactId>"));
            }
            if ("qing/application.yml.vm".equals(templateName))
            {
                assertTrue(rendered.contains("name: demo-app-service"));
            }
            if ("qing/application.java.vm".equals(templateName))
            {
                assertTrue(rendered.contains("public class DemoAppApplication"));
            }
            if ("qing/readme.md.vm".equals(templateName))
            {
                assertTrue(rendered.contains("# Demo App"));
                assertTrue(rendered.contains("sql/demo-app.sql"));
            }
            if ("qing/vue-index.html.vm".equals(templateName))
            {
                assertTrue(rendered.contains("<title>Demo App</title>"));
            }
        }
    }

    @Test
    public void qingPortalProjectTemplatesUseProjectNameAndFrontendMenus()
    {
        GenProject project = project();
        project.setProjectName("Library Portal");
        GenTable book = table();
        book.setTableName("book");
        book.setModuleName("system");
        book.setBusinessName("book");
        book.setClassName("Book");
        book.setFunctionName("Book Catalog");
        GenTable borrow = table();
        borrow.setTableName("borrow_record");
        borrow.setModuleName("system");
        borrow.setBusinessName("borrowRecord");
        borrow.setClassName("BorrowRecord");
        borrow.setFunctionName("Borrow Records");
        project.setTables(Arrays.asList(book, borrow));
        project.setFrontendMenus(Arrays.asList(
                menu("book_catalog", "Catalog", "/books", false),
                menu("my_borrows", "My Borrows", "/my-borrows", true)));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        String app = render("qing/vue-app.vue.vm", context);
        assertTrue(app.contains("Library Portal"));
        assertTrue(app.contains("Catalog"));
        assertTrue(app.contains("My Borrows"));
        assertTrue(app.contains("to=\"/books\""));
        assertTrue(app.contains("to=\"/my-borrows\""));
        assertFalse(app.contains("游戏资源平台"));
        assertFalse(app.contains("游戏攻略"));

        String router = render("qing/vue-router.js.vm", context);
        assertTrue(router.contains("redirect: \"/books\""));
        assertTrue(router.contains("path: \"/books\""));
        assertTrue(router.contains("path: \"/my-borrows\""));
        assertTrue(router.contains("component: Book"));
        assertTrue(router.contains("component: BorrowRecord"));
        assertTrue(router.contains("meta: { requiresAuth: false }"));
        assertTrue(router.contains("meta: { requiresAuth: true }"));

        String login = render("qing/vue-login.vue.vm", context);
        assertTrue(login.contains("Library Portal"));
        assertFalse(login.contains("游戏资源平台"));
    }

    @Test
    public void qingPortalRoutesBusinessBlockPagesToGeneratedPageComponent()
    {
        GenProject project = project();
        GenTable cart = table();
        cart.setTableName("shop_cart");
        cart.setModuleName("shop");
        cart.setBusinessName("cart");
        cart.setClassName("ShopCart");
        cart.setFunctionName("Cart");
        project.setTables(Arrays.asList(cart));
        AppMenuDesign cartMenu = menu("cart_menu", "Cart", "/cart", true);
        cartMenu.setPageCode("cart_page");
        project.setFrontendMenus(Arrays.asList(cartMenu));
        AppPageDesign cartPage = new AppPageDesign();
        cartPage.setCode("cart_page");
        cartPage.setMenuCode("cart_menu");
        cartPage.setName("Cart");
        cartPage.setPath("/cart");
        cartPage.setPageType("list");
        cartPage.setTableName("shop_cart");
        project.setFrontendPages(Arrays.asList(cartPage));
        FrontProjectPageDesign cartDesign = new FrontProjectPageDesign();
        cartDesign.setPageCode("cart_page");
        cartDesign.setMenuCode("cart_menu");
        cartDesign.setPageName("Cart");
        cartDesign.setRoutePath("/cart");
        cartDesign.setPageScope("frontend");
        cartDesign.setPageType("business_block");
        cartDesign.setTableName("shop_cart");
        cartDesign.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[]}");
        project.setPageDesigns(Arrays.asList(cartDesign));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        String router = render("qing/vue-router.js.vm", context);
        assertTrue(router.contains("import CartPage from \"@/views/pages/cart_page/index.vue\""));
        assertTrue(router.contains("path: \"/cart\""));
        assertTrue(router.contains("component: CartPage"));
    }

    @Test
    public void qingPortalUsesPageDesignNavigationToHideMenuAndKeepRoute()
    {
        GenProject project = project();
        GenTable book = table();
        book.setTableName("book_info");
        book.setModuleName("library");
        book.setBusinessName("book");
        book.setClassName("Book");
        book.setFunctionName("Books");
        project.setTables(Arrays.asList(book));
        AppMenuDesign menu = menu("book_catalog", "Catalog", "/books", true);
        menu.setPageCode("book_catalog_page");
        project.setFrontendMenus(Arrays.asList(menu));
        AppPageDesign page = new AppPageDesign();
        page.setCode("book_catalog_page");
        page.setMenuCode("book_catalog");
        page.setName("Books");
        page.setPath("/books");
        page.setPageType("list");
        page.setTableName("book_info");
        project.setFrontendPages(Arrays.asList(page));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("book_catalog_page");
        design.setMenuCode("book_catalog");
        design.setPageName("Books");
        design.setRoutePath("/books");
        design.setPageScope("frontend");
        design.setPageType("list");
        design.setTableName("book_info");
        design.setLayoutJson("{\"navigation\":{\"showInMenu\":false},\"regions\":[]}");
        project.setPageDesigns(Arrays.asList(design));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        String app = render("qing/vue-app.vue.vm", context);
        assertFalse(app.contains("Catalog"));

        String router = render("qing/vue-router.js.vm", context);
        assertTrue(router.contains("import BookCatalogPage from \"@/views/pages/book_catalog_page/index.vue\""));
        assertTrue(router.contains("path: \"/books\""));
        assertTrue(router.contains("component: BookCatalogPage"));
    }

    @Test
    public void qingPortalRoutesVisibleNormalPageDesignsToGeneratedPageComponent()
    {
        GenProject project = project();
        GenTable book = table();
        book.setTableName("book_info");
        book.setModuleName("library");
        book.setBusinessName("book");
        book.setClassName("Book");
        book.setFunctionName("Books");
        project.setTables(Arrays.asList(book));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("book_catalog_page");
        design.setMenuCode("book_catalog_page");
        design.setPageName("Books");
        design.setRoutePath("/books");
        design.setPageScope("frontend");
        design.setPageType("list");
        design.setTableName("book_info");
        design.setLayoutJson("{\"navigation\":{\"showInMenu\":true},\"regions\":[]}");
        project.setPageDesigns(Arrays.asList(design));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        String app = render("qing/vue-app.vue.vm", context);
        assertTrue(app.contains("to=\"/books\""));

        String router = render("qing/vue-router.js.vm", context);
        assertTrue(router.contains("import BookCatalogPage from \"@/views/pages/book_catalog_page/index.vue\""));
        assertTrue(router.contains("component: BookCatalogPage"));
    }

    @Test
    public void qingPortalAppRendersPageDesignerMenuHierarchy()
    {
        GenProject project = project();
        project.setProjectName("Library Portal");
        GenTable book = table();
        book.setTableName("book_info");
        book.setModuleName("library");
        book.setBusinessName("book");
        book.setClassName("Book");
        book.setFunctionName("Books");
        project.setTables(Arrays.asList(book));

        FrontProjectPageDesign home = new FrontProjectPageDesign();
        home.setPageCode("home_page");
        home.setMenuCode("home_page");
        home.setPageName("Home");
        home.setRoutePath("/home");
        home.setPageScope("frontend");
        home.setPageType("list");
        home.setTableName("book_info");
        home.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"home_page\",\"menuName\":\"Home\",\"menuOrder\":1},\"regions\":[]}");

        FrontProjectPageDesign directory = new FrontProjectPageDesign();
        directory.setPageCode("library_directory");
        directory.setMenuCode("library_directory");
        directory.setPageName("Library");
        directory.setPageScope("frontend");
        directory.setPageType("menu_directory");
        directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"library_directory\",\"menuName\":\"Library\",\"menuOrder\":2}}");

        FrontProjectPageDesign books = new FrontProjectPageDesign();
        books.setPageCode("books_page");
        books.setMenuCode("library_directory");
        books.setPageName("Books");
        books.setRoutePath("/books");
        books.setPageScope("frontend");
        books.setPageType("list");
        books.setTableName("book_info");
        books.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"secondary\",\"parentMenuCode\":\"library_directory\",\"parentMenuName\":\"Library\",\"menuOrder\":1},\"regions\":[]}");

        FrontProjectPageDesign notices = new FrontProjectPageDesign();
        notices.setPageCode("notices_page");
        notices.setMenuCode("notices_page");
        notices.setPageName("Notices");
        notices.setRoutePath("/notices");
        notices.setPageScope("frontend");
        notices.setPageType("list");
        notices.setTableName("book_info");
        notices.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"notices_page\",\"menuName\":\"Notices\",\"menuOrder\":3},\"regions\":[]}");
        project.setPageDesigns(Arrays.asList(home, directory, books, notices));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        assertEquals(true, context.get("hasFrontendNavGroups"));
        String app = render("qing/vue-app.vue.vm", context);
        assertTrue(app.contains("class=\"portal-nav-group has-children\""));
        assertTrue(app.contains(":class='{ active: isNavGroupActive([\"/home\"]) }'"));
        assertFalse(app.contains(":class=\"{ active: isNavGroupActive([\""));
        assertTrue(app.contains("portal-nav-directory"));
        assertTrue(app.contains("class=\"portal-nav-dropdown\""));
        assertTrue(app.contains(">Library<"));
        assertTrue(app.contains(">Books<"));
        assertTrue(app.indexOf(">Home<") < app.indexOf(">Library<"));
        assertTrue(app.indexOf(">Library<") < app.indexOf(">Notices<"));

        String router = render("qing/vue-router.js.vm", context);
        assertFalse(router.contains("LibraryDirectory"));
        assertTrue(router.contains("path: \"/books\""));
        assertTrue(router.contains("path: \"/notices\""));
    }

    @Test
    public void qingPortalTreatsLegacyMenuCodeAsPrimaryMenu()
    {
        GenProject project = project();
        project.setProjectName("Library Portal");
        GenTable announcement = table();
        announcement.setTableName("announcement");
        announcement.setModuleName("library");
        announcement.setBusinessName("announcement");
        announcement.setClassName("Announcement");
        announcement.setFunctionName("Announcements");
        project.setTables(Arrays.asList(announcement));

        FrontProjectPageDesign page = new FrontProjectPageDesign();
        page.setPageCode("announcement_list_page");
        page.setMenuCode("announcements");
        page.setPageName("\u516c\u544a\u5217\u8868");
        page.setRoutePath("/announcements");
        page.setPageScope("frontend");
        page.setPageType("list");
        page.setTableName("announcement");
        page.setLayoutJson("{\"canvas\":\"frontend-list-v1\",\"regions\":[]}");
        project.setPageDesigns(Arrays.asList(page));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);
        String app = render("qing/vue-app.vue.vm", context);

        assertTrue(app.contains("class=\"portal-nav-parent\" to=\"/announcements\" exact>\u516c\u544a\u5217\u8868</router-link>"));
        assertFalse(app.contains("portal-nav-directory\">announcements</span>"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void qingAdminContextUsesPageDesignerMenuHierarchy()
    {
        GenProject project = project();
        GenTable book = table();
        book.setTableName("book_info");
        book.setModuleName("library");
        book.setBusinessName("book");
        book.setClassName("Book");
        book.setFunctionName("Books");
        GenTable user = table();
        user.setTableName("user_info");
        user.setModuleName("system");
        user.setBusinessName("user");
        user.setClassName("User");
        user.setFunctionName("Users");
        project.setTables(Arrays.asList(book, user));

        FrontProjectPageDesign directory = new FrontProjectPageDesign();
        directory.setPageCode("admin_library");
        directory.setMenuCode("admin_library");
        directory.setPageName("Library");
        directory.setPageScope("admin");
        directory.setPageType("menu_directory");
        directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"admin_library\",\"menuName\":\"Library\",\"menuOrder\":1}}");

        FrontProjectPageDesign books = new FrontProjectPageDesign();
        books.setPageCode("admin_books");
        books.setMenuCode("admin_library");
        books.setPageName("Books");
        books.setRoutePath("/admin/books");
        books.setPageScope("admin");
        books.setPageType("list");
        books.setTableName("book_info");
        books.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"secondary\",\"parentMenuCode\":\"admin_library\",\"parentMenuName\":\"Library\",\"menuOrder\":1}}");

        FrontProjectPageDesign users = new FrontProjectPageDesign();
        users.setPageCode("admin_users");
        users.setMenuCode("admin_users");
        users.setPageName("Users");
        users.setRoutePath("/admin/users");
        users.setPageScope("admin");
        users.setPageType("list");
        users.setTableName("user_info");
        users.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"admin_users\",\"menuName\":\"Users\",\"menuOrder\":2}}");
        project.setPageDesigns(Arrays.asList(directory, books, users));

        VelocityContext context = VelocityUtils.prepareContextProject(project);

        assertEquals(true, context.get("hasAdminNavGroups"));
        assertEquals("/admin/books", context.get("defaultAdminPath"));
        List<Map<String, Object>> groups = (List<Map<String, Object>>) context.get("adminNavGroups");
        assertEquals(2, groups.size());
        assertEquals("Library", groups.get(0).get("name"));
        assertEquals(true, groups.get(0).get("directory"));
        assertEquals(true, groups.get(0).get("hasChildren"));
        List<Map<String, Object>> children = (List<Map<String, Object>>) groups.get(0).get("children");
        assertEquals("Books", children.get(0).get("name"));
        assertEquals("/admin/books", children.get(0).get("path"));

        List<Map<String, Object>> routeItems = (List<Map<String, Object>>) context.get("adminRouteItems");
        assertEquals(2, routeItems.size());
        assertEquals("/admin/books", routeItems.get(0).get("path"));
        assertEquals("Book", routeItems.get(0).get("componentName"));
        assertEquals("/admin/users", routeItems.get(1).get("path"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void qingAdminContextRoutesBusinessBlockHomePageDesign()
    {
        GenProject project = project();
        FrontProjectPageDesign home = new FrontProjectPageDesign();
        home.setPageCode("admin_home");
        home.setMenuCode("admin_home");
        home.setPageName("Home");
        home.setRoutePath("/admin/home");
        home.setPageScope("admin");
        home.setPageType("business_block");
        home.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[],\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"admin_home\",\"menuName\":\"Home\",\"menuOrder\":1}}");
        project.setPageDesigns(Arrays.asList(home));

        VelocityContext context = VelocityUtils.prepareContextProject(project);

        assertEquals("/admin/home", context.get("defaultAdminPath"));
        List<Map<String, Object>> routeItems = (List<Map<String, Object>>) context.get("adminRouteItems");
        assertEquals(1, routeItems.size());
        assertEquals("/admin/home", routeItems.get(0).get("path"));
        assertEquals("AdminHome", routeItems.get(0).get("componentName"));
        assertEquals("@/views/pages/admin_home/index.vue", routeItems.get(0).get("componentPath"));
    }

    @Test
    public void qingPortalSkipsMenuDirectoryPageDesignsAsRoutes()
    {
        GenProject project = project();
        FrontProjectPageDesign directory = new FrontProjectPageDesign();
        directory.setPageCode("library_directory");
        directory.setMenuCode("library_directory");
        directory.setPageName("Library");
        directory.setPageScope("frontend");
        directory.setPageType("menu_directory");
        directory.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"library_directory\",\"menuName\":\"Library\"}}");
        project.setPageDesigns(Arrays.asList(directory));

        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContextProject(project);

        String app = render("qing/vue-app.vue.vm", context);
        assertTrue(app.contains("Library"));
        assertTrue(app.contains("portal-nav-directory"));

        String router = render("qing/vue-router.js.vm", context);
        assertFalse(router.contains("LibraryDirectory"));
        assertFalse(router.contains("library_directory"));
    }

    @Test
    public void qingIndexTemplateUsesFrontendPageDesignFields()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("product_page");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"regions\":["
                + "{\"id\":\"query\",\"type\":\"query\",\"fields\":[\"productName\"]},"
                + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"create\"]},"
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"edit\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
                + "]}");
        design.setActionJson("{\"toolbarActions\":[{\"code\":\"create\",\"label\":\"Create\",\"type\":\"primary\"}],"
                + "\"rowActions\":[{\"code\":\"edit\",\"label\":\"Edit\",\"type\":\"primary\"}]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String rendered = render("qing/index.vue.vm", context);

        assertTrue(rendered.contains("prop=\"productName\""));
        assertFalse(rendered.contains("prop=\"productId\" label=\"ID\""));
        assertTrue(rendered.contains("@click=\"handleAdd\""));
        assertTrue(rendered.contains("@click=\"handleEdit(scope.row)\""));
        assertFalse(rendered.contains("@confirm=\"del(scope.row.productId)\""));
    }

    @Test
    public void qingIndexTemplateRendersDesignedViewAction()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("product_page");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"regions\":["
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
                + "]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String rendered = render("qing/index.vue.vm", context);

        assertTrue(rendered.contains("@click=\"handleView(scope.row)\""));
        assertTrue(rendered.contains("viewing: false"));
        assertTrue(rendered.contains("handleView(row)"));
        assertTrue(rendered.contains(":disabled=\"viewing\""));
    }

    @Test
    public void qingIndexTemplateRoutesConfiguredViewActionToAssociatedDetailPage()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign listDesign = new FrontProjectPageDesign();
        listDesign.setPageCode("product_page");
        listDesign.setTableName("demo_product");
        listDesign.setRoutePath("/products");
        listDesign.setLayoutJson("{\"regions\":["
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
                + "],\"interactions\":{\"actions\":{\"table:view\":{\"type\":\"route\",\"targetPageCode\":\"product_detail_page\",\"paramMap\":{\"id\":\"productId\"},\"fallback\":\"dialog\"}}}}");
        FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
        detailDesign.setPageCode("product_detail_page");
        detailDesign.setTableName("demo_product");
        detailDesign.setPageType("detail");
        detailDesign.setRoutePath("/products/:id");
        detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
        project.setPageDesigns(Arrays.asList(listDesign, detailDesign));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_page");

        String rendered = render("qing/index.vue.vm", context);

        assertTrue(rendered.contains("\"table:view\": {"));
        assertTrue(rendered.contains("path: \"/products/:id\""));
        assertTrue(rendered.contains("paramMap: {\"id\":\"productId\"}"));
        assertTrue(rendered.contains("if (this.handleConfiguredRoute(\"table:view\", row))"));
        assertTrue(rendered.contains("this.$router.push(path)"));
    }

    @Test
    public void qingIndexTemplateRepairsLegacyDetailRouteAndInfersViewParamMap()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign listDesign = new FrontProjectPageDesign();
        listDesign.setPageCode("product_page");
        listDesign.setTableName("demo_product");
        listDesign.setRoutePath("/products");
        listDesign.setLayoutJson("{\"regions\":["
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]}"
                + "],\"interactions\":{\"actions\":{\"table:view\":{\"type\":\"route\",\"targetPageCode\":\"product_detail_page\",\"paramMap\":{}}}}}");
        FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
        detailDesign.setPageCode("product_detail_page");
        detailDesign.setTableName("demo_product");
        detailDesign.setPageType("detail");
        detailDesign.setRoutePath("/products/-id");
        detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
        project.setPageDesigns(Arrays.asList(listDesign, detailDesign));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_page");

        String rendered = render("qing/index.vue.vm", context);

        assertTrue(rendered.contains("path: \"/products/:id\""));
        assertTrue(rendered.contains("paramMap: {\"id\":\"productId\"}"));
        assertFalse(rendered.contains("path: \"/products/-id\""));
    }

    @Test
    public void qingIndexTemplateRendersDetailPageAsSingleRecord()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign detailDesign = new FrontProjectPageDesign();
        detailDesign.setPageCode("product_detail_page");
        detailDesign.setTableName("demo_product");
        detailDesign.setPageType("detail");
        detailDesign.setRoutePath("/products/:id");
        detailDesign.setLayoutJson("{\"navigation\":{\"showInMenu\":false,\"parentPageCode\":\"product_page\"}}");
        project.setPageDesigns(Arrays.asList(detailDesign));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", "product_detail_page");

        String rendered = render("qing/index.vue.vm", context);

        assertTrue(rendered.contains("detailRecord: {}"));
        assertTrue(rendered.contains("loadDetail()"));
        assertTrue(rendered.contains("const detailId = this.resolveDetailId()"));
        assertTrue(rendered.contains("this.request.get(\"/demo/product/\" + detailId)"));
        assertTrue(rendered.contains("{{ formatDetailValue(detailRecord.productName) }}"));
        assertFalse(rendered.contains("<el-table :data=\"productList\""));
        assertFalse(rendered.contains("this.request.get(\"/demo/product/page\""));
    }

    @Test
    public void qingCurrentUserListAddsUserFilterFlagAndControllerFilter()
    {
        GenTable table = renderTable();
        GenTableColumn userId = column("user_id", "用户ID", "Long", "userId", "0", "1", "1", "1", "EQ", "input");
        List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
        columns.add(userId);
        table.setColumns(columns);
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("my_products_page");
        design.setPageType("current_user_list");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"canvas\":\"frontend-current-user-list-v1\",\"regions\":["
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"view\"]}"
                + "]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String page = render("qing/index.vue.vm", context);
        assertTrue(page.contains("__currentUserOnly: true"));

        String controller = render("qing/controller.java.vm", context);
        assertTrue(controller.contains("PortalAuthTokenStore"));
        assertTrue(controller.contains("HttpServletRequest request"));
        assertTrue(controller.contains("queryWrapper.eq(\"user_id\", currentUser.getUserId())"));
    }

    @Test
    public void qingIndexTemplateUsesCardLayoutFromFrontendPageDesign()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("product_page");
        design.setPageType("list");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
                + "{\"id\":\"query\",\"type\":\"query\",\"fields\":[\"productName\"]},"
                + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"create\"]},"
                + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"productName\"],\"rowActions\":[\"edit\",\"delete\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
                + "]}");
        design.setActionJson("{\"toolbarActions\":[{\"code\":\"create\",\"label\":\"Create\",\"type\":\"primary\"}],"
                + "\"rowActions\":[{\"code\":\"edit\",\"label\":\"Edit\",\"type\":\"primary\"},{\"code\":\"delete\",\"label\":\"Delete\",\"type\":\"danger\"}]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String rendered = render("qing/index.vue.vm", context);

        assertEquals("frontend-list-card-v1", context.get("pageDesignCanvas"));
        assertTrue(rendered.contains("class=\"resource-card-grid\""));
        assertTrue(rendered.contains("class=\"resource-card-item\""));
        assertFalse(rendered.contains("class=\"resource-table\""));
        assertTrue(rendered.contains("@click=\"handleEdit(item)\""));
        assertTrue(rendered.contains("@confirm=\"del(item.productId)\""));
    }

    @Test
    public void qingIndexTemplateUsesCardRegionAndHiddenQueryFromFrontendPageDesign()
    {
        GenTable table = renderTable();
        GenTableColumn cover = column("cover_url", "封面", "String", "coverUrl", "0", "1", "1", "1", "EQ", "imageUpload");
        List<GenTableColumn> columns = new ArrayList<GenTableColumn>(table.getColumns());
        columns.add(cover);
        table.setColumns(columns);
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("product_page");
        design.setPageType("list");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
                + "{\"id\":\"query\",\"type\":\"query\",\"visible\":false,\"fields\":[\"productName\"]},"
                + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"create\"]},"
                + "{\"id\":\"card\",\"type\":\"cardList\",\"titleField\":\"productName\",\"imageField\":\"coverUrl\",\"fields\":[\"productName\"]},"
                + "{\"id\":\"cardActions\",\"type\":\"actions\",\"actions\":[\"edit\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"productName\"]}"
                + "]}");
        design.setActionJson("{\"toolbarActions\":[{\"code\":\"create\",\"label\":\"Create\",\"type\":\"primary\"}],"
                + "\"rowActions\":[{\"code\":\"edit\",\"label\":\"Edit\",\"type\":\"primary\"}]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String rendered = render("qing/index.vue.vm", context);

        assertEquals(false, context.get("pageDesignShowQuery"));
        assertEquals(cover, context.get("pageDesignCardImageColumn"));
        assertTrue(rendered.contains("class=\"resource-card-image\""));
        assertTrue(rendered.contains(":src=\"resolveResourceUrl(item.coverUrl)\""));
        assertFalse(rendered.contains("class=\"filter-bar\""));
        assertFalse(rendered.contains("productName: undefined"));
        assertTrue(rendered.contains("@click=\"handleEdit(item)\""));
    }

    @Test
    public void qingIndexTemplateKeepsExplicitCardEditWhenFormRegionHidden()
    {
        GenTable table = renderTable();
        GenProject project = project();
        project.setTables(Arrays.asList(table));
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setPageCode("product_page");
        design.setPageType("list");
        design.setTableName("demo_product");
        design.setLayoutJson("{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
                + "{\"id\":\"card\",\"type\":\"cardList\",\"titleField\":\"productName\",\"fields\":[\"productName\"]},"
                + "{\"id\":\"cardActions\",\"type\":\"actions\",\"actions\":[\"edit\",\"delete\"]},"
                + "{\"id\":\"form\",\"type\":\"dialogForm\",\"visible\":false,\"fields\":[\"productName\"]}"
                + "]}");
        project.setPageDesigns(Arrays.asList(design));
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table);

        String rendered = render("qing/index.vue.vm", context);

        assertEquals(false, context.get("pageDesignShowForm"));
        assertTrue(rendered.contains("@click=\"handleEdit(item)\""));
        assertTrue(rendered.contains("@confirm=\"del(item.productId)\""));
        assertTrue(rendered.contains("<el-dialog"));
    }

    @Test
    public void qingResourceTemplatesUseAssignedPorts() throws Exception
    {
        Path resourceRoot = repoRoot().resolve("ruoyi-generator/src/main/resources/qing");
        String applicationYml = normalizeNewlines(read(resourceRoot.resolve("application.yml.vm")));
        String vueConfig = normalizeNewlines(read(resourceRoot.resolve("vue-config.js.vm")));
        String readme = normalizeNewlines(read(resourceRoot.resolve("readme.md.vm")));

        assertTrue("Qing backend template should allow preview to override the port",
                applicationYml.contains("server:\n  port: ${d}{SERVER_PORT:8080}"));
        assertFalse("Qing backend template should not keep the old 9090 port",
                applicationYml.contains("port: 9090"));

        assertTrue("Qing admin frontend template should allow preview to override the port",
                vueConfig.contains("const port = process.env.PORT || 8081"));
        assertTrue("Qing frontend proxy should allow preview to override the backend URL",
                vueConfig.contains("const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || \"http://localhost:8080\""));
        assertTrue("Qing frontend proxy should use the resolved backend URL",
                vueConfig.contains("target: apiBaseUrl"));
        assertFalse("Qing frontend proxy should not target the old backend port 9090",
                vueConfig.contains("http://localhost:9090"));

        assertTrue("Qing README should document backend port 8080",
                readme.contains("http://localhost:8080"));
        assertFalse("Qing README should not document the old backend port 9090",
                readme.contains("http://localhost:9090"));
    }

    @Test
    public void qingDatabaseScriptRegistersTemplateAndFiles() throws Exception
    {
        Path script = repoRoot().resolve("sql/qing_templates.sql");
        assertTrue("qing template SQL script should exist", Files.exists(script));
        String sql = read(script);

        assertTrue(sql.contains("INSERT INTO sys_template"));
        assertTrue(sql.contains("'Runnable Backend Template'"));
        assertTrue(sql.contains("'Runnable Admin Frontend Template'"));
        assertTrue(sql.contains("'Runnable Portal Frontend Template'"));
        assertTrue(sql.contains("'/runnable/backend'"));
        assertTrue(sql.contains("'/runnable/admin_frontend'"));
        assertTrue(sql.contains("'/runnable/frontend'"));
        assertTrue(sql.contains("'backend'"));
        assertTrue(sql.contains("'admin_frontend'"));
        assertTrue(sql.contains("'frontend'"));
        assertTrue(sql.contains("INSERT INTO sys_template_file"));
        assertTrue(sql.contains("INSERT INTO sys_project_structure"));
        assertFalse("Database template file names and paths should not include qing/",
                sql.contains("qing/"));

        assertTrue(sql.contains("'Application.java.vm'"));
        assertTrue(sql.contains("'Result.java.vm'"));
        assertTrue(sql.contains("'MybatisPlusConfig.java.vm'"));
        assertTrue(sql.contains("'FileController.java.vm'"));
        assertTrue(sql.contains("'AdminUser.java.vm'"));
        assertTrue(sql.contains("'AdminUserMapper.java.vm'"));
        assertTrue(sql.contains("'PortalUser.java.vm'"));
        assertTrue(sql.contains("'PortalUserMapper.java.vm'"));
        assertTrue(sql.contains("'PortalAuthTokenStore.java.vm'"));
        assertTrue(sql.contains("'PortalAuthInterceptor.java.vm'"));
        assertTrue(sql.contains("'AdminAuthController.java.vm'"));
        assertTrue(sql.contains("'PortalAuthController.java.vm'"));
        assertTrue(sql.contains("\"/business-blocks/**\""));
        assertTrue(sql.contains("create table if not exists t_admin"));
        assertTrue(sql.contains("insert into t_admin"));
        assertTrue(sql.contains("select ''admin''"));
        assertTrue(sql.contains("where not exists (select 1 from t_admin where username = ''admin'')"));
        assertTrue(sql.contains("create table if not exists t_user"));
        assertTrue(sql.contains("insert into t_user"));
        assertTrue(sql.contains("select ''user''"));
        assertTrue(sql.contains("where not exists (select 1 from t_user where username = ''user'')"));
        assertTrue(sql.contains("password_hash"));
        assertFalse(sql.contains("portal_user"));
        assertTrue(sql.contains("'entity.java.vm'"));
        assertTrue(sql.contains("'controller.java.vm'"));
        assertTrue(sql.contains("'mapper.xml.vm'"));
        assertTrue(sql.contains("'package.json.vm'"));
        assertTrue(sql.contains("'admin-main.js.vm'"));
        assertTrue(sql.contains("'admin-router.js.vm'"));
        assertTrue(sql.contains("'admin-login.vue.vm'"));
        assertTrue(sql.contains("'admin-index.vue.vm'"));
        assertTrue(sql.contains("class=\"admin-shell\""));
        assertTrue(sql.contains("class=\"admin-sidebar\""));
        assertTrue(sql.contains("path: \"/login\""));
        assertTrue(sql.contains("this.request.post(\"/auth/admin/login\""));
        assertTrue(sql.contains("localStorage.setItem(\"admin-token\""));
        assertTrue(sql.contains("localStorage.getItem(\"admin-token\")"));
        assertTrue(sql.contains("Authorization"));
        assertTrue(sql.contains("this.request.post(\"/auth/admin/logout\""));
        assertFalse(sql.contains("adminLoggedIn"));
        assertTrue(sql.contains("欢迎登录${projectName}后台"));
        assertTrue(sql.contains("'frontend-main.js.vm'"));
        assertTrue(sql.contains("'frontend-router.js.vm'"));
        assertTrue(sql.contains("'frontend-login.vue.vm'"));
        assertTrue(sql.contains("'frontend-index.vue.vm'"));
        assertTrue("Frontend SQL template should include page design context variables",
                sql.contains("$hasCurrentFrontendPageDesign"));
        assertTrue("Frontend SQL template should include card layout rendering",
                sql.contains("resource-card-grid"));
        assertTrue("Frontend SQL template should support hidden query regions",
                sql.contains("$pageDesignShowQuery"));
        assertTrue("Frontend SQL template should render page-designed business buttons",
                sql.contains("handleBusinessAction"));
        assertTrue("Frontend SQL template should serialize page-designed business actions",
                sql.contains("businessActionMap"));
        assertTrue("Frontend SQL template should serialize business button labels",
                sql.contains("$action.buttonLabel"));
        assertTrue("Frontend SQL template should serialize business button styles",
                sql.contains("type=\"${buttonType}\""));
        assertTrue("Frontend SQL template should serialize business button icons",
                sql.contains("icon=\"${buttonIcon}\""));
        assertTrue("Frontend SQL template should execute configured business actions",
                sql.contains("executeBusinessAction"));
        assertTrue("Frontend SQL template should refresh configured business action target",
                sql.contains("refreshBusinessActionTarget"));
        assertTrue("Frontend SQL template should support business action confirmation",
                sql.contains("confirmMessage"));
        assertTrue("Frontend SQL template should support business action success messages",
                sql.contains("successMessage"));
        assertTrue("Frontend SQL template should support business action refresh targets",
                sql.contains("refreshTarget"));
        assertTrue("Frontend SQL template should render detail pages as a single record",
                sql.contains("detailRecord: {}"));
        assertTrue("Frontend SQL template should load detail pages by route id",
                sql.contains("loadDetail()"));
        assertTrue("Frontend SQL template should avoid SQL-fragile route param regex literals",
                sql.contains("hasUnresolvedRouteParam(path)"));
        assertFalse("Frontend SQL template should not keep escaped regex literals that MySQL can unescape",
                sql.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
        assertFalse("Frontend SQL template should not contain already-broken route regex literals",
                sql.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
        assertTrue("Frontend SQL template should resolve uploaded image resources through the preview proxy",
                sql.contains("resolveResourceUrl(url)"));
        assertTrue(sql.contains(":src=\"resolveResourceUrl(scope.row.${javaField})\""));
        assertTrue(sql.contains(":src=\"resolveResourceUrl(form.${javaField})\""));
        assertTrue(sql.contains(":src=\"resolveResourceUrl(item.${cardImageColumn.javaField})\""));
        assertFalse(sql.contains(":src=\"scope.row.${javaField}\""));
        assertFalse(sql.contains(":src=\"form.${javaField}\""));
        assertFalse(sql.contains(":src=\"item.${cardImageColumn.javaField}\""));
        assertTrue(sql.contains("${projectName}"));
        assertTrue(sql.contains("#foreach($group in $frontendNavGroups)"));
        assertTrue(sql.contains("portal-nav-group#if($group.hasChildren) has-children#end"));
        assertTrue(sql.contains(":class=''{ active: isNavGroupActive(${group.activePathsLiteral}) }''"));
        assertTrue(sql.contains(":class=''{ active: isSidebarGroupActive(${group.activePathsLiteral}) }''"));
        assertFalse(sql.contains(":class=\"{ active: isNavGroupActive"));
        assertFalse(sql.contains(":class=\"{ active: isSidebarGroupActive"));
        assertTrue(sql.contains("frontendRouteItems"));
        assertFalse(sql.contains("游戏资源平台"));
        assertFalse(sql.contains("游戏攻略"));
        assertTrue(sql.contains("class=\"portal-login\""));
        assertTrue(sql.contains("this.request.post(\"/auth/login\""));
        assertTrue(sql.contains("localStorage.setItem(\"portal-token\""));
        assertTrue(sql.contains("'Vue2 + Element UI runnable portal frontend'"));
        assertTrue(sql.contains("\"vue\": \"^2.6.14\""));
        assertTrue(sql.contains("\"vue-router\": \"^3.6.5\""));
        assertTrue(sql.contains("\"element-ui\": \"^2.15.14\""));
        assertTrue(sql.contains("new Vue({"));
        assertTrue(sql.contains("Vue.use(Router)"));
        assertFalse(sql.contains("Vue3"));
        assertFalse(sql.contains("vite.config.js"));
        assertFalse(sql.contains("@vitejs/plugin-vue"));
        assertFalse(sql.contains("createApp("));
        assertFalse(sql.contains("createWebHashHistory"));
        assertVueCli4ConfigCompatible(sql, "sql/qing_templates.sql");
        assertRunnableTemplatePorts(sql, "sql/qing_templates.sql");

        assertTrue(sql.contains("'{projectName}-backend'"));
        assertTrue(sql.contains("'{projectName}-admin'"));
        assertTrue(sql.contains("'{projectName}-web'"));
        assertTrue(sql.contains("'{packagePath}'"));
        assertTrue(sql.contains("'{businessName}.sql'"));
        assertTrue(sql.contains("'{projectClassName}Application.java'"));
        assertTrue(sql.contains("'index.vue'"));
        assertFalse(sql.contains("'src/main/java/{packageName}/Application.java'"));
        assertFalse(sql.contains("'src/views/{moduleName}/{businessName}/index.vue'"));
        assertProjectStructureNodeNamesFitSchema(sql);
        assertTrue("SQL should escape template backslashes for MySQL string literals",
                sql.contains("filename=\\\\\""));

        String dbSql = read(repoRoot().resolve("sql/db.sql"));
        assertTrue("db.sql should include runnable backend template",
                dbSql.contains("'Runnable Backend Template'"));
        assertTrue("db.sql should include runnable admin frontend template",
                dbSql.contains("'Runnable Admin Frontend Template'"));
        assertTrue("db.sql should include runnable portal frontend template",
                dbSql.contains("'Runnable Portal Frontend Template'"));
        assertTrue("db.sql should include Vue2 portal frontend template",
                dbSql.contains("'Vue2 + Element UI runnable portal frontend'"));
        assertTrue("db.sql should include runnable portal login template",
                dbSql.contains("'frontend-login.vue.vm'"));
        assertTrue("db.sql should include page design-aware frontend template",
                dbSql.contains("$hasCurrentFrontendPageDesign"));
        assertTrue("db.sql should include card layout frontend template",
                dbSql.contains("resource-card-grid"));
        assertTrue("db.sql should include page-designed business buttons",
                dbSql.contains("handleBusinessAction"));
        assertTrue("db.sql should serialize business button labels",
                dbSql.contains("$action.buttonLabel"));
        assertTrue("db.sql should serialize business button styles",
                dbSql.contains("type=\"${buttonType}\""));
        assertTrue("db.sql should serialize business button icons",
                dbSql.contains("icon=\"${buttonIcon}\""));
        assertTrue("db.sql should execute configured business actions",
                dbSql.contains("executeBusinessAction"));
        assertTrue("db.sql should refresh configured business action target",
                dbSql.contains("refreshBusinessActionTarget"));
        assertTrue("db.sql should support business action confirmation",
                dbSql.contains("confirmMessage"));
        assertTrue("db.sql should support business action success messages",
                dbSql.contains("successMessage"));
        assertTrue("db.sql should support business action refresh targets",
                dbSql.contains("refreshTarget"));
        assertTrue("db.sql should render detail pages as a single record",
                dbSql.contains("detailRecord: {}"));
        assertTrue("db.sql should load detail pages by route id",
                dbSql.contains("loadDetail()"));
        assertTrue("db.sql should avoid SQL-fragile route param regex literals",
                dbSql.contains("hasUnresolvedRouteParam(path)"));
        assertFalse("db.sql should not keep escaped regex literals that MySQL can unescape",
                dbSql.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
        assertFalse("db.sql should not contain already-broken route regex literals",
                dbSql.contains("return /(^|/):[^/]+/.test(path) ? \"\" : path"));
        assertTrue("db.sql should include runnable portal auth backend",
                dbSql.contains("'PortalAuthController.java.vm'"));
        assertTrue("db.sql should allow public business block APIs",
                dbSql.contains("\"/business-blocks/**\""));
        assertTrue("db.sql should resolve uploaded image resources through the preview proxy",
                dbSql.contains("resolveResourceUrl(url)"));
        assertTrue(dbSql.contains(":src=\"resolveResourceUrl(scope.row.${javaField})\""));
        assertTrue(dbSql.contains(":src=\"resolveResourceUrl(form.${javaField})\""));
        assertTrue(dbSql.contains(":src=\"resolveResourceUrl(item.${cardImageColumn.javaField})\""));
        assertFalse(dbSql.contains(":src=\"scope.row.${javaField}\""));
        assertFalse(dbSql.contains(":src=\"form.${javaField}\""));
        assertFalse(dbSql.contains(":src=\"item.${cardImageColumn.javaField}\""));
        assertTrue("db.sql should include fixed admin table",
                dbSql.contains("create table if not exists t_admin"));
        assertTrue("db.sql should include default admin account",
                dbSql.contains("select ''admin''"));
        assertTrue("db.sql should seed admin only when missing",
                dbSql.contains("where not exists (select 1 from t_admin where username = ''admin'')"));
        assertTrue("db.sql should include portal user auth table",
                dbSql.contains("create table if not exists t_user"));
        assertTrue("db.sql should include default portal user account",
                dbSql.contains("select ''user''"));
        assertTrue("db.sql should seed portal user only when missing",
                dbSql.contains("where not exists (select 1 from t_user where username = ''user'')"));
        assertFalse("db.sql should not keep portal_user auth table",
                dbSql.contains("portal_user"));
        assertTrue("db.sql should include admin login page template",
                dbSql.contains("'admin-login.vue.vm'"));
        assertTrue("db.sql should include uploaded-image-like admin shell layout",
                dbSql.contains("class=\"admin-shell\""));
        assertTrue("db.sql should render admin menu groups from page designer",
                dbSql.contains("#foreach($group in $adminNavGroups)"));
        assertTrue("db.sql should include admin route items from page designer",
                dbSql.contains("#foreach($item in $adminRouteItems)"));
        assertTrue("db.sql should route to admin login",
                dbSql.contains("path: \"/login\""));
        assertTrue("db.sql should include real admin login call",
                dbSql.contains("this.request.post(\"/auth/admin/login\""));
        assertTrue("db.sql should store admin auth token",
                dbSql.contains("localStorage.setItem(\"admin-token\""));
        assertFalse("db.sql should not keep local fake admin login flag",
                dbSql.contains("adminLoggedIn"));
        assertTrue("db.sql should include runnable project structures",
                dbSql.contains("node_id BETWEEN 920100 AND 920399"));
        assertVueCli4ConfigCompatible(dbSql, "sql/db.sql");
        assertRunnableTemplatePorts(dbSql, "sql/db.sql");
    }

    @Test
    public void qingDatabaseSeedsContainCurrentBusinessActionTemplates() throws Exception
    {
        Path resourceRoot = repoRoot().resolve("ruoyi-generator/src/main/resources/qing");
        String qingSql = normalizeNewlines(read(repoRoot().resolve("sql/qing_templates.sql")));
        String databaseSql = normalizeNewlines(read(repoRoot().resolve("sql/db.sql")));
        assertFalse("db.sql should not fall back from effect condition fields to action request fields",
                databaseSql.contains("#set($conditionFields = $action.requestFields)"));

        assertSeedContainsBundledTemplate(qingSql, resourceRoot, "service.java.vm", "service.java.vm");
        assertSeedContainsBundledTemplate(qingSql, resourceRoot, "serviceImpl.java.vm", "serviceImpl.java.vm");
        assertSeedContainsBundledTemplate(qingSql, resourceRoot, "controller.java.vm", "controller.java.vm");
        assertSeedContainsBundledTemplate(qingSql, resourceRoot, "index.vue.vm", "frontend-index.vue.vm");

        assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "service.java.vm", "service.java.vm");
        assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "serviceImpl.java.vm", "serviceImpl.java.vm");
        assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "controller.java.vm", "controller.java.vm");
        assertSeedContainsBundledTemplate(databaseSql, resourceRoot, "index.vue.vm", "frontend-index.vue.vm");
    }

    @Test
    public void projectStructurePackagePathPlaceholderBuildsJavaDirectories() throws Exception
    {
        GenProject project = new GenProject();
        project.setProjectFileName("demo-product");
        project.setPackageName("com.example.demo");

        Method replaceNodeName = GenProjectServiceImpl.class.getDeclaredMethod(
                "replaceNodeName", String.class, GenProject.class, GenTable.class);
        replaceNodeName.setAccessible(true);

        assertEquals("com/example/demo", replaceNodeName.invoke(
                new GenProjectServiceImpl(), "{packagePath}", project, null));
    }

    private GenTable table()
    {
        GenTable table = new GenTable();
        table.setPackageName("com.example.demo");
        table.setModuleName("demo");
        table.setClassName("DemoProduct");
        table.setBusinessName("product");
        return table;
    }

    private GenProject project()
    {
        GenProject project = new GenProject();
        project.setProjectId(1L);
        project.setProjectName("Demo App");
        project.setProjectFileName("demo-app");
        project.setPackageName("com.example.demo");
        return project;
    }

    private AppMenuDesign menu(String code, String name, String path, boolean requiresLogin)
    {
        AppMenuDesign menu = new AppMenuDesign();
        menu.setCode(code);
        menu.setName(name);
        menu.setPath(path);
        menu.setRequiresLogin(requiresLogin);
        return menu;
    }

    private GenTable renderTable()
    {
        GenTable table = table();
        table.setTableName("demo_product");
        table.setFunctionName("演示商品");
        table.setFunctionAuthor("Codex");
        table.setTplCategory("crud");
        table.setOptions("{}");
        table.setCreateTableSql("CREATE TABLE demo_product (product_id bigint primary key, product_name varchar(100));");

        GenTableColumn pk = column("product_id", "商品ID", "Long", "productId", "1", "0", "0", "0", "EQ", "input");
        GenTableColumn name = column("product_name", "商品名称", "String", "productName", "0", "1", "1", "1", "LIKE", "input");
        table.setPkColumn(pk);
        table.setColumns(Arrays.asList(pk, name));
        return table;
    }

    private GenTableColumn column(String columnName, String comment, String javaType, String javaField,
                                  String isPk, String isInsert, String isEdit, String isList,
                                  String queryType, String htmlType)
    {
        GenTableColumn column = new GenTableColumn();
        column.setColumnName(columnName);
        column.setColumnComment(comment);
        column.setJavaType(javaType);
        column.setJavaField(javaField);
        column.setIsPk(isPk);
        column.setIsIncrement(isPk);
        column.setIsRequired("0");
        column.setIsInsert(isInsert);
        column.setIsEdit(isEdit);
        column.setIsList(isList);
        column.setIsQuery(isList);
        column.setQueryType(queryType);
        column.setHtmlType(htmlType);
        column.setDictType("");
        return column;
    }

    private BusinessActionDesign businessAction()
    {
        BusinessActionDesign action = new BusinessActionDesign();
        action.setCode("borrow_book");
        action.setName("借书");
        action.setOwnerTable("demo_product");
        action.setMethod("POST");
        action.setPath("/library/borrow/borrow");
        action.setTransaction(true);
        action.setRequestFields(Arrays.asList("product_id", "due_date"));

        BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
        effect.setType("INSERT_ROW");
        effect.setTargetTable("borrow_record");
        Map<String, String> values = new LinkedHashMap<String, String>();
        values.put("product_id", "${param.product_id}");
        values.put("user_id", "${current_user.id}");
        values.put("due_date", "${param.due_date}");
        effect.setValues(values);
        action.setEffects(Arrays.asList(effect));
        return action;
    }

    private Path repoRoot()
    {
        Path cwd = Paths.get("").toAbsolutePath();
        if ("ruoyi-generator".equals(cwd.getFileName().toString()))
        {
            return cwd.getParent();
        }
        return cwd;
    }

    private String read(Path path) throws Exception
    {
        return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
    }

    private String render(String templateName, VelocityContext context)
    {
        Template template = Velocity.getTemplate(templateName, "UTF-8");
        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        return writer.toString();
    }

    private String normalizeNewlines(String text)
    {
        return text.replace("\r\n", "\n");
    }

    private void assertSeedContainsBundledTemplate(String sql, Path resourceRoot, String resourceName,
            String databaseFileName) throws Exception
    {
        String content = normalizeNewlines(read(resourceRoot.resolve(resourceName))).replace("'", "''");
        String recordFragment = "'" + databaseFileName + "', NULL, '" + databaseFileName + "', '" + content
                + "', 'admin', sysdate())";
        assertTrue(databaseFileName + " should match the bundled Qing template", sql.contains(recordFragment));
    }

    private void assertProjectStructureNodeNamesFitSchema(String sql)
    {
        Matcher matcher = Pattern.compile("^\\(\\d+, \\d+, '([^']*)', '", Pattern.MULTILINE).matcher(sql);
        int nodeCount = 0;
        while (matcher.find())
        {
            nodeCount++;
            String nodeName = matcher.group(1);
            assertTrue("Project structure node_name must fit varchar(50): " + nodeName,
                    nodeName.length() <= 50);
        }
        assertTrue("Should find project structure rows", nodeCount > 0);
    }

    private void assertVueCli4ConfigCompatible(String sql, String label)
    {
        assertFalse(label + " should not use defineConfig in Vue CLI 4 vue.config.js templates",
                sql.contains("defineConfig"));
        assertFalse(label + " should not use boolean transpileDependencies in Vue CLI 4 vue.config.js templates",
                sql.contains("transpileDependencies: true"));
        assertTrue(label + " should use array transpileDependencies in Vue CLI 4 vue.config.js templates",
                sql.contains("transpileDependencies: []"));
    }

    private void assertRunnableTemplatePorts(String sql, String label)
    {
        String normalized = normalizeNewlines(sql);
        assertTrue(label + " backend template should allow preview to override port 8080",
                normalized.contains("(920108, 9201, 'application.yml.vm', NULL, 'application.yml.vm', '#set($d = ''$'')\nserver:\n  port: ${d}{SERVER_PORT:8080}"));
        assertTrue(label + " backend README should document port 8080",
                normalized.contains("The service starts at `http://localhost:8080`."));
        assertTrue(label + " admin frontend template should allow preview to override port 8081",
                normalized.contains("const port = process.env.PORT || 8081"));
        assertTrue(label + " portal frontend template should allow preview to override port 8082",
                normalized.contains("const port = process.env.PORT || 8082"));
        assertTrue(label + " frontend templates should allow preview to override backend proxy",
                normalized.contains("const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || \"http://localhost:8080\""));
        assertTrue(label + " frontend templates should use the resolved backend proxy",
                normalized.contains("target: apiBaseUrl"));

        assertFalse(label + " should not keep old backend port 9090",
                normalized.contains("port: 9090"));
        assertFalse(label + " should not proxy to old backend port 9090",
                normalized.contains("http://localhost:9090"));
        assertFalse(label + " should not keep old portal frontend port 5173",
                normalized.contains("port: 5173"));
    }
}
