package com.ruoyi.ccdi.project.service.impl;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Date;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CcdiProjectServiceImplTest {

    @InjectMocks
    private CcdiProjectServiceImpl service;

    @Mock
    private CcdiProjectMapper projectMapper;

    @Mock
    private CcdiProjectAccessService projectAccessService;

    @Mock
    private CcdiBankTagTaskMapper bankTagTaskMapper;

    @Mock
    private LsfxAnalysisClient lsfxAnalysisClient;

    @Mock
    private ApplicationEventPublisher applicationEventPublisher;

    @Test
    void shouldCountTaggingProjectsSeparately() {
        when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 0L);

        CcdiProjectStatusCountsVO counts = service.getStatusCounts();

        assertEquals(1L, counts.getStatus3());
    }

    @Test
    void shouldCountTagFailedProjectsSeparately() {
        when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 5L);

        CcdiProjectStatusCountsVO counts = service.getStatusCounts();

        assertEquals(5L, counts.getStatus4());
    }

    @Test
    void shouldCountDeletedProjectsSeparatelyForManagerScope() {
        when(projectAccessService.buildCurrentScope()).thenReturn(
            new ProjectAccessScope("manager", true, false, true)
        );
        when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 5L);
        when(projectMapper.selectDeletedProjectCount(any())).thenReturn(6L);

        CcdiProjectStatusCountsVO counts = service.getStatusCounts();

        assertEquals(6L, counts.getStatus5());
    }

    @Test
    void shouldMarkProjectDeletedWithoutCallingBaseDelete() {
        when(projectMapper.markProjectDeleted(40L, "tester")).thenReturn(1);

        boolean result = service.deleteProject(40L, "tester");

        assertTrue(result);
        verify(projectAccessService).assertCanDelete(40L);
        verify(projectMapper).markProjectDeleted(40L, "tester");
        verify(projectMapper, never()).deleteById(40L);
    }

    @Test
    void shouldRestoreDeletedProjectToCompleted() {
        when(projectMapper.restoreDeletedProject(40L, "tester")).thenReturn(1);

        service.restoreProject(40L, "tester");

        verify(projectAccessService).assertCanManageDeletedProjects();
        verify(projectMapper).restoreDeletedProject(40L, "tester");
    }

    @Test
    void shouldRejectRestoreWhenProjectIsNotDeleted() {
        when(projectMapper.restoreDeletedProject(41L, "tester")).thenReturn(0);

        assertThrows(ServiceException.class, () -> service.restoreProject(41L, "tester"));
    }

    @Test
    void shouldReturnLatestFailedTagTaskOnFailedProjectDetail() {
        Date endTime = new Date();
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setStatus("4");
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiBankTagTask failedTask = new CcdiBankTagTask();
        failedTask.setErrorMessage("threshold missing");
        failedTask.setEndTime(endTime);
        when(bankTagTaskMapper.selectLatestFailedTaskByProjectId(40L)).thenReturn(failedTask);

        CcdiProjectVO result = service.getProjectById(40L);

        assertEquals("threshold missing", result.getLatestTagTaskErrorMessage());
        assertEquals(endTime, result.getLatestTagTaskEndTime());
    }

    @Test
    void shouldNotReturnLatestFailedTagTaskWhenProjectIsNotFailed() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setStatus("0");
        when(projectMapper.selectById(40L)).thenReturn(project);

        CcdiProjectVO result = service.getProjectById(40L);

        assertNotNull(result);
        assertNull(result.getLatestTagTaskErrorMessage());
        verify(bankTagTaskMapper, never()).selectLatestFailedTaskByProjectId(any());
    }

    @Test
    void shouldRejectUpdatingArchivedProjectToTagging() {
        CcdiProject archived = new CcdiProject();
        archived.setProjectId(99L);
        archived.setStatus("2");
        when(projectMapper.selectById(99L)).thenReturn(archived);

        assertThrows(ServiceException.class,
            () -> service.updateProjectStatus(99L, "3", "system"));
    }

    @Test
    void shouldRejectUpdatingDeletedProjectToTaggingOrCompleted() {
        CcdiProject deleted = new CcdiProject();
        deleted.setProjectId(99L);
        deleted.setStatus("5");
        when(projectMapper.selectById(99L)).thenReturn(deleted);

        assertThrows(ServiceException.class,
            () -> service.updateProjectStatus(99L, "3", "system"));
        assertThrows(ServiceException.class,
            () -> service.updateProjectStatus(99L, "1", "system"));
    }

    @Test
    void shouldRejectWritingWhenProjectIsTagging() {
        CcdiProject tagging = new CcdiProject();
        tagging.setProjectId(40L);
        tagging.setStatus("3");
        when(projectMapper.selectById(40L)).thenReturn(tagging);

        assertThrows(ServiceException.class,
            () -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标，暂不允许修改参数"));
    }

    @Test
    void shouldAllowWritingWhenProjectTagFailed() {
        CcdiProject tagFailed = new CcdiProject();
        tagFailed.setProjectId(40L);
        tagFailed.setStatus("4");
        when(projectMapper.selectById(40L)).thenReturn(tagFailed);

        assertDoesNotThrow(() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标，暂不允许修改参数"));
    }

    @Test
    void shouldArchiveCompletedProject() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setProjectName("专案A");
        project.setStatus("1");
        project.setIsArchived(0);
        when(projectMapper.selectById(40L)).thenReturn(project);

        service.archiveProject(40L, "tester");

        assertEquals("2", project.getStatus());
        assertEquals(1, project.getIsArchived());
        verify(projectMapper).updateById(project);
    }

    @Test
    void shouldRejectArchivingProjectWhenStatusIsNotCompleted() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(41L);
        project.setStatus("0");
        when(projectMapper.selectById(41L)).thenReturn(project);

        assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
    }

    @Test
    void shouldRejectArchivingProjectWhenStatusIsTagFailed() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(41L);
        project.setStatus("4");
        when(projectMapper.selectById(41L)).thenReturn(project);

        assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
    }

    @Test
    void shouldRejectWritingWhenProjectIsArchived() {
        CcdiProject archived = new CcdiProject();
        archived.setProjectId(42L);
        archived.setStatus("2");
        when(projectMapper.selectById(42L)).thenReturn(archived);

        assertThrows(ServiceException.class,
            () -> service.ensureProjectNotArchived(42L, "已归档项目暂不允许修改参数"));
    }

    @Test
    void shouldOnlyReturnCompletedAndArchivedHistoryProjects() {
        CcdiProjectQueryDTO queryDTO = new CcdiProjectQueryDTO();
        queryDTO.setProjectName("历史");

        CcdiProjectHistoryListItemVO completed = new CcdiProjectHistoryListItemVO();
        completed.setProjectId(1L);
        completed.setStatus("1");

        CcdiProjectHistoryListItemVO archived = new CcdiProjectHistoryListItemVO();
        archived.setProjectId(2L);
        archived.setStatus("2");

        when(projectMapper.selectHistoryProjects(any(), any())).thenReturn(List.of(completed, archived));

        List<CcdiProjectHistoryListItemVO> result = service.listHistoryProjects(queryDTO);

        assertEquals(2, result.size());
        assertEquals(List.of(completed, archived), result);
        verify(projectMapper).selectHistoryProjects(any(), any());
    }

    @Test
    void shouldCreateProjectThenPublishHistoryImportEventAfterCommit() {
        CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
        dto.setProjectName("新项目");
        dto.setDescription("从历史导入");
        dto.setSourceProjectIds(List.of(11L, 12L));
        dto.setStartDate("2026-01-01");
        dto.setEndDate("2026-01-31");

        when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(3001));
        doAnswer(invocation -> {
            CcdiProject project = invocation.getArgument(0);
            project.setProjectId(90L);
            return 1;
        }).when(projectMapper).insert(any(CcdiProject.class));

        TransactionSynchronizationManager.initSynchronization();
        try {
            CcdiProjectVO project = service.importFromHistory(dto, "tester");

            assertNotNull(project);
            assertEquals(90L, project.getProjectId());
            assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
            verify(applicationEventPublisher, never()).publishEvent(any());

            TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit());

            ArgumentCaptor<Object> eventCaptor = ArgumentCaptor.forClass(Object.class);
            verify(applicationEventPublisher).publishEvent(eventCaptor.capture());
            CcdiProjectHistoryImportSubmittedEvent event =
                (CcdiProjectHistoryImportSubmittedEvent) eventCaptor.getValue();
            assertEquals(90L, event.getTargetProjectId());
            assertEquals(3001, event.getTargetLsfxProjectId());
            assertEquals("tester", event.getOperator());
            assertEquals(dto, event.getDto());
        } finally {
            TransactionSynchronizationManager.clearSynchronization();
        }
    }

    @Test
    void shouldLogProjectInitialStatusWhenProjectIsCreated() {
        CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
        dto.setProjectName("专案A");
        dto.setDescription("测试项目");
        dto.setConfigType("default");

        when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(2001));
        doAnswer(invocation -> {
            CcdiProject project = invocation.getArgument(0);
            project.setProjectId(88L);
            return 1;
        }).when(projectMapper).insert(any(CcdiProject.class));

        Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class);
        ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
        logAppender.start();
        logger.addAppender(logAppender);

        try {
            service.createProject(dto);

            assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("项目状态初始化")
                    && message.contains("projectId=88")
                    && message.contains("projectName=专案A")
                    && message.contains("newStatus=0")));
        } finally {
            logger.detachAppender(logAppender);
        }
    }

    @Test
    void shouldLogProjectStatusTransitionWhenStatusChanges() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setProjectName("专案B");
        project.setStatus("0");
        when(projectMapper.selectById(40L)).thenReturn(project);

        Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class);
        ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
        logAppender.start();
        logger.addAppender(logAppender);

        try {
            service.updateProjectStatus(40L, "3", "tester");

            assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("项目状态变更")
                    && message.contains("projectId=40")
                    && message.contains("projectName=专案B")
                    && message.contains("oldStatus=0")
                    && message.contains("newStatus=3")
                    && message.contains("operator=tester")));
        } finally {
            logger.detachAppender(logAppender);
        }
    }

    @Test
    void shouldNotLogProjectStatusTransitionWhenStatusDoesNotChange() {
        CcdiProject project = new CcdiProject();
        project.setProjectId(40L);
        project.setProjectName("专案C");
        project.setStatus("3");
        when(projectMapper.selectById(40L)).thenReturn(project);

        Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class);
        ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
        logAppender.start();
        logger.addAppender(logAppender);

        try {
            service.updateProjectStatus(40L, "3", "tester");

            assertFalse(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
                .anyMatch(message -> message.contains("项目状态变更")
                    && message.contains("projectId=40")));
        } finally {
            logger.detachAppender(logAppender);
        }
    }

    private GetTokenResponse buildTokenResponse(Integer projectId) {
        GetTokenResponse response = new GetTokenResponse();
        response.setCode("200");

        GetTokenResponse.TokenData data = new GetTokenResponse.TokenData();
        data.setProjectId(projectId);
        response.setData(data);
        return response;
    }
}
