package com.ruoyi.generator.service.front;

import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.GenProject;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.service.IGenProjectService;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.VelocityContext;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

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

public class FrontProjectPreviewServiceImplTest
{
    private FrontProjectPreviewServiceImpl previewService;

    @Mock
    private IFrontProjectService frontProjectService;
    @Mock
    private IGenProjectService genProjectService;

    @Before
    public void setUp() throws Exception
    {
        MockitoAnnotations.initMocks(this);
        previewService = new FrontProjectPreviewServiceImpl();
        setField("frontProjectService", frontProjectService);
        setField("genProjectService", genProjectService);
    }

    @Test
    public void returnsStructureForDraftProject()
    {
        when(frontProjectService.getProject(100L, 200L)).thenReturn(frontProjectWithOneTable());
        when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
                .thenReturn(Collections.singletonList(Collections.singletonMap("label", "backend")));

        List<Map<String, Object>> structure = previewService.getStructure(100L, 200L, "backend");

        assertEquals("backend", structure.get(0).get("label"));
    }

    @Test
    public void passesBusinessActionsIntoGenerationContext()
    {
        FrontProject project = frontProjectWithOneTable();
        project.setBusinessBlueprint("[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"book_borrow\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\"}]");
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
        when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
                .thenReturn(Collections.<Map<String, Object>>emptyList());

        previewService.getStructure(100L, 200L, "backend");

        ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
        verify(genProjectService).getProjectStructure(projectCaptor.capture(), eq("backend"));
        GenProject genProject = projectCaptor.getValue();
        assertEquals("borrow_book", genProject.getBusinessActions().get(0).getCode());
        VelocityContext context = VelocityUtils.prepareContextProject(genProject);
        assertEquals(genProject.getBusinessActions(), context.get("businessActions"));
        assertEquals(Boolean.TRUE, context.get("hasBusinessActions"));
    }

    @Test
    public void passesAppBlueprintMenusIntoGenerationContext()
    {
        FrontProject project = frontProjectWithOneTable();
        project.setAppBlueprint("{\"roles\":[{\"code\":\"reader\",\"name\":\"Reader\"}],\"frontendMenus\":[{\"code\":\"my_borrows\",\"name\":\"My Borrows\",\"path\":\"/my-borrows\",\"requiresLogin\":true,\"visibleRoles\":[\"reader\"],\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Books\",\"path\":\"/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}");
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
        when(genProjectService.getProjectStructure(any(GenProject.class), eq("frontend")))
                .thenReturn(Collections.<Map<String, Object>>emptyList());

        previewService.getStructure(100L, 200L, "frontend");

        ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
        verify(genProjectService).getProjectStructure(projectCaptor.capture(), eq("frontend"));
        GenProject genProject = projectCaptor.getValue();
        VelocityContext context = VelocityUtils.prepareContextProject(genProject);
        assertEquals("reader", genProject.getAppRoles().get(0).getCode());
        assertEquals(genProject.getFrontendMenus(), context.get("frontendMenus"));
        assertEquals(genProject.getAdminMenus(), context.get("adminMenus"));
        assertEquals(Boolean.TRUE, context.get("hasFrontendMenus"));
        assertEquals(Boolean.TRUE, context.get("hasAdminMenus"));
    }

    @Test
    public void projectVelocityContextMarksEmptyBusinessActions()
    {
        GenProject genProject = new GenProject();
        genProject.setProjectName("Demo");
        genProject.setProjectFileName("demo");
        genProject.setPackageName("com.example.demo");
        genProject.setTables(Collections.emptyList());

        VelocityContext context = VelocityUtils.prepareContextProject(genProject);

        assertFalse((Boolean) context.get("hasBusinessActions"));
    }

    @Test
    public void projectVelocityContextExposesTablesForProjectLevelTemplates()
    {
        GenProject genProject = new GenProject();
        genProject.setProjectName("Demo");
        genProject.setProjectFileName("demo");
        genProject.setPackageName("com.example.demo");
        GenTable table = new GenTable();
        table.setTableName("book_info");
        table.setClassName("BookInfo");
        table.setBusinessName("bookInfo");
        genProject.setTables(Arrays.asList(table));

        VelocityContext context = VelocityUtils.prepareContextProject(genProject);

        assertEquals(genProject.getTables(), context.get("tables"));
    }

    @Test
    public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
    {
        FrontProject project = frontProjectWithOneTable();
        project.setFrontendEnabled("1");
        project.setPreviewStatus("1");
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
        when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
                .thenReturn(zipWithEntry("demo/backend.txt", "backend"));
        when(genProjectService.downloadStructure(any(GenProject.class), eq("frontend")))
                .thenReturn(zipWithEntry("demo/frontend.txt", "frontend"));
        when(genProjectService.downloadStructure(any(GenProject.class), eq("admin_frontend")))
                .thenReturn(zipWithEntry("demo/admin.txt", "admin"));

        byte[] data = previewService.downloadAll(100L, 200L);

        Set<String> entries = zipEntries(data);
        assertTrue(entries.contains("backend/demo/backend.txt"));
        assertTrue(entries.contains("front-web/demo/frontend.txt"));
        assertTrue(entries.contains("admin-web/demo/admin.txt"));
    }

    @Test
    public void rejectsFrontendStructureWhenProjectDisablesFrontend()
    {
        FrontProject project = frontProjectWithOneTable();
        project.setFrontendEnabled("0");
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);

        ServiceException exception = expectServiceException(new ThrowingRunnable()
        {
            @Override
            public void run()
            {
                previewService.getStructure(100L, 200L, "frontend");
            }
        });

        assertEquals("该项目未启用前台前端", exception.getMessage());
        verify(genProjectService, never()).getProjectStructure(any(GenProject.class), eq("frontend"));
    }

    @Test
    public void downloadAllSkipsFrontendWhenProjectDisablesFrontend() throws Exception
    {
        FrontProject project = frontProjectWithOneTable();
        project.setFrontendEnabled("0");
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
        when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
                .thenReturn(zipWithEntry("demo/backend.txt", "backend"));
        when(genProjectService.downloadStructure(any(GenProject.class), eq("admin_frontend")))
                .thenReturn(zipWithEntry("demo/admin.txt", "admin"));

        byte[] data = previewService.downloadAll(100L, 200L);

        Set<String> entries = zipEntries(data);
        assertTrue(entries.contains("backend/demo/backend.txt"));
        assertTrue(entries.contains("admin-web/demo/admin.txt"));
        assertFalse(entries.contains("front-web/demo/frontend.txt"));
        verify(genProjectService, never()).downloadStructure(any(GenProject.class), eq("frontend"));
    }

    @Test
    public void passesSelectedModulesIntoDraftProject()
    {
        FrontProject project = frontProjectWithOneTable();
        project.setModules(Arrays.asList(projectModule(11L), projectModule(22L)));
        when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
        when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
                .thenReturn(Collections.<Map<String, Object>>emptyList());

        previewService.getStructure(100L, 200L, "backend");

        ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
        verify(genProjectService, times(1)).getProjectStructure(projectCaptor.capture(), eq("backend"));
        assertEquals(2, projectCaptor.getValue().getModules().size());
        assertEquals(Long.valueOf(11L), projectCaptor.getValue().getModules().get(0).getModuleId());
        assertEquals(Long.valueOf(22L), projectCaptor.getValue().getModules().get(1).getModuleId());
    }

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

    private FrontProject frontProjectWithOneTable()
    {
        FrontProject project = new FrontProject();
        project.setProjectId(200L);
        project.setProjectName("Demo");
        project.setProjectFileName("demo");
        project.setPackageName("com.example.demo");
        project.setFrontendEnabled("1");
        project.setTables(Arrays.asList(table()));
        return project;
    }

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

    private FrontProjectTable table()
    {
        FrontProjectTable table = new FrontProjectTable();
        table.setTableId(300L);
        table.setTableName("sys_user");
        table.setClassName("User");
        table.setColumns(Arrays.asList(column()));
        return table;
    }

    private FrontProjectColumn column()
    {
        FrontProjectColumn column = new FrontProjectColumn();
        column.setTableId(300L);
        column.setColumnName("id");
        column.setJavaType("Long");
        column.setJavaField("id");
        column.setIsPk("1");
        return column;
    }

    private byte[] zipWithEntry(String name, String content) throws IOException
    {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(outputStream);
        zip.putNextEntry(new ZipEntry(name));
        zip.write(content.getBytes("UTF-8"));
        zip.closeEntry();
        zip.close();
        return outputStream.toByteArray();
    }

    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;
    }

    private ServiceException expectServiceException(ThrowingRunnable runnable)
    {
        try
        {
            runnable.run();
        }
        catch (ServiceException e)
        {
            return e;
        }
        throw new AssertionError("Expected ServiceException");
    }

    private interface ThrowingRunnable
    {
        void run();
    }
}
