package com.yimeng.okr.service;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import com.yimeng.okr.entity.AssessmentPeriod;
import com.yimeng.okr.entity.AuditCorrection;
import com.yimeng.okr.entity.PerformanceScore;
import com.yimeng.okr.entity.PerformanceFeedback;
import com.yimeng.okr.dto.OkrDetailDto;
import com.yimeng.okr.dto.ScoringDto;
import com.yimeng.okr.entity.SysUser;
import com.yimeng.okr.enums.PeriodStatus;
import com.yimeng.okr.enums.ScoreType;
import com.yimeng.okr.exception.BusinessException;
import com.yimeng.okr.event.NotificationEvent;
import com.yimeng.okr.mapper.*;
import com.yimeng.okr.entity.PerformanceScoreItem;
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 java.util.List;
import org.springframework.context.ApplicationEventPublisher;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PerformanceServiceTest {

    @Mock PerformanceScoreMapper scoreMapper;
    @Mock PerformanceFeedbackMapper feedbackMapper;
    @Mock AssessmentPeriodMapper periodMapper;
    @Mock SysUserMapper userMapper;
    @Mock AuditCorrectionMapper auditCorrectionMapper;
    @Mock PerformanceScoreItemMapper scoreItemMapper;
    @Mock OkrService okrService;
    @Mock AuthorizationService authorizationService;
    @Mock ApplicationEventPublisher publisher;
    private PerformanceScore selfScore;
    private PerformanceScore finalScore;
    private PerformanceFeedback feedback;
    private PerformanceScoreItem sampleItem;
    private OkrDetailDto okrDetail;
    @InjectMocks PerformanceService performanceService;

    @Test
    void submitSelfScoreRejectsOkrScoreAboveSixty() {
        when(periodMapper.selectById(1L)).thenReturn(period());

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.submitSelfScore(1L, 2L, 61.0, 30.0, 0.0, 0.0, "{}"));

        assertTrue(ex.getMessage().contains("OKR"));
        verify(scoreMapper, never()).insert(any());
    }

    @Test
    void submitSelfScoreRejectsKpiScoreAboveForty() {
        when(periodMapper.selectById(1L)).thenReturn(period());

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.submitSelfScore(1L, 2L, 50.0, 41.0, 0.0, 0.0, "{}"));

        assertTrue(ex.getMessage().contains("绩效"));
        verify(scoreMapper, never()).insert(any());
    }

    @Test
    void submitSelfScoreAcceptsOkr60Kpi40Max() {
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(user(2L, null));
        when(scoreMapper.insert(any())).thenReturn(1);

        assertDoesNotThrow(() ->
                performanceService.submitSelfScore(1L, 2L, 60.0, 40.0, 0.0, 0.0, "{}"));
    }

    @Test
    void getScoringPageRequiresViewPermission() {
        doThrow(new BusinessException(403, "无权查看"))
                .when(authorizationService).requireCanViewUserData(9L, 2L);

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.getScoringPage(1L, 2L, 9L));

        assertEquals(403, ex.getCode());
    }

    @Test
    void correctFinalScoreWritesAuditBeforeUpdatingScore() {
        PerformanceScore finalScore = new PerformanceScore();
        finalScore.setId(11L);
        finalScore.setPeriodId(1L);
        finalScore.setUserId(2L);
        finalScore.setEvaluatorId(3L);
        finalScore.setType(ScoreType.FINAL);
        finalScore.setOkrScore(30.0);
        finalScore.setKpiScore(30.0);
        finalScore.setBonus(0.0);
        finalScore.setPenalty(0.0);
        finalScore.setTotal(80.0);
        when(scoreMapper.selectById(11L)).thenReturn(finalScore);
        when(authorizationService.isHrOrAbove(99L)).thenReturn(true);

        PerformanceScore corrected = performanceService.correctFinalScore(
                11L, 99L, 55.0, 35.0, 0.0, 0.0, "{\"note\":\"修正\"}", "复核通过");

        assertEquals(90.0, corrected.getTotal());
        verify(auditCorrectionMapper).insert(argThat(audit ->
                audit instanceof AuditCorrection
                        && ((AuditCorrection) audit).getPerformanceId().equals(11L)
                        && ((AuditCorrection) audit).getOperatedBy().equals(99L)));
        verify(scoreMapper).updateById(finalScore);
    }

    @Test
    void correctFinalScoreResetsConfirmedScoreToPendingPublish() {
        PerformanceScore finalScore = new PerformanceScore();
        finalScore.setId(11L);
        finalScore.setPeriodId(1L);
        finalScore.setUserId(2L);
        finalScore.setEvaluatorId(3L);
        finalScore.setType(ScoreType.FINAL);
        finalScore.setStatus("PUBLISHED");
        finalScore.setConfirmedAt(java.time.LocalDateTime.now());
        when(scoreMapper.selectById(11L)).thenReturn(finalScore);
        when(authorizationService.isHrOrAbove(99L)).thenReturn(true);

        PerformanceScore corrected = performanceService.correctFinalScore(
                11L, 99L, 55.0, 35.0, 0.0, 0.0, "{}", "确认后修正");

        assertEquals("PENDING_PUBLISH", corrected.getStatus());
        assertNull(corrected.getConfirmedAt());
        verify(auditCorrectionMapper).insert(any());
        verify(scoreMapper).updateById(finalScore);
    }

    @Test
    void correctFinalScoreResetsPublishedScoreToPendingPublishForReconfirmation() {
        PerformanceScore finalScore = new PerformanceScore();
        finalScore.setId(12L);
        finalScore.setPeriodId(1L);
        finalScore.setUserId(2L);
        finalScore.setEvaluatorId(3L);
        finalScore.setType(ScoreType.FINAL);
        finalScore.setStatus("PUBLISHED");
        finalScore.setPublishedAt(java.time.LocalDateTime.now());
        when(scoreMapper.selectById(12L)).thenReturn(finalScore);
        when(authorizationService.isHrOrAbove(99L)).thenReturn(true);

        PerformanceScore corrected = performanceService.correctFinalScore(
                12L, 99L, 55.0, 35.0, 0.0, 0.0, "{}", "复核修正");

        assertEquals("PENDING_PUBLISH", corrected.getStatus());
        assertNull(corrected.getPublishedAt());
        assertNull(corrected.getConfirmedAt());
        verify(auditCorrectionMapper).insert(any());
        verify(scoreMapper).updateById(finalScore);
    }

    @Test
    void submitSuperiorScoreRejectsWhenSelfScoreMissing() {
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(user(2L, 3L));
        when(userMapper.selectById(3L)).thenReturn(user(3L, null));
        when(scoreMapper.selectOne(any())).thenReturn(null);

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.submitSuperiorScore(1L, 2L, 3L, 50.0, 30.0, 0.0, 0.0, "{}"));

        assertTrue(ex.getMessage().contains("自评"));
        verify(scoreMapper, never()).insert(any());
    }

    @Test
    void submitSuperiorScoreRejectsDisabledEvaluator() {
        SysUser target = user(2L, 3L);
        SysUser evaluator = user(3L, null);
        evaluator.setStatus("DISABLED");
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(target);
        when(userMapper.selectById(3L)).thenReturn(evaluator);

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.submitSuperiorScore(1L, 2L, 3L, 50.0, 30.0, 0.0, 0.0, "{}"));

        assertTrue(ex.getMessage().contains("评分人"));
        verify(scoreMapper, never()).insert(any());
    }

    @Test
    void submitSuperiorScoreRejectsDeletedTargetUser() {
        SysUser target = user(2L, 3L);
        target.setDeleted(1);
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(target);

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.submitSuperiorScore(1L, 2L, 3L, 50.0, 30.0, 0.0, 0.0, "{}"));

        assertTrue(ex.getMessage().contains("被评分人"));
        verify(scoreMapper, never()).insert(any());
    }

    @Test
    void submitSuperiorScoreCreatesUnpublishedFinalScoreWithoutNotifyingEmployee() {
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(user(2L, 3L));
        when(userMapper.selectById(3L)).thenReturn(user(3L, null));
        PerformanceScore selfScore = score(ScoreType.SELF, "SELF_SUBMITTED");
        when(scoreMapper.selectOne(any())).thenReturn(selfScore, null);

        performanceService.submitSuperiorScore(1L, 2L, 3L, 50.0, 30.0, 0.0, 0.0, "{}");

        ArgumentCaptor<PerformanceScore> captor = ArgumentCaptor.forClass(PerformanceScore.class);
        verify(scoreMapper, times(2)).insert(captor.capture());
        PerformanceScore superiorScore = captor.getAllValues().get(0);
        PerformanceScore finalScore = captor.getAllValues().get(1);
        assertEquals("SUPERIOR_SUBMITTED", superiorScore.getStatus());
        assertEquals(ScoreType.FINAL, finalScore.getType());
        assertEquals("PENDING_PUBLISH", finalScore.getStatus());
        assertNull(finalScore.getPublishedAt());
        verify(publisher, never()).publishEvent(any());
    }

    @Test
    void confirmScoreRejectsUnpublishedFinalScore() {
        when(scoreMapper.selectOne(any())).thenReturn(score(ScoreType.FINAL, "PENDING_PUBLISH"));

        BusinessException ex = assertThrows(BusinessException.class,
                () -> performanceService.confirmScore(1L, 2L));

        assertTrue(ex.getMessage().contains("尚未发布"));
        verify(scoreMapper, never()).updateById(any());
    }

    @Test
    void publishFinalScorePublishesAndNotifiesEmployee() {
        PerformanceScore finalScore = score(ScoreType.FINAL, "PENDING_PUBLISH");
        finalScore.setId(9L);
        when(scoreMapper.selectOne(any())).thenReturn(finalScore);

        PerformanceScore published = performanceService.publishFinalScore(1L, 2L, 99L);

        assertEquals("PUBLISHED", published.getStatus());
        assertNotNull(published.getPublishedAt());
        verify(authorizationService).requireCanManageUserData(99L, 2L);
        verify(scoreMapper).updateById(finalScore);
        verify(publisher).publishEvent(any(NotificationEvent.class));
    }

    private AssessmentPeriod period() {
        AssessmentPeriod period = new AssessmentPeriod();
        period.setId(1L); period.setStartDate(java.time.LocalDate.of(2026,1,1)); period.setEndDate(java.time.LocalDate.of(2026,12,31));
        period.setStatus(PeriodStatus.ASSESSING);
        return period;
    }

    private SysUser user(Long id, Long superiorId) {
        SysUser user = new SysUser();
        user.setId(id);
        user.setSuperiorId(superiorId);
        user.setStatus("ACTIVE");
        user.setDeleted(0);
        return user;
    }

    private PerformanceScore score(ScoreType type, String status) {
        PerformanceScore score = new PerformanceScore();
        score.setPeriodId(1L);
        score.setUserId(2L);
        score.setEvaluatorId(3L);
        score.setType(type);
        score.setStatus(status);
        score.setOkrScore(30.0);
        score.setKpiScore(30.0);
        score.setBonus(0.0);
        score.setPenalty(0.0);
        score.setTotal(80.0);
        return score;
    }

    @Test
    void submitSelfScoreWritesStructuredItems() {
        when(periodMapper.selectById(1L)).thenReturn(period());
        when(userMapper.selectById(2L)).thenReturn(user(2L, null));
        when(scoreMapper.insert(any())).thenReturn(1);

        String json = "{\"krScores\":[{\"krId\":1,\"title\":\"KR1\",\"score\":0.8}],"
                + "\"dimensions\":[{\"dimensionId\":5,\"name\":\"质量\",\"maxScore\":10,\"score\":8}]}";
        performanceService.submitSelfScore(1L, 2L, 50.0, 30.0, 0.0, 0.0, json);

        ArgumentCaptor<PerformanceScoreItem> captor = ArgumentCaptor.forClass(PerformanceScoreItem.class);
        verify(scoreItemMapper, times(2)).insert(captor.capture());
        List<PerformanceScoreItem> items = captor.getAllValues();
        assertEquals(2, items.size());
        // KR item
        PerformanceScoreItem krItem = items.get(0);
        assertEquals("KR", krItem.getItemType());
        assertEquals(Long.valueOf(1), krItem.getReferenceId());
        assertEquals(0.8, krItem.getScore());
        // Dimension item
        PerformanceScoreItem dimItem = items.get(1);
        assertEquals("DIMENSION", dimItem.getItemType());
        assertEquals(Long.valueOf(5), dimItem.getReferenceId());
        assertEquals(8.0, dimItem.getScore());
        assertEquals(10.0, dimItem.getMaxScore());
    }

    @Test
    void selfCannotSeeUnpublishedFinalItems() {
        setupPrivacyMocks(1L, 2L, 2L, "SELF_SUBMITTED", "PENDING_PUBLISH");

        ScoringDto dto = performanceService.getScoringPage(1L, 2L, 2L);

        assertNull(dto.getFinalScore(), "Self should not see unpublished final score");
        assertNull(dto.getFinalItems(), "Self should not see unpublished final items");
        assertNotNull(dto.getSelfScore(), "Self can see their self score");
        assertNotNull(dto.getSelfItems(), "Self can see their self items");
    }

    @Test
    void superiorCannotSeeSelfItems() {
        setupPrivacyMocks(1L, 2L, 3L, "SELF_SUBMITTED", "PUBLISHED");

        ScoringDto dto = performanceService.getScoringPage(1L, 2L, 3L);

        assertNotNull(dto.getSelfScore(), "Superior can see self score exists");
        assertNull(dto.getSelfScore().getCommentsJson(), "Superior should not see self commentsJson");
        assertNull(dto.getSelfItems(), "Superior should not see selfItems");
        assertNotNull(dto.getFinalScore(), "Superior can see final score");
        assertNotNull(dto.getFinalItems(), "Superior can see final items");
    }

    @Test
    void hrCanSeeAllItems() {
        setupPrivacyMocks(1L, 2L, 99L, "SELF_SUBMITTED", "PUBLISHED"); when(authorizationService.isHrOrAbove(99L)).thenReturn(true);

        ScoringDto dto = performanceService.getScoringPage(1L, 2L, 99L);

        assertNotNull(dto.getSelfScore());
        assertNotNull(dto.getSelfItems());
        assertNotNull(dto.getFinalScore());
        assertNotNull(dto.getFinalItems());
    }

    @Test
    void selfCanSeePublishedFinalItems() {
        setupPrivacyMocks(1L, 2L, 2L, "SELF_SUBMITTED", "PUBLISHED");

        ScoringDto dto = performanceService.getScoringPage(1L, 2L, 2L);

        assertNotNull(dto.getFinalScore(), "Self can see published final score");
        assertNotNull(dto.getFinalItems(), "Self can see published final items");
        assertNotNull(dto.getSelfScore(), "Self can see their self score");
    }

    private void setupPrivacyMocks(Long periodId, Long userId, Long actorId, 
                                    String selfStatus, String finalStatus) {
        lenient().when(periodMapper.selectById(periodId)).thenReturn(period());
        SysUser targetUser = user(userId, 3L);
        lenient().when(userMapper.selectById(userId)).thenReturn(targetUser);
        lenient().doNothing().when(authorizationService).requireCanViewUserData(actorId, userId);

        selfScore = new PerformanceScore();
        selfScore.setId(10L);
        selfScore.setPeriodId(periodId);
        selfScore.setUserId(userId);
        selfScore.setType(ScoreType.SELF);
        selfScore.setStatus(selfStatus);
        selfScore.setOkrScore(50.0);
        selfScore.setKpiScore(30.0);
        selfScore.setCommentsJson("{\"details\":\"self\"}");

        finalScore = new PerformanceScore();
        finalScore.setId(20L);
        finalScore.setPeriodId(periodId);
        finalScore.setUserId(userId);
        finalScore.setType(ScoreType.FINAL);
        finalScore.setStatus(finalStatus);
        finalScore.setTotal(80.0);
        finalScore.setGrade(com.yimeng.okr.enums.Grade.A);

        sampleItem = new PerformanceScoreItem();
        sampleItem.setId(1L);
        sampleItem.setScoreId(10L);
        sampleItem.setItemType("KR");
        sampleItem.setScore(0.8);

        okrDetail = new OkrDetailDto();
        okrDetail.setUserId(userId);
        okrDetail.setPeriodId(periodId);

        lenient().when(scoreMapper.selectOne(any())).thenReturn(selfScore, finalScore);
        lenient().when(scoreItemMapper.selectList(any())).thenReturn(List.of(sampleItem));
        lenient().when(okrService.getMyOkr(periodId, userId)).thenReturn(okrDetail);
        feedback = new PerformanceFeedback();
        lenient().when(feedbackMapper.selectOne(any())).thenReturn(feedback);
    }

}
