package com.njcn.rdms.module.project.service.duealert;

import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.constant.DueAlertConstants;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.dal.dataobject.duealert.DueAlertRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
import com.njcn.rdms.module.project.dal.mysql.duealert.DueAlertRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import com.njcn.rdms.module.system.api.notify.NotifyMessageSendApi;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageRespDTO;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * 扫描编排单测：边界分类 / 暂停与终态排除入参 / 判重 / 接收人组装 / 字典缺档降级 / 逾期未读不重发。
 */
class DueAlertScanServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private DueAlertScanServiceImpl dueAlertScanService;

    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private ProjectRequirementMapper projectRequirementMapper;
    @Mock
    private ProductRequirementMapper productRequirementMapper;
    @Mock
    private ProjectExecutionMapper projectExecutionMapper;
    @Mock
    private ProjectTaskMapper projectTaskMapper;
    @Mock
    private PersonalItemMapper personalItemMapper;
    @Mock
    private TaskAssigneeMapper taskAssigneeMapper;
    @Mock
    private ExecutionAssigneeMapper executionAssigneeMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private DueAlertRecordMapper dueAlertRecordMapper;
    @Mock
    private DictDataApi dictDataApi;
    @Mock
    private NotifyMessageSendApi notifyMessageSendApi;
    @Mock
    private DueAlertSendService dueAlertSendService;

    private static final LocalDate TODAY = LocalDate.of(2026, 6, 15);

    private DictDataRespDTO dict(String objectTypeCode, String days) {
        DictDataRespDTO dto = new DictDataRespDTO();
        dto.setValue(objectTypeCode);
        dto.setLabel(days);
        return dto;
    }

    private void stubDict(DictDataRespDTO... items) {
        when(dictDataApi.getDictDataList(ProjectDictTypeConstants.DUE_ALERT_ADVANCE_DAYS))
                .thenReturn(CommonResult.success(List.of(items)));
    }

    private ProjectTaskDO task(Long id, Long ownerId, LocalDate plannedEndDate) {
        ProjectTaskDO t = new ProjectTaskDO();
        t.setId(id);
        t.setOwnerId(ownerId);
        t.setTaskTitle("任务" + id);
        t.setPlannedEndDate(plannedEndDate);
        return t;
    }

    @Test
    void scan_taskBoundary_shouldClassifyApproachingAndOverdue() {
        stubDict(dict("task", "1"));
        // scan 遍历全部 6 种对象类型；用 lenient 通配，避免 strict stub 参数不匹配报错
        lenient().when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(anyString()))
                .thenReturn(List.of("completed", "cancelled"));
        // 候选：今天（临期）、今天+1（临期上界）、昨天（逾期）
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(1L, 11L, TODAY), task(2L, 12L, TODAY.plusDays(1)), task(3L, 13L, TODAY.minusDays(1))));

        dueAlertScanService.scan(TODAY);

        // 排除集 = 终态 + paused
        @SuppressWarnings("unchecked")
        ArgumentCaptor<Collection<String>> excludeCaptor = ArgumentCaptor.forClass(Collection.class);
        verify(projectTaskMapper).selectDueAlertCandidateList(eq(TODAY.plusDays(1)), excludeCaptor.capture());
        assertTrue(excludeCaptor.getValue().contains(DueAlertConstants.STATUS_PAUSED));
        assertTrue(excludeCaptor.getValue().contains("completed"));

        // 1、2 临期，3 逾期
        ArgumentCaptor<DueAlertCandidate> candidateCaptor = ArgumentCaptor.forClass(DueAlertCandidate.class);
        ArgumentCaptor<String> alertTypeCaptor = ArgumentCaptor.forClass(String.class);
        verify(dueAlertSendService, times(3))
                .sendAlert(candidateCaptor.capture(), alertTypeCaptor.capture(), eq(TODAY));
        assertEquals(DueAlertConstants.ALERT_TYPE_APPROACHING, alertTypeCaptor.getAllValues().get(0));
        assertEquals(DueAlertConstants.ALERT_TYPE_APPROACHING, alertTypeCaptor.getAllValues().get(1));
        assertEquals(DueAlertConstants.ALERT_TYPE_OVERDUE, alertTypeCaptor.getAllValues().get(2));
        assertEquals(3L, candidateCaptor.getAllValues().get(2).getObjectId());
    }

    @Test
    void scan_dictMissing_shouldScanOverdueOnly() {
        stubDict(); // 全部缺档
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.minusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));

        dueAlertScanService.scan(TODAY);

        // maxDate = 今天-1（只够到逾期），且发的是逾期档
        verify(projectTaskMapper).selectDueAlertCandidateList(eq(TODAY.minusDays(1)), anyCollection());
        verify(dueAlertSendService).sendAlert(any(DueAlertCandidate.class),
                eq(DueAlertConstants.ALERT_TYPE_OVERDUE), eq(TODAY));
    }

    @Test
    void scan_dictLabelNotNumber_shouldTreatAsMissing() {
        stubDict(dict("task", "abc"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.minusDays(1)), anyCollection()))
                .thenReturn(List.of());

        dueAlertScanService.scan(TODAY);

        verify(projectTaskMapper).selectDueAlertCandidateList(eq(TODAY.minusDays(1)), anyCollection());
    }

    @Test
    void scan_approachingDedup_sameSnapshotSkipped_changedDateResent() {
        stubDict(dict("task", "1"));
        ProjectTaskDO sameSnapshot = task(1L, 11L, TODAY);          // 已发过同快照 → 跳过
        ProjectTaskDO changedDate = task(2L, 12L, TODAY.plusDays(1)); // 记录是旧快照 → 改期重发
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(sameSnapshot, changedDate));
        DueAlertRecordDO sent1 = new DueAlertRecordDO();
        sent1.setObjectId(1L);
        sent1.setPlannedEndDate(TODAY); // 与候选 1 快照一致
        DueAlertRecordDO sent2 = new DueAlertRecordDO();
        sent2.setObjectId(2L);
        sent2.setPlannedEndDate(TODAY.minusDays(3)); // 旧快照，与候选 2 不一致
        when(dueAlertRecordMapper.selectApproachingListByObjectIds(eq("task"), anyCollection()))
                .thenReturn(List.of(sent1, sent2));

        dueAlertScanService.scan(TODAY);

        ArgumentCaptor<DueAlertCandidate> captor = ArgumentCaptor.forClass(DueAlertCandidate.class);
        verify(dueAlertSendService).sendAlert(captor.capture(),
                eq(DueAlertConstants.ALERT_TYPE_APPROACHING), eq(TODAY));
        assertEquals(2L, captor.getValue().getObjectId());
    }

    @Test
    void scan_overdueDedup_sameDaySkipped() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));
        DueAlertRecordDO sentToday = new DueAlertRecordDO();
        sentToday.setObjectId(3L);
        when(dueAlertRecordMapper.selectOverdueListByObjectIdsAndAlertDate(eq("task"), anyCollection(), eq(TODAY)))
                .thenReturn(List.of(sentToday));

        dueAlertScanService.scan(TODAY);

        verify(dueAlertSendService, never()).sendAlert(any(), any(), any());
    }

    @Test
    void scan_taskAssigneeOverlapOwner_shouldBeExcludedFromAssigneeGroup() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(1L, 11L, TODAY)));
        TaskAssigneeDO sameAsOwner = new TaskAssigneeDO();
        sameAsOwner.setTaskId(1L);
        sameAsOwner.setUserId(11L); // 与负责人重叠 → 剔除
        TaskAssigneeDO other = new TaskAssigneeDO();
        other.setTaskId(1L);
        other.setUserId(22L);
        when(taskAssigneeMapper.selectActiveListByTaskIds(anyCollection()))
                .thenReturn(List.of(sameAsOwner, other));

        dueAlertScanService.scan(TODAY);

        ArgumentCaptor<DueAlertCandidate> captor = ArgumentCaptor.forClass(DueAlertCandidate.class);
        verify(dueAlertSendService).sendAlert(captor.capture(), any(), eq(TODAY));
        assertEquals(List.of(11L), captor.getValue().getOwnerUserIds());
        assertEquals(List.of(22L), captor.getValue().getAssigneeUserIds());
    }

    private NotifyUnreadMessageRespDTO unreadOverdueMsg(Long userId, String objectType, String objectId) {
        return new NotifyUnreadMessageRespDTO()
                .setUserId(userId)
                .setTemplateCode(NotifyTemplateCodeConstants.DUE_ALERT_OVERDUE_OWNER)
                .setTemplateParams(Map.of(DueAlertConstants.PARAM_OBJECT_TYPE, objectType,
                        DueAlertConstants.PARAM_OBJECT_ID, objectId));
    }

    @Test
    void scan_overdueUnread_ownerSkippedAssigneeStillSent() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));
        TaskAssigneeDO assignee = new TaskAssigneeDO();
        assignee.setTaskId(3L);
        assignee.setUserId(22L);
        when(taskAssigneeMapper.selectActiveListByTaskIds(anyCollection())).thenReturn(List.of(assignee));
        // 负责人 13 压着该对象的未读逾期提醒 → 本轮只发协办人
        when(notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(any()))
                .thenReturn(CommonResult.success(List.of(unreadOverdueMsg(13L, "task", "3"))));

        dueAlertScanService.scan(TODAY);

        ArgumentCaptor<DueAlertCandidate> captor = ArgumentCaptor.forClass(DueAlertCandidate.class);
        verify(dueAlertSendService).sendAlert(captor.capture(),
                eq(DueAlertConstants.ALERT_TYPE_OVERDUE), eq(TODAY));
        assertTrue(captor.getValue().getOwnerUserIds().isEmpty());
        assertEquals(List.of(22L), captor.getValue().getAssigneeUserIds());
    }

    @Test
    void scan_overdueUnread_allRecipientsUnread_skipWithoutRecord() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));
        when(notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(any()))
                .thenReturn(CommonResult.success(List.of(unreadOverdueMsg(13L, "task", "3"))));

        dueAlertScanService.scan(TODAY);

        // 唯一接收人未读 → 整条跳过（不写记录，读完后次日恢复）
        verify(dueAlertSendService, never()).sendAlert(any(), any(), any());
    }

    @Test
    void scan_overdueUnread_legacyOrOtherObjectMessage_notBlocked() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));
        // 历史消息无对象标识参数 + 其他对象(999)的未读：都不拦本对象
        NotifyUnreadMessageRespDTO legacy = new NotifyUnreadMessageRespDTO()
                .setUserId(13L)
                .setTemplateCode(NotifyTemplateCodeConstants.DUE_ALERT_OVERDUE_OWNER)
                .setTemplateParams(Map.of("objectName", "旧消息"));
        when(notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(any()))
                .thenReturn(CommonResult.success(List.of(legacy, unreadOverdueMsg(13L, "task", "999"))));

        dueAlertScanService.scan(TODAY);

        verify(dueAlertSendService).sendAlert(any(DueAlertCandidate.class),
                eq(DueAlertConstants.ALERT_TYPE_OVERDUE), eq(TODAY));
    }

    @Test
    void scan_overdueUnreadQueryFail_shouldDegradeAndSend() {
        stubDict(dict("task", "1"));
        when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection()))
                .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1))));
        when(notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(any()))
                .thenThrow(new RuntimeException("rpc down"));

        dueAlertScanService.scan(TODAY);

        // 查询失败按"无未读"降级：照发（最坏退回旧的每天一条，不丢催办）
        verify(dueAlertSendService).sendAlert(any(DueAlertCandidate.class),
                eq(DueAlertConstants.ALERT_TYPE_OVERDUE), eq(TODAY));
    }

    @Test
    void scan_requirementRecipient_handlerFallbackToProposer_bothNullSkipped() {
        stubDict(dict("project_requirement", "5"));
        ProjectRequirementDO withHandler = new ProjectRequirementDO();
        withHandler.setId(1L);
        withHandler.setTitle("需求1");
        withHandler.setExpectedTime(TODAY);
        withHandler.setCurrentHandlerUserId(31L);
        withHandler.setProposerId(32L);
        ProjectRequirementDO proposerOnly = new ProjectRequirementDO();
        proposerOnly.setId(2L);
        proposerOnly.setTitle("需求2");
        proposerOnly.setExpectedTime(TODAY);
        proposerOnly.setProposerId(32L);
        ProjectRequirementDO bothNull = new ProjectRequirementDO();
        bothNull.setId(3L);
        bothNull.setTitle("需求3");
        bothNull.setExpectedTime(TODAY);
        when(projectRequirementMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(5)), anyCollection()))
                .thenReturn(List.of(withHandler, proposerOnly, bothNull));

        dueAlertScanService.scan(TODAY);

        ArgumentCaptor<DueAlertCandidate> captor = ArgumentCaptor.forClass(DueAlertCandidate.class);
        verify(dueAlertSendService, times(2)).sendAlert(captor.capture(), any(), eq(TODAY));
        assertEquals(List.of(31L), captor.getAllValues().get(0).getOwnerUserIds()); // 处理人优先
        assertEquals(List.of(32L), captor.getAllValues().get(1).getOwnerUserIds()); // 处理人空→提出人
        // 需求3 双空：未发送（times(2) 已隐含）
    }

}
