package com.ruoyi.generator.service;

import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.GenProject;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.domain.SysCodeSnippet;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.SysProjectStructure;
import com.ruoyi.generator.domain.SysProjectTemplate;
import com.ruoyi.generator.domain.Template;
import com.ruoyi.generator.domain.TemplateFile;
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionUiBindingDesign;
import com.ruoyi.generator.mapper.GenProjectMapper;
import com.ruoyi.generator.mapper.GenTableMapper;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.Field;
import java.io.ByteArrayInputStream;
import java.io.IOException;
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.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

public class GenProjectServiceImplTest {
    private GenProjectServiceImpl service;

    @Mock
    private GenProjectMapper genProjectMapper;
    @Mock
    private GenTableMapper genTableMapper;
    @Mock
    private ITemplateService templateService;
    @Mock
    private ITemplateFileService templateFileService;
    @Mock
    private ISysCodeSnippetService codeSnippetService;
    @Mock
    private ISysProjectModuleService sysProjectModuleService;
    @Mock
    private ISysProjectTemplateService sysProjectTemplateService;
    @Mock
    private ISysProjectStructureService sysProjectStructureService;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        service = new GenProjectServiceImpl();
        setField("genProjectMapper", genProjectMapper);
        setField("genTableMapper", genTableMapper);
        setField("templateService", templateService);
        setField("templateFileService", templateFileService);
        setField("codeSnippetService", codeSnippetService);
        setField("sysProjectModuleService", sysProjectModuleService);
        setField("sysProjectTemplateService", sysProjectTemplateService);
        setField("sysProjectStructureService", sysProjectStructureService);
        BusinessBlockGenerationService businessBlockGenerationService = new BusinessBlockGenerationService();
        setObjectField(businessBlockGenerationService, BusinessBlockGenerationService.class,
                "businessBlockRegistryService", new BusinessBlockRegistryService());
        trySetField("businessBlockGenerationService", businessBlockGenerationService);
    }

    @Test
    public void getProjectStructureAttachesTableScopedChildrenToEachGeneratedTableFolder() {
        GenProject project = project();
        GenTable userTable = table(11L, "sys_user", "User");
        GenTable roleTable = table(12L, "sys_role", "Role");
        SysProjectTemplate projectTemplate = projectTemplate(100L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(userTable, roleTable));
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate);
        when(sysProjectStructureService.selectProjectStructureListByModule(1L, 100L)).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null),
                structure(3L, 2L, "{ClassName}", "folder", 0L, null),
                structure(4L, 3L, "{ClassName}Controller.java", "file", 0L, "controller.java.vm")
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(1L, "backend");

        Map<String, Object> root = structure.get(0);
        assertEquals("demo-app", root.get("name"));
        Map<String, Object> src = child(root, 0);
        Map<String, Object> userFolder = child(src, 0);
        Map<String, Object> roleFolder = child(src, 1);
        assertEquals("User", userFolder.get("name"));
        assertEquals("Role", roleFolder.get("name"));
        assertEquals("UserController.java", child(userFolder, 0).get("name"));
        assertEquals("RoleController.java", child(roleFolder, 0).get("name"));
    }

    @Test
    public void previewCodeUsesExactTemplateFileNameMatch() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("controller.java.vm", "controller"),
                templateFile("my-controller.java.vm", "wrong")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "controller.java.vm", "backend");

        assertEquals(1, preview.size());
        assertTrue(preview.containsKey("controller.java.vm"));
        assertFalse(preview.containsKey("my-controller.java.vm"));
    }

    @Test
    public void previewCodeImportsDateTypesForRunnableBackendEntityAuditColumns() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        GenTableColumn createTime = new GenTableColumn();
        createTime.setTableId(11L);
        createTime.setColumnName("create_time");
        createTime.setColumnType("datetime");
        createTime.setJavaType("Date");
        createTime.setJavaField("createTime");
        createTime.setIsPk("0");
        table.setColumns(Arrays.asList(pkColumn(11L), createTime));

        TemplateFile entityTemplate = templateFile("entity.java.vm",
                "#foreach ($import in $importList)\nimport ${import};\n#end\n"
                        + "@JsonFormat(pattern = \"yyyy-MM-dd\")\nprivate Date createTime;");
        entityTemplate.setTemplateId(9201L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend"))
                .thenReturn(projectTemplate(9201L));
        when(templateFileService.selectTemplateFilesByTemplateId(9201L)).thenReturn(Arrays.asList(entityTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "entity.java.vm", "backend");

        String content = preview.get("entity.java.vm");
        assertTrue(content.contains("import java.util.Date;"));
        assertTrue(content.contains("import com.fasterxml.jackson.annotation.JsonFormat;"));
    }

    @Test
    public void previewCodeAppliesOnlyMatchingActiveSnippetToTemplateInsertPoint() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        SysCodeSnippet matching = snippet(1L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void audit() {}");
        SysCodeSnippet unrelatedTable = snippet(1L, "sys_role", "controller.java.vm", "controllerExtraMethods", "public void wrong() {}");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList(matching, unrelatedTable));
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("controller.java.vm", "class UserController { $controllerExtraMethods }")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "controller.java.vm", "backend");

        String content = preview.get("controller.java.vm");
        assertTrue(content.contains("public void audit() {}"));
        assertFalse(content.contains("public void wrong() {}"));
    }

    @Test
    public void previewCodeNormalizesResourceUrlProtocolChecks() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("index.vue.vm",
                        "if (/^(https?:)?\\/\\//.test(url) || url.indexOf(\"data:\") === 0 || url.indexOf(\"blob:\") === 0) {\n"
                                + "  return url\n"
                                + "}\n"
                                + "if (/^(https?:)?///.test(url) || url.indexOf(\"data:\") === 0 || url.indexOf(\"blob:\") === 0) {\n"
                                + "  return url\n"
                                + "}")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "index.vue.vm", "backend");

        String content = preview.get("index.vue.vm");
        assertFalse(content.contains("/^(https?:)?\\/\\//"));
        assertFalse(content.contains("/^(https?:)?///"));
        assertTrue(content.contains("url.indexOf(\"http://\") === 0"));
        assertTrue(content.contains("url.indexOf(\"//\") === 0"));
    }

    @Test
    public void previewCodeExposesBusinessActionsForCurrentTableOnly() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(
                businessAction("borrow_book", "sys_user"),
                businessAction("return_book", "sys_role")
        ));
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("controller.java.vm", "#if($hasTableBusinessActions)#foreach($action in $tableBusinessActions)${action.code} #end#end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "controller.java.vm", "backend");

        String content = preview.get("controller.java.vm");
        assertTrue(content.contains("borrow_book"));
        assertFalse(content.contains("return_book"));
    }

    @Test
    public void previewCodeGroupsBusinessActionsByFrontendPageSlot() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(
                businessActionWithBinding("borrow_book", "sys_user", "book_detail_page", "detail.primaryActions"),
                businessActionWithBinding("return_book", "sys_user", "my_borrows_page", "list.rowActions")
        ));
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("detail.vue.vm", "#set($actions = $slotBusinessActions.get(\"book_detail_page:detail.primaryActions\"))#foreach($action in $actions)${action.code} #end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "detail.vue.vm", "frontend");

        String content = preview.get("detail.vue.vm");
        assertTrue(content.contains("borrow_book"));
        assertFalse(content.contains("return_book"));
    }

    @Test
    public void previewCodeUsesPageDesignActionPositionsForBusinessActionButtons() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(
                businessAction("approve_book", "sys_user"),
                businessAction("return_book", "sys_user"),
                businessAction("sync_role", "sys_role")
        ));
        project.setPageDesigns(Arrays.asList(pageDesign("user_page", "sys_user",
                "{\"canvas\":\"frontend-list-v1\",\"regions\":["
                        + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"create\",\"business:approve_book\"]},"
                        + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[\"edit\",\"business:return_book\"]},"
                        + "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"id\"]}"
                        + "]}")));
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("list.vue.vm",
                        "#foreach($action in $pageDesignToolbarBusinessActions)toolbar:${action.code}:${action.path};#end"
                                + "#foreach($action in $pageDesignRowBusinessActions)row:${action.code}:${action.path};#end"
                                + "#foreach($action in $pageDesignBusinessActions)all:${action.code};#end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");

        String content = preview.get("list.vue.vm");
        assertTrue(content.contains("toolbar:approve_book:/approve_book;"));
        assertTrue(content.contains("row:return_book:/return_book;"));
        assertTrue(content.contains("all:approve_book;"));
        assertTrue(content.contains("all:return_book;"));
        assertFalse(content.contains("sync_role"));
    }

    @Test
    public void previewCodeEnrichesPageDesignedBusinessActionsWithRenderMetadata() {
        GenProject project = project();
        BusinessActionDesign action = businessAction("borrow_book", "sys_user");
        action.setRequestFields(Arrays.asList("id", "user_id", "due_date"));
        BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
        effect.setType("INSERT_ROW");
        effect.setTargetTable("sys_user");
        Map<String, String> values = new LinkedHashMap<String, String>();
        values.put("user_id", "${current_user.id}");
        effect.setValues(values);
        action.setEffects(Arrays.asList(effect));
        project.setBusinessActions(Arrays.asList(action));
        project.setPageDesigns(Arrays.asList(pageDesign("user_page", "sys_user",
                "{\"canvas\":\"frontend-list-v1\",\"regions\":["
                        + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"business:borrow_book\"]},"
                        + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[]}"
                        + "]}")));
        GenTable table = table(11L, "sys_user", "User");
        GenTableColumn dueDate = column(11L, "due_date", "dueDate");
        dueDate.setColumnComment("Due date");
        dueDate.setHtmlType("datetime");
        table.setColumns(Arrays.asList(pkColumn(11L), column(11L, "user_id", "userId"), dueDate));
        project.setTables(Arrays.asList(table));

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("list.vue.vm",
                        "#foreach($action in $pageDesignBusinessActions)"
                                + "trusted:#foreach($field in $action.currentUserFields)${field};#end"
                                + "#foreach($field in $action.inputFields)"
                                + "input:${field.field}:${field.label}:${field.htmlType};#end"
                                + "#end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");

        String content = preview.get("list.vue.vm");
        assertTrue(content.contains("trusted:user_id;"));
        assertTrue(content.contains("input:due_date:Due date:datetime;"));
    }

    @Test
    public void previewCodeAppliesPageDesignBusinessButtonPresentation() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(
                businessAction("approve_book", "sys_user")
        ));
        project.setPageDesigns(Arrays.asList(pageDesign("user_page", "sys_user",
                "{\"canvas\":\"frontend-list-v1\","
                        + "\"businessButtons\":{\"approve_book\":{"
                        + "\"label\":\"审核通过\","
                        + "\"type\":\"success\","
                        + "\"icon\":\"el-icon-check\","
                        + "\"confirmMessage\":\"确认审核通过？\","
                        + "\"successMessage\":\"已审核\","
                        + "\"refreshTarget\":\"list\""
                        + "}},"
                        + "\"regions\":["
                        + "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"business:approve_book\"]},"
                        + "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[]}"
                        + "]}")));
        GenTable table = table(11L, "sys_user", "User");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("list.vue.vm",
                        "#foreach($action in $pageDesignToolbarBusinessActions)"
                                + "${action.code}:${action.buttonLabel}:${action.buttonType}:${action.buttonIcon}:"
                                + "${action.confirmMessage}:${action.successMessage}:${action.refreshTarget};"
                                + "#end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");

        String content = preview.get("list.vue.vm");
        assertTrue(content.contains("approve_book:审核通过:success:el-icon-check:确认审核通过？:已审核:list;"));
    }

    @Test
    public void previewCodeUsesPageDesignedBusinessActionsFromOtherOwnerTables() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(
                businessAction("borrow_book", "borrow_record"),
                businessAction("reserve_book", "reservation")
        ));
        project.setPageDesigns(Arrays.asList(pageDesign("book_catalog_page", "book",
                "{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
                        + "{\"id\":\"card\",\"type\":\"cardList\",\"fields\":[\"id\"]},"
                        + "{\"id\":\"cardActions\",\"type\":\"actions\",\"actions\":[\"business:borrow_book\",\"delete\"]}"
                        + "]}")));
        GenTable table = table(11L, "book", "Book");

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
        when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
                templateFile("list.vue.vm",
                        "#foreach($action in $pageDesignRowBusinessActions)row:${action.code}:${action.ownerTable};#end"
                                + "#foreach($action in $pageDesignBusinessActions)all:${action.code};#end")
        ));

        Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");

        String content = preview.get("list.vue.vm");
        assertTrue(content.contains("row:borrow_book:borrow_record;"));
        assertTrue(content.contains("all:borrow_book;"));
        assertFalse(content.contains("reserve_book"));
    }

    @Test
    public void previewCodeUsesBundledFrontendIndexWhenDefaultRunnableTemplateIsStale() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        TemplateFile staleTemplate = templateFile("frontend-index.vue.vm",
                "#set($designed = $hasCurrentFrontendPageDesign)\n"
                        + "<template><div>old page design template</div></template>\n"
                        + "<script>export default { data() { return { businessActionMap: {} } } }</script>");
        staleTemplate.setTemplateId(9203L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
        when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "frontend-index.vue.vm", "frontend");

        String content = preview.get("frontend-index.vue.vm");
        assertFalse(content.contains("old page design template"));
        assertTrue(content.contains("portal-page"));
        assertTrue(content.contains("content-board"));
        assertTrue(content.contains("businessActionMap"));
        assertTrue(content.contains("routeActionMap"));
        assertTrue(content.contains("handleConfiguredRoute"));
    }

    @Test
    public void previewCodeUsesBundledFrontendIndexWhenDefaultRunnableTemplateLacksBusinessButtonConfig() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        TemplateFile staleTemplate = templateFile("frontend-index.vue.vm",
                "#set($designed = $hasCurrentFrontendPageDesign)\n"
                        + "#set($renderForm = true)\n"
                        + "<template><div>old business button template</div></template>\n"
                        + "<script>export default { data() { return { businessActionMap: {}, routeActionMap: {}, __currentUserOnly: false } },"
                        + " methods: { handleConfiguredRoute() {}, handleView(row) {}, resolveResourceUrl(url) { return url } } }</script>");
        staleTemplate.setTemplateId(9203L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
        when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "frontend-index.vue.vm", "frontend");

        String content = preview.get("frontend-index.vue.vm");
        assertFalse(content.contains("old business button template"));
        assertTrue(content.contains("executeBusinessAction"));
        assertTrue(content.contains("refreshBusinessActionTarget"));
        assertTrue(content.contains("businessActionDialogVisible"));
        assertTrue(content.contains("submitBusinessAction"));
        assertTrue(content.contains("currentUserFields"));
        assertFalse(content.contains("this.$prompt"));
    }

    @Test
    public void previewCodeUsesBundledFrontendIndexWhenDefaultRunnableTemplateHasSqlFragileRouteRegex() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        TemplateFile staleTemplate = templateFile("frontend-index.vue.vm",
                "#set($designed = $hasCurrentFrontendPageDesign)\n"
                        + "#set($renderForm = true)\n"
                        + "<template><div>old route regex template</div></template>\n"
                        + "<script>export default { data() { return { businessActionMap: {}, routeActionMap: {}, __currentUserOnly: false } },"
                        + " methods: { handleConfiguredRoute() { return /(^|\\/):[^/]+/.test(path) ? \"\" : path },"
                        + " handleView(row) {}, resolveResourceUrl(url) { return url }, executeBusinessAction() {}, refreshBusinessActionTarget() {} } }</script>");
        staleTemplate.setTemplateId(9203L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
        when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "frontend-index.vue.vm", "frontend");

        String content = preview.get("frontend-index.vue.vm");
        assertFalse(content.contains("old route regex template"));
        assertFalse(content.contains("return /(^|\\/):[^/]+/.test(path) ? \"\" : path"));
        assertTrue(content.contains("hasUnresolvedRouteParam(path)"));
    }

    @Test
    public void previewCodeUsesBundledFrontendIndexWhenDefaultRunnableTemplateCannotResolveUploadedResourceUrls() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        TemplateFile staleTemplate = templateFile("frontend-index.vue.vm",
                "#set($designed = $hasCurrentFrontendPageDesign)\n"
                        + "#set($renderForm = true)\n"
                        + "<template><el-image :src=\"scope.row.avatar\" /></template>\n"
                        + "<script>export default { data() { return { businessActionMap: {}, routeActionMap: {}, __currentUserOnly: false } },"
                        + " methods: { handleConfiguredRoute() {}, handleView(row) {} } }</script>");
        staleTemplate.setTemplateId(9203L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
        when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "frontend-index.vue.vm", "frontend");

        String content = preview.get("frontend-index.vue.vm");
        assertFalse(content.contains("<el-image :src=\"scope.row.avatar\" />"));
        assertTrue(content.contains("resolveResourceUrl(url)"));
    }

    @Test
    public void previewCodeUsesBundledFrontendAppWhenDefaultRunnableTemplateHasBrokenNavBinding() {
        GenProject project = project();
        GenTable table = table(11L, "sys_user", "User");
        FrontProjectPageDesign home = new FrontProjectPageDesign();
        home.setPageCode("home");
        home.setPageName("Home");
        home.setRoutePath("/home");
        home.setPageScope("frontend");
        home.setPageType("list");
        home.setTableName("sys_user");
        home.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"menuCode\":\"home\",\"menuName\":\"Home\",\"menuOrder\":1},\"regions\":[]}");
        project.setPageDesigns(Arrays.asList(home));
        TemplateFile staleTemplate = templateFile("frontend-App.vue.vm",
                "<template><nav><div class=\"portal-nav-group\" "
                        + ":class=\"{ active: isNavGroupActive(${group.activePathsLiteral}) }\"></div></nav></template>");
        staleTemplate.setTemplateId(9203L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
        when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "frontend-App.vue.vm", "frontend");

        String content = preview.get("frontend-App.vue.vm");
        assertTrue(content.contains("portal-shell"));
        assertFalse(content.contains(":class=\"{ active: isNavGroupActive("));
    }

    @Test
    public void previewCodeUsesBundledControllerWhenDefaultRunnableTemplateIsStale() {
        GenProject project = project();
        BusinessActionDesign action = businessAction("borrow_book", "sys_user");
        action.setRequestFields(Arrays.asList("user_id"));
        BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
        effect.setType("INSERT_ROW");
        effect.setTargetTable("sys_user");
        Map<String, String> values = new LinkedHashMap<String, String>();
        values.put("user_id", "${current_user.id}");
        effect.setValues(values);
        action.setEffects(Arrays.asList(effect));
        project.setBusinessActions(Arrays.asList(action));
        GenTable table = table(11L, "sys_user", "User");
        table.setColumns(Arrays.asList(pkColumn(11L), column(11L, "user_id", "userId")));
        table.setPkColumn(table.getColumns().get(0));
        TemplateFile staleTemplate = templateFile("controller.java.vm",
                "package ${packageName}.controller; "
                        + "// __currentUserOnly prepareBusinessActionParams\n"
                        + "public class ${ClassName}Controller {}");
        staleTemplate.setTemplateId(9201L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(9201L));
        when(templateFileService.selectTemplateFilesByTemplateId(9201L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "controller.java.vm", "backend");

        String content = preview.get("controller.java.vm");
        assertFalse(content.contains("public class UserController {}"));
        assertTrue(content.contains("@GetMapping(\"/system/user/page\")"));
        assertTrue(content.contains("__currentUserOnly"));
        assertTrue(content.contains("new String[] {\"user_id\"}"));
        assertTrue(content.contains("actionParams.put(field, currentUser.getUserId())"));
    }

    @Test
    public void previewCodeUsesBundledBusinessServiceWhenDefaultRunnableTemplateIsStale() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(businessAction("borrow_book", "sys_user")));
        GenTable table = table(11L, "sys_user", "User");
        table.setColumns(Arrays.asList(pkColumn(11L), column(11L, "user_id", "userId")));
        table.setPkColumn(table.getColumns().get(0));
        TemplateFile staleTemplate = templateFile("service.java.vm",
                "package ${packageName}.service; public interface I${ClassName}Service {}");
        staleTemplate.setTemplateId(9201L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(9201L));
        when(templateFileService.selectTemplateFilesByTemplateId(9201L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "service.java.vm", "backend");

        String content = preview.get("service.java.vm");
        assertTrue(content.contains("int borrow_book(Map<String, Object> params)"));
    }

    @Test
    public void previewCodeUsesBundledBusinessServiceImplWhenDefaultRunnableTemplateIsStale() {
        GenProject project = project();
        project.setBusinessActions(Arrays.asList(businessAction("borrow_book", "sys_user")));
        GenTable table = table(11L, "sys_user", "User");
        table.setColumns(Arrays.asList(pkColumn(11L), column(11L, "user_id", "userId")));
        table.setPkColumn(table.getColumns().get(0));
        TemplateFile staleTemplate = templateFile("serviceImpl.java.vm",
                "package ${packageName}.service.impl; "
                        + "// @Transactional resolveBusinessValue\n"
                        + "public class ${ClassName}ServiceImpl {}");
        staleTemplate.setTemplateId(9201L);

        when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
        when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
        when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
        when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(9201L));
        when(templateFileService.selectTemplateFilesByTemplateId(9201L)).thenReturn(Arrays.asList(staleTemplate));

        Map<String, String> preview = service.previewCode(1L, 11L, "serviceImpl.java.vm", "backend");

        String content = preview.get("serviceImpl.java.vm");
        assertTrue(content.contains("@Transactional(rollbackFor = Exception.class)"));
        assertTrue(content.contains("resolveBusinessValue"));
        assertTrue(content.contains("STRICT_TARGET_TABLE_CONDITIONS"));
    }

    @Test
    public void previewCodeRejectsSavedBusinessActionConditionOutsideTargetTable() {
        GenProject project = project();
        GenTable bookTable = table(21L, "book_info", "Book");
        bookTable.setColumns(Arrays.asList(
                pkColumn(21L),
                column(21L, "available_quantity", "availableQuantity")));
        bookTable.setPkColumn(bookTable.getColumns().get(0));
        GenTable borrowTable = table(22L, "borrow_info", "Borrow");
        borrowTable.setColumns(Arrays.asList(
                pkColumn(22L),
                column(22L, "book_id", "bookId")));
        borrowTable.setPkColumn(borrowTable.getColumns().get(0));
        project.setTables(Arrays.asList(bookTable, borrowTable));

        BusinessActionDesign action = businessAction("borrow_book", "book_info");
        action.setRequestFields(Arrays.asList("book_id"));
        BusinessActionRuleDesign rule = new BusinessActionRuleDesign();
        rule.setType("NUMBER_GTE");
        rule.setTargetTable("book_info");
        rule.setTargetField("available_quantity");
        rule.setCompareValue("1");
        rule.setConditionFields(Arrays.asList("book_id"));
        action.setRuleChecks(Arrays.asList(rule));
        project.setBusinessActions(Arrays.asList(action));

        try {
            service.previewCode(project, 21L, "serviceImpl.java.vm", "backend");
            fail("Expected invalid saved business action to be rejected");
        } catch (ServiceException e) {
            assertEquals("Business rule condition field does not exist on target table book_info: book_id",
                    e.getMessage());
        }
    }

    @Test
    public void businessBlueprintSqlTemplateGeneratesExecutableActionEffects() throws Exception {
        Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
        if (!Files.exists(sqlPath)) {
            sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
        }
        String sql = new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);

        assertFalse(sql.contains("Business action not implemented"));
        assertTrue(sql.contains("executeUpdateFieldEffect"));
        assertTrue(sql.contains("assertExists"));
        assertTrue(sql.contains("assertFieldEquals"));
        assertTrue(sql.contains("assertNumberGte"));
        assertTrue(sql.contains("insertRow"));
        assertTrue(sql.contains("updateFields"));
        assertTrue(sql.contains("setStatus"));
        assertTrue(sql.contains("increaseNumberField"));
        assertTrue(sql.contains("decreaseNumberField"));
        assertTrue(sql.contains("jdbcTemplate.update"));
        assertTrue(sql.contains("conditionFields"));
    }

    @Test
    public void businessBlueprintSqlTemplateGeneratesStaticMenuAuthFiles() throws Exception {
        Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
        if (!Files.exists(sqlPath)) {
            sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
        }
        String sql = new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);

        assertTrue(sql.contains("frontendMenus.js.vm"));
        assertTrue(sql.contains("adminMenus.js.vm"));
        assertTrue(sql.contains("canShowMenu"));
        assertTrue(sql.contains("requiresLogin"));
        assertTrue(sql.contains("router.beforeEach"));
        assertTrue(sql.contains("installAdminGuard"));
        assertFalse(sql.contains("insert into generated_menu"));
    }

    @Test
    public void businessBlueprintSqlVueTemplatesRenderFrontendActionSlots() throws Exception {
        String sql = readBusinessBlueprintSql();

        assertTrue(sql.contains("@click.stop=\"openDetail(row)\""));
        assertTrue(sql.contains("\u67e5\u770b\u8be6\u60c5"));
        assertTrue(sql.contains("$hasListRowBusinessActions"));
        assertTrue(sql.contains("$listRowBusinessActions"));
        assertTrue(sql.contains("$hasDetailPrimaryBusinessActions"));
        assertTrue(sql.contains("$detailPrimaryBusinessActions"));
        assertTrue(sql.contains("resolveSlotActions"));
    }

    @Test
    public void businessBlueprintBackendTemplateGeneratesRunnableSpringBootProject() throws Exception {
        String sql = readBusinessBlueprintSql();

        assertTrue(sql.contains("<artifactId>spring-boot-starter-parent</artifactId>"));
        assertTrue(sql.contains("<ruoyi.version>3.8.8</ruoyi.version>"));
        assertTrue(sql.contains("<version>${ruoyi.version}</version>"));
        assertTrue(sql.contains("<artifactId>mysql-connector-java</artifactId>"));
        assertTrue(sql.contains("<artifactId>spring-boot-maven-plugin</artifactId>"));
        assertTrue(sql.contains("Application.java.vm"));
        assertTrue(sql.contains("@MapperScan(\"${packageName}.mapper\")"));
        assertTrue(sql.contains("SecurityAutoConfiguration.class"));
        assertTrue(sql.contains("SpringApplication.run(${projectClassName}Application.class, args);"));
        assertTrue(sql.contains("port: ${d}{SERVER_PORT:8080}"));
        assertTrue(sql.contains("url: ${d}{DB_URL:jdbc:mysql://localhost:3306/vip?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8}"));
        assertTrue(sql.contains("mapperLocations: classpath:mapper/**/*.xml"));
        assertFalse(sql.contains("mapperLocations: classpath*:mapper/**/*.xml"));
    }

    @Test
    public void businessBlueprintAdminTemplateGeneratesRunnableVueCliProject() throws Exception {
        String sql = readBusinessBlueprintSql();
        String packageJson = extractTemplateSqlContent(sql, "910201");

        assertTrue(packageJson.contains("\"dev\": \"vue-cli-service serve --host 0.0.0.0\""));
        assertTrue(packageJson.contains("\"@vue/cli-service\":"));
        assertTrue(packageJson.contains("\"@vue/cli-plugin-babel\":"));
        assertTrue(packageJson.contains("\"vue-template-compiler\":"));
        assertTrue(packageJson.contains("\"axios\":"));
        assertTrue(packageJson.contains("\"core-js\":"));

        for (String templateFileId : Arrays.asList("910206", "910207", "910208", "910209", "910210", "910211", "910212", "910213")) {
            assertFalse(extractTemplateSqlContent(sql, templateFileId).trim().isEmpty());
        }

        assertTrue(sql.contains("'public', 'folder', NULL, 9102"));
        assertTrue(sql.contains("'index.html', 'file', NULL, 9102, -1, 'index.html.vm'"));
        assertTrue(sql.contains("'main.js', 'file', NULL, 9102, -1, 'main.js.vm'"));
        assertTrue(sql.contains("'App.vue', 'file', NULL, 9102, -1, 'App.vue.vm'"));
        assertTrue(sql.contains("'router', 'folder', NULL, 9102"));
        assertTrue(sql.contains("'index.js', 'file', NULL, 9102, -1, 'router.js.vm'"));
        assertTrue(sql.contains("'utils', 'folder', NULL, 9102"));
        assertTrue(sql.contains("'request.js', 'file', NULL, 9102, -1, 'request.js.vm'"));
        assertTrue(sql.contains("'components', 'folder', NULL, 9102"));
        assertTrue(sql.contains("'Pagination.vue', 'file', NULL, 9102, -1, 'Pagination.vue.vm'"));
    }

    @Test
    public void businessBlueprintSqlVueTemplatesAreVelocityParseable() throws Exception {
        String sql = readBusinessBlueprintSql();
        GenTable table = table(11L, "sys_user", "User");
        table.setPackageName("com.example.demo");
        table.setPkColumn(table.getColumns().get(0));
        VelocityContext context = VelocityUtils.prepareContext(table);
        context.put("hasTableBusinessActions", true);
        context.put("tableBusinessActions", Arrays.asList(businessAction("sync_user", "sys_user")));
        VelocityInitializer.initVelocity();

        for (String templateFileId : Arrays.asList("910108", "910203", "910308", "910309")) {
            StringWriter output = new StringWriter();
            Velocity.evaluate(context, output, "", extractTemplateSqlContent(sql, templateFileId));
            assertTrue(output.toString().contains("sync_user"));
        }
    }

    @Test
    public void businessBlueprintAdminShellTemplatesAreVelocityParseable() throws Exception {
        String sql = readBusinessBlueprintSql();
        GenProject project = project();
        VelocityContext context = VelocityUtils.prepareContextProject(project);
        VelocityInitializer.initVelocity();

        for (String templateFileId : Arrays.asList("910206", "910207", "910208", "910209", "910210", "910211", "910212", "910213")) {
            StringWriter output = new StringWriter();
            Velocity.evaluate(context, output, "", extractTemplateSqlContent(sql, templateFileId));
            assertFalse(output.toString().trim().isEmpty());
        }
    }

    @Test
    public void qingControllerUsesAddOnlySaveWhenEditDisabled() throws Exception {
        GenTable table = table(11L, "order_info", "Order");
        table.setPackageName("com.example.demo");
        table.setPkColumn(table.getColumns().get(0));
        table.setAllowAdd("1");
        table.setAllowEdit("0");
        table.setAllowRemove("0");
        table.setRemoveType("none");
        VelocityInitializer.initVelocity();

        StringWriter output = new StringWriter();
        Velocity.evaluate(VelocityUtils.prepareContext(table), output, "", readQingControllerTemplate());

        String content = output.toString();
        assertTrue(content.contains("orderService.save(order);"));
        assertFalse(content.contains("saveOrUpdate"));
        assertFalse(content.contains("updateById"));
        assertFalse(content.contains("@DeleteMapping"));
    }

    @Test
    public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
        GenProject project = project();
        project.setTables(Arrays.asList(table(11L, "sys_user", "User")));

        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(null);
        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(
                template(99L, 1L),
                template(101L, 0L)
        ));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "{ClassName}.java", "file", 0L, "controller.java.vm")
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "backend");

        Map<String, Object> root = structure.get(0);
        assertEquals("demo-app", root.get("name"));
        assertEquals("User.java", child(root, 0).get("name"));
        verify(genProjectMapper, never()).selectGenProjectById(any(Long.class));

        ArgumentCaptor<Template> templateCaptor = ArgumentCaptor.forClass(Template.class);
        verify(templateService).selectTemplateList(templateCaptor.capture());
        assertEquals("backend", templateCaptor.getValue().getTemplateType());

        ArgumentCaptor<SysProjectStructure> structureCaptor = ArgumentCaptor.forClass(SysProjectStructure.class);
        verify(sysProjectStructureService).selectSysProjectStructureList(structureCaptor.capture());
        assertEquals(Long.valueOf(101L), structureCaptor.getValue().getTemplateId());
        assertEquals("0", structureCaptor.getValue().getStatus());
    }

    @Test
    public void frontDraftProjectStructureIgnoresProjectTemplateBindingAndModules() {
        GenProject project = project();
        project.setFrontDraft(true);
        project.setTables(Arrays.asList(table(11L, "sys_user", "User")));

        when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(999L));
        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(
                template(99L, 1L),
                template(101L, 0L)
        ));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "{ClassName}.java", "file", 0L, "controller.java.vm")
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "backend");

        assertEquals("demo-app", structure.get(0).get("name"));
        assertEquals("User.java", child(structure.get(0), 0).get("name"));
        verify(sysProjectTemplateService, never()).selectSysProjectTemplateByProjectAndType(1L, "backend");
        verify(sysProjectStructureService, never()).selectProjectStructureListByModule(1L, 999L);
        verify(sysProjectModuleService, never()).selectSysProjectModuleList(any(SysProjectModule.class));

        ArgumentCaptor<SysProjectStructure> structureCaptor = ArgumentCaptor.forClass(SysProjectStructure.class);
        verify(sysProjectStructureService).selectSysProjectStructureList(structureCaptor.capture());
        assertEquals(Long.valueOf(101L), structureCaptor.getValue().getTemplateId());
        assertEquals("0", structureCaptor.getValue().getStatus());
    }

    @Test
    public void frontDraftProjectStructurePrefersRunnableFrontendTemplateWhenAvailable() {
        GenProject project = project();
        project.setFrontDraft(true);
        project.setTables(Arrays.asList(table(11L, "sys_user", "User")));

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(
                template(9103L, 0L),
                template(9203L, 0L)
        ));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "index.vue", "file", null, "frontend-index.vue.vm")
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");

        assertEquals("demo-app", structure.get(0).get("name"));

        ArgumentCaptor<SysProjectStructure> structureCaptor = ArgumentCaptor.forClass(SysProjectStructure.class);
        verify(sysProjectStructureService).selectSysProjectStructureList(structureCaptor.capture());
        assertEquals(Long.valueOf(9203L), structureCaptor.getValue().getTemplateId());
        assertEquals("0", structureCaptor.getValue().getStatus());
    }

    @Test
    public void getProjectStructureReplacesAndGroupsTableScopedModuleFolders() {
        GenProject project = project();
        project.setFrontDraft(true);
        GenTable userTable = table(11L, "sys_user", "User");
        GenTable bookTable = table(12L, "book", "Book");
        userTable.setModuleName("library");
        bookTable.setModuleName("library");
        project.setTables(Arrays.asList(userTable, bookTable));

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(102L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}-admin", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null),
                structure(3L, 2L, "api", "folder", null, null),
                structure(4L, 3L, "{moduleName}", "folder", 0L, null),
                structure(5L, 4L, "{businessName}.js", "file", 0L, "api.js.vm"),
                structure(6L, 2L, "views", "folder", null, null),
                structure(7L, 6L, "{moduleName}", "folder", 0L, null),
                structure(8L, 7L, "{businessName}", "folder", 0L, null),
                structure(9L, 8L, "index.vue", "file", 0L, "index.vue.vm")
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "admin_frontend");

        Map<String, Object> src = child(structure.get(0), 0);
        Map<String, Object> api = child(src, 0);
        assertEquals(1, children(api).size());
        Map<String, Object> apiModuleFolder = child(api, 0);
        assertEquals("library", apiModuleFolder.get("name"));
        assertEquals("user.js", child(apiModuleFolder, 0).get("name"));
        assertEquals("book.js", child(apiModuleFolder, 1).get("name"));

        Map<String, Object> views = child(src, 1);
        assertEquals(1, children(views).size());
        Map<String, Object> viewsModuleFolder = child(views, 0);
        assertEquals("library", viewsModuleFolder.get("name"));
        assertEquals("user", child(viewsModuleFolder, 0).get("name"));
        assertEquals("book", child(viewsModuleFolder, 1).get("name"));
    }

    @Test(expected = ServiceException.class)
    public void frontDraftPreviewRejectsTableOutsideDraftWithoutMapperFallback() {
        GenProject project = project();
        project.setFrontDraft(true);
        project.setTables(Arrays.asList(table(11L, "sys_user", "User")));

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(101L, 0L)));
        when(templateFileService.selectTemplateFilesByTemplateId(101L)).thenReturn(Arrays.asList(
                templateFile("controller.java.vm", "class Outside {}")
        ));

        try {
            service.previewCode(project, 99L, "controller.java.vm", "backend");
        } finally {
            verify(genTableMapper, never()).selectGenTableById(99L);
        }
    }

    @Test
    public void frontDraftPreviewAppliesSelectedModuleSnippets() {
        GenProject project = project();
        project.setFrontDraft(true);
        project.setModules(Arrays.asList(projectModule(1L)));
        GenTable table = table(11L, "sys_user", "User");
        project.setTables(Arrays.asList(table));
        SysCodeSnippet selectedModule = snippet(1L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void selected() {}");
        SysCodeSnippet unselectedModule = snippet(2L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void skipped() {}");

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(101L, 0L)));
        when(templateFileService.selectTemplateFilesByTemplateId(101L)).thenReturn(Arrays.asList(
                templateFile("controller.java.vm", "class UserController { $controllerExtraMethods }")
        ));
        when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList(selectedModule, unselectedModule));

        Map<String, String> preview = service.previewCode(project, 11L, "controller.java.vm", "backend");

        String content = preview.get("controller.java.vm");
        assertTrue(content.contains("public void selected() {}"));
        assertFalse(content.contains("public void skipped() {}"));
        verify(sysProjectModuleService, never()).selectSysProjectModuleList(any(SysProjectModule.class));
    }

    @Test
    public void sqlProjectStructureShowsSqlFilesUnderSqlFolder() {
        GenProject project = project();
        project.setFrontDraft(true);
        GenTable table = table(11L, "sys_user", "User");
        table.setBusinessName("user");
        table.setCreateTableSql("create table sys_user (id bigint);");
        project.setTables(Arrays.asList(table));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "sql");

        Map<String, Object> sqlFolder = structure.get(0);
        assertEquals("sql", sqlFolder.get("name"));
        assertEquals("folder", sqlFolder.get("type"));
        Map<String, Object> sqlFile = child(sqlFolder, 0);
        assertEquals("user.sql", sqlFile.get("name"));
        assertEquals("file", sqlFile.get("type"));
        assertEquals("sql", sqlFile.get("category"));
        assertEquals(Long.valueOf(11L), sqlFile.get("tableId"));
    }

    @Test
    public void sqlPreviewReturnsCreateTableSqlForSelectedTable() {
        GenProject project = project();
        project.setFrontDraft(true);
        GenTable table = table(11L, "sys_user", "User");
        table.setBusinessName("user");
        table.setCreateTableSql("create table sys_user (id bigint);");
        project.setTables(Arrays.asList(table));

        Map<String, String> preview = service.previewCode(project, 11L, "user.sql", "sql");

        assertEquals(1, preview.size());
        assertTrue(preview.get("user.sql").contains("create table sys_user"));
    }

    @Test
    public void sqlDownloadPackagesFilesUnderSqlFolder() throws Exception {
        GenProject project = project();
        project.setFrontDraft(true);
        GenTable table = table(11L, "sys_user", "User");
        table.setBusinessName("user");
        table.setCreateTableSql("create table sys_user (id bigint);");
        project.setTables(Arrays.asList(table));

        byte[] data = service.downloadStructure(project, "sql");

        Set<String> entries = zipEntries(data);
        assertTrue(entries.contains("sql/user.sql"));
        assertFalse(entries.contains("user.sql"));
    }

    @Test
    public void getProjectStructureAddsBusinessBlockFrontendFiles() {
        GenProject project = businessBlockProject();
        project.setFrontDraft(true);

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null)
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");

        Set<String> paths = structurePaths(structure);
        assertTrue(paths.contains("demo-app/src/views/pages/cart_page/index.vue"));
        assertTrue(paths.contains("demo-app/src/views/pages/cart_page/blocks/cart_001/CartBlock.vue"));
        assertTrue(paths.contains("demo-app/src/api/business-blocks/cart_001.js"));
    }

    @Test
    public void getProjectStructureAddsAdminBusinessBlockPageDesignFiles() {
        GenProject project = project();
        project.setFrontDraft(true);
        FrontProjectPageDesign home = pageDesign("admin_home", "",
                "{\"canvas\":\"business-blocks-v1\",\"blocks\":[],\"navigation\":{\"showInMenu\":true,\"menuOrder\":1}}");
        home.setPageScope("admin");
        home.setPageName("Home");
        home.setRoutePath("/admin/home");
        home.setPageType("business_block");
        project.setPageDesigns(Arrays.asList(home));

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9202L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}-admin", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null)
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "admin_frontend");

        Set<String> paths = structurePaths(structure);
        assertTrue(paths.contains("demo-app-admin/src/views/pages/admin_home/index.vue"));
    }

    @Test
    public void adminChartFilesAppearInPreviewAndDownloadStructure() {
        GenProject project = chartBusinessBlockProject();
        project.setFrontDraft(true);

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(
                template(9202L, 0L),
                template(9201L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class)))
                .thenReturn(Arrays.asList(
                        structure(1L, 0L, "{projectName}-admin", "folder", null, null),
                        structure(2L, 1L, "src", "folder", null, null)));

        Set<String> frontendPaths = structurePaths(service.getProjectStructure(project, "admin_frontend"));
        Set<String> backendPaths = structurePaths(service.getProjectStructure(project, "backend"));

        assertTrue(frontendPaths.contains("demo-app-admin/src/utils/chartRuntime.js"));
        assertTrue(frontendPaths.contains(
                "demo-app-admin/src/views/pages/admin_home/blocks/admin_line_chart_001/AdminLineChart001Block.vue"));
        assertTrue(backendPaths.contains(
                "demo-app-admin/src/main/resources/mapper/block/AdminLineChart001BlockMapper.xml"));
    }

    @Test
    public void getProjectStructureAddsNormalFrontendPageDesignFiles() {
        GenProject project = project();
        project.setFrontDraft(true);
        FrontProjectPageDesign design = pageDesign("book_catalog_page", "sys_user",
                "{\"navigation\":{\"showInMenu\":true},\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[\"view\"]}]}");
        design.setPageName("Books");
        design.setRoutePath("/books");
        project.setPageDesigns(Arrays.asList(design));

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null)
        ));

        List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");

        Set<String> paths = structurePaths(structure);
        assertTrue(paths.contains("demo-app/src/views/pages/book_catalog_page/index.vue"));
    }

    @Test
    public void previewCodeReturnsBusinessBlockFileByCategory() {
        GenProject project = businessBlockProject();
        project.setFrontDraft(true);
        BusinessBlockGenerationService generationService = new BusinessBlockGenerationService();
        setObjectFieldUnchecked(generationService, BusinessBlockGenerationService.class,
                "businessBlockRegistryService", new BusinessBlockRegistryService());
        GeneratedBusinessBlockFile file = findBusinessBlockPath(generationService.listFiles(project, "frontend"),
                "src/views/pages/cart_page/blocks/cart_001/CartBlock.vue");

        Map<String, String> preview = service.previewCode(project, null, file.getCategory(), "frontend");

        assertEquals(1, preview.size());
        assertTrue(preview.containsKey(file.getOutputPath()));
        assertTrue(preview.get(file.getOutputPath()).contains("listCart001BlockItems"));
    }

    @Test
    public void downloadStructurePackagesBusinessBlockFiles() throws Exception {
        GenProject project = businessBlockProject();
        project.setFrontDraft(true);

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "{projectName}", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null)
        ));

        byte[] data = service.downloadStructure(project, "frontend");

        Set<String> entries = zipEntries(data);
        assertTrue(entries.contains("demo-app/src/views/pages/cart_page/index.vue"));
        assertTrue(entries.contains("demo-app/src/views/pages/cart_page/blocks/cart_001/CartBlock.vue"));
        assertTrue(entries.contains("demo-app/src/api/business-blocks/cart_001.js"));
    }

    @Test
    public void downloadStructureSkipsDuplicateDirectoryEntriesFromEquivalentPaths() throws Exception {
        GenProject project = project();
        project.setFrontDraft(true);
        project.setTables(Arrays.<GenTable>asList());

        when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9201L, 0L)));
        when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
                structure(1L, 0L, "demo", "folder", null, null),
                structure(2L, 1L, "src", "folder", null, null),
                structure(3L, 2L, "com/example", "folder", null, null),
                structure(4L, 3L, "controller", "folder", null, null),
                structure(5L, 2L, "com", "folder", null, null),
                structure(6L, 5L, "example", "folder", null, null),
                structure(7L, 6L, "controller", "folder", null, null)
        ));

        byte[] data = service.downloadStructure(project, "backend");

        Set<String> entries = zipEntries(data);
        assertTrue(entries.contains("demo/src/com/example/controller/"));
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> child(Map<String, Object> node, int index) {
        return children(node).get(index);
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> children(Map<String, Object> node) {
        return (List<Map<String, Object>>) node.get("children");
    }

    private void setField(String name, Object value) throws Exception {
        Field field = GenProjectServiceImpl.class.getDeclaredField(name);
        field.setAccessible(true);
        field.set(service, value);
    }

    private void trySetField(String name, Object value) throws Exception {
        try {
            setField(name, value);
        } catch (NoSuchFieldException ignored) {
        }
    }

    private void setObjectField(Object target, Class<?> targetClass, String name, Object value) throws Exception {
        Field field = targetClass.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    }

    private void setObjectFieldUnchecked(Object target, Class<?> targetClass, String name, Object value) {
        try {
            setObjectField(target, targetClass, name, value);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private GenProject project() {
        GenProject project = new GenProject();
        project.setProjectId(1L);
        project.setProjectName("Demo");
        project.setProjectFileName("demo-app");
        project.setPackageName("com.example.demo");
        project.setTables(Arrays.asList(table(11L, "sys_user", "User")));
        return project;
    }

    private GenTable table(Long tableId, String tableName, String className) {
        GenTable table = new GenTable();
        table.setTableId(tableId);
        table.setTableName(tableName);
        table.setTableComment(className);
        table.setClassName(className);
        table.setModuleName("system");
        table.setBusinessName(className.toLowerCase());
        table.setFunctionName(className);
        table.setFunctionAuthor("tester");
        table.setTplCategory("crud");
        table.setOptions("{}");
        table.setColumns(Arrays.asList(pkColumn(tableId)));
        return table;
    }

    private GenTableColumn pkColumn(Long tableId) {
        GenTableColumn column = new GenTableColumn();
        column.setTableId(tableId);
        column.setColumnName("id");
        column.setColumnType("bigint");
        column.setJavaType("Long");
        column.setJavaField("id");
        column.setIsPk("1");
        return column;
    }

    private SysProjectTemplate projectTemplate(Long templateId) {
        SysProjectTemplate projectTemplate = new SysProjectTemplate();
        projectTemplate.setProjectId(1L);
        projectTemplate.setTemplateId(templateId);
        projectTemplate.setTemplateType("backend");
        return projectTemplate;
    }

    private Template template(Long templateId, Long templateStatus) {
        Template template = new Template();
        template.setTemplateId(templateId);
        template.setTemplateType("backend");
        template.setTemplateStatus(templateStatus);
        return template;
    }

    private SysProjectModule projectModule(Long moduleId) {
        SysProjectModule module = new SysProjectModule();
        module.setProjectId(1L);
        module.setModuleId(moduleId);
        module.setStatus("0");
        return module;
    }

    private SysProjectStructure structure(Long nodeId, Long parentId, String name, String type, Long tableId, String category) {
        SysProjectStructure structure = new SysProjectStructure();
        structure.setNodeId(nodeId);
        structure.setParentId(parentId);
        structure.setNodeName(name);
        structure.setNodeType(type);
        structure.setTemplateId(100L);
        structure.setTableId(tableId);
        structure.setCategory(category);
        structure.setStatus("0");
        return structure;
    }

    private TemplateFile templateFile(String fileName, String fileContent) {
        TemplateFile templateFile = new TemplateFile();
        templateFile.setTemplateId(100L);
        templateFile.setFileName(fileName);
        templateFile.setFileContent(fileContent);
        return templateFile;
    }

    private SysCodeSnippet snippet(Long moduleId, String relationTableName, String templateFileName, String insertPoint, String content) {
        SysCodeSnippet snippet = new SysCodeSnippet();
        snippet.setModuleId(moduleId);
        snippet.setRelationTableName(relationTableName);
        snippet.setTemplateFileName(templateFileName);
        snippet.setInsertPoint(insertPoint);
        snippet.setSnippetContent(content);
        snippet.setStatus("0");
        snippet.setDelFlag("0");
        return snippet;
    }

    private BusinessActionDesign businessAction(String code, String ownerTable) {
        BusinessActionDesign action = new BusinessActionDesign();
        action.setCode(code);
        action.setName(code);
        action.setOwnerTable(ownerTable);
        action.setMethod("POST");
        action.setPath("/" + code);
        return action;
    }

    private BusinessActionDesign businessActionWithBinding(String code, String ownerTable, String pageCode, String slot) {
        BusinessActionDesign action = businessAction(code, ownerTable);
        BusinessActionUiBindingDesign binding = new BusinessActionUiBindingDesign();
        binding.setTarget("frontend");
        binding.setPageCode(pageCode);
        binding.setSlot(slot);
        binding.setComponent("button");
        action.setUiBindings(Arrays.asList(binding));
        return action;
    }

    private FrontProjectPageDesign pageDesign(String pageCode, String tableName, String layoutJson) {
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setProjectId(1L);
        design.setPageCode(pageCode);
        design.setPageName(pageCode);
        design.setPageType("list");
        design.setTableName(tableName);
        design.setLayoutJson(layoutJson);
        design.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
        return design;
    }

    private GenProject businessBlockProject() {
        GenProject project = project();
        GenTable cart = table(201L, "shop_cart", "ShopCart");
        cart.setColumns(Arrays.asList(
                pkColumn(201L),
                column(201L, "product_id", "productId"),
                column(201L, "quantity", "quantity"),
                column(201L, "user_id", "userId")));
        cart.setPkColumn(cart.getColumns().get(0));
        GenTable product = table(202L, "shop_product", "ShopProduct");
        product.setColumns(Arrays.asList(
                pkColumn(202L),
                column(202L, "name", "name"),
                column(202L, "price", "price"),
                column(202L, "stock", "stock")));
        product.setPkColumn(product.getColumns().get(0));
        project.setTables(Arrays.asList(cart, product));
        project.setPageDesigns(Arrays.asList(businessBlockPageDesign()));
        return project;
    }

    private GenProject chartBusinessBlockProject() {
        GenProject project = project();
        GenTable orders = table(203L, "shop_order", "ShopOrder");
        orders.setColumns(Arrays.asList(
                pkColumn(203L),
                column(203L, "amount", "amount"),
                column(203L, "status", "status"),
                column(203L, "create_time", "createTime")));
        orders.getColumns().get(1).setJavaType("BigDecimal");
        orders.getColumns().get(1).setColumnType("decimal(10,2)");
        orders.getColumns().get(3).setJavaType("Date");
        orders.getColumns().get(3).setColumnType("datetime");
        orders.setPkColumn(orders.getColumns().get(0));
        project.setTables(Arrays.asList(orders));

        FrontProjectPageDesign design = pageDesign("admin_home", "",
                "{\"canvas\":\"business-blocks-v1\",\"blocks\":["
                        + "{\"id\":\"admin_line_chart_001\",\"blockCode\":\"admin_line_chart\",\"span\":6,"
                        + "\"dataset\":{\"schema\":\"single-table-aggregate-v1\",\"table\":\"shop_order\","
                        + "\"dimension\":{\"field\":\"createTime\",\"timeUnit\":\"day\"},"
                        + "\"metrics\":[{\"field\":\"amount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"Sales\"}],"
                        + "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}],"
                        + "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"},\"limit\":100},"
                        + "\"display\":{\"title\":\"Sales Trend\"}}]}");
        design.setPageScope("admin");
        design.setPageType("business_block");
        design.setRoutePath("/admin/home");
        project.setPageDesigns(Arrays.asList(design));
        return project;
    }

    private FrontProjectPageDesign businessBlockPageDesign() {
        FrontProjectPageDesign design = pageDesign("cart_page", "shop_cart",
                "{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{"
                        + "\"cartTable\":\"shop_cart\","
                        + "\"productTable\":\"shop_product\","
                        + "\"cartProductId\":\"product_id\","
                        + "\"productId\":\"id\","
                        + "\"productName\":\"name\","
                        + "\"price\":\"price\","
                        + "\"quantity\":\"quantity\","
                        + "\"userId\":\"user_id\","
                        + "\"stock\":\"stock\""
                        + "}}]}");
        design.setPageName("Cart");
        design.setPageType("business_block");
        design.setRoutePath("/cart");
        return design;
    }

    private GenTableColumn column(Long tableId, String columnName, String javaField) {
        GenTableColumn column = new GenTableColumn();
        column.setTableId(tableId);
        column.setColumnName(columnName);
        column.setColumnType("varchar");
        column.setJavaType("String");
        column.setJavaField(javaField);
        column.setIsPk("0");
        return column;
    }

    private GeneratedBusinessBlockFile findBusinessBlockPath(List<GeneratedBusinessBlockFile> files, String outputPath) {
        for (GeneratedBusinessBlockFile file : files) {
            if (outputPath.equals(file.getOutputPath())) {
                return file;
            }
        }
        throw new AssertionError("Missing business block file: " + outputPath);
    }

    private Set<String> structurePaths(List<Map<String, Object>> structure) {
        Set<String> paths = new HashSet<String>();
        collectStructurePaths(paths, "", structure);
        return paths;
    }

    @SuppressWarnings("unchecked")
    private void collectStructurePaths(Set<String> paths, String prefix, List<Map<String, Object>> nodes) {
        if (nodes == null) {
            return;
        }
        for (Map<String, Object> node : nodes) {
            String path = prefix.length() == 0 ? (String) node.get("name") : prefix + "/" + node.get("name");
            if ("file".equals(node.get("type"))) {
                paths.add(path);
            }
            collectStructurePaths(paths, path, (List<Map<String, Object>>) node.get("children"));
        }
    }

    private String readBusinessBlueprintSql() throws Exception {
        Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
        if (!Files.exists(sqlPath)) {
            sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
        }
        return new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
    }

    private String readQingControllerTemplate() throws Exception {
        Path templatePath = Paths.get("ruoyi-generator/src/main/resources/qing/controller.java.vm");
        if (!Files.exists(templatePath)) {
            templatePath = Paths.get("src/main/resources/qing/controller.java.vm");
        }
        return new String(Files.readAllBytes(templatePath), StandardCharsets.UTF_8);
    }

    private String extractTemplateSqlContent(String sql, String templateFileId) {
        String marker = "(" + templateFileId + ",";
        int rowStart = sql.indexOf(marker);
        assertTrue("Template row not found: " + templateFileId, rowStart >= 0);
        int contentStart = sql.indexOf("\n'", rowStart);
        assertTrue("Template content start not found: " + templateFileId, contentStart >= 0);
        contentStart += 2;
        int contentEnd = sql.indexOf("\n', 'admin'", contentStart);
        assertTrue("Template content end not found: " + templateFileId, contentEnd >= 0);
        return sql.substring(contentStart, contentEnd);
    }

    private Set<String> zipEntries(byte[] data) throws IOException {
        Set<String> entries = new HashSet<String>();
        ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(data));
        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            entries.add(entry.getName());
            zip.closeEntry();
        }
        zip.close();
        return entries;
    }
}
