package tech.easyflow.skill.publish;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.ArgumentCaptor;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
import tech.easyflow.approval.enums.ApprovalActionType;
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.approval.service.ApprovalInstanceService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.mapper.SkillMapper;
import tech.easyflow.skill.service.SkillService;
import tech.easyflow.system.service.ResourceAccessService;

import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link SkillApprovalSubjectHandler} 发布候选与已发布快照引用所有权测试。
 */
public class SkillApprovalSubjectHandlerContentReferenceTest {

    private static final BigInteger SKILL_ID = BigInteger.valueOf(101);
    private static final BigInteger OPERATOR_ID = BigInteger.valueOf(7);

    private ApprovalInstanceService approvalInstanceService;
    private SkillService skillService;
    private SkillMapper skillMapper;
    private SkillApprovalSubjectHandler handler;
    private MockedStatic<SaTokenUtil> saToken;

    /**
     * 初始化审批处理器。
     */
    @Before
    public void setUp() {
        approvalInstanceService = mock(ApprovalInstanceService.class);
        skillService = mock(SkillService.class);
        skillMapper = mock(SkillMapper.class);
        LoginAccount account = new LoginAccount();
        account.setId(OPERATOR_ID);
        account.setTenantId(BigInteger.ONE);
        saToken = mockStatic(SaTokenUtil.class);
        saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
        when(skillMapper.updateApprovalState(any(), any(), any(), any())).thenReturn(1);
        when(skillMapper.publish(any(), any(), any(), any(), any(), any())).thenReturn(1);
        when(skillMapper.publishApproved(any(), any(), any(), any(), any(), any(), any())).thenReturn(1);
        when(skillMapper.markOfflineApproved(any(), any(), any())).thenReturn(1);
        when(skillMapper.restoreApprovalState(any(), any(), any(), any())).thenReturn(1);
        handler = new SkillApprovalSubjectHandler(
                approvalInstanceService,
                new ObjectMapper(),
                skillService,
                skillMapper,
                mock(ResourceAccessService.class));
    }

    /**
     * 释放静态登录态 Mock。
     */
    @After
    public void tearDown() {
        saToken.close();
    }

    /**
     * 验证只构建审批请求不会持有内容引用，避免预检产生副作用。
     */
    @Test
    public void buildPublishRequestDoesNotRetainSnapshotContents() {
        Skill draft = skill(PublishStatus.DRAFT, Map.of());
        Map<String, Object> candidate = snapshot("sha256:candidate");
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(draft);
        when(skillService.buildPublishSnapshot(draft)).thenReturn(candidate);

        ApprovalSubmitRequest request = handler.buildSubmitRequest(
                SKILL_ID, ApprovalActionType.PUBLISH.getCode(), OPERATOR_ID);

        ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
        verify(skillMapper).selectOneByQuery(queryCaptor.capture());
        assertTrue(queryCaptor.getValue().toSQL().toLowerCase().contains("for update"));
        verify(skillService, never()).retainSnapshotContents(candidate);
        assertSame(candidate, request.getSnapshotJson().get("resourceSnapshot"));
        assertEquals(PublishStatus.DRAFT.getCode(), request.getSnapshotJson().get("previousPublishStatus"));
    }

    /**
     * 验证重新发布时候选引用转为已发布持有，只释放被替换的旧快照。
     */
    @Test
    public void approvedRepublishReleasesOnlyPreviousPublishedSnapshot() {
        Map<String, Object> previous = snapshot("sha256:previous");
        Map<String, Object> candidate = snapshot("sha256:candidate");
        Skill published = skill(PublishStatus.PUBLISHED, previous);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);

        handler.applyApprovedAction(
                ApprovalActionType.PUBLISH.getCode(), SKILL_ID, candidate, OPERATOR_ID);

        verify(skillMapper).publish(
                eq(SKILL_ID), eq(BigInteger.ONE), same(candidate), any(Date.class),
                eq(OPERATOR_ID), isNull());
        verify(skillService).releaseSnapshotContents(previous);
        verify(skillService, never()).releaseSnapshotContents(candidate);
    }

    /**
     * 验证发布审批驳回或撤回会释放候选快照，且不会释放当前线上快照。
     */
    @Test
    public void rejectedPublishReleasesCandidateButKeepsPublishedSnapshot() {
        BigInteger instanceId = BigInteger.valueOf(99);
        Map<String, Object> publishedSnapshot = snapshot("sha256:published");
        Map<String, Object> candidate = snapshot("sha256:candidate");
        Skill published = skill(PublishStatus.PUBLISH_PENDING, publishedSnapshot);
        published.setCurrentApprovalInstanceId(instanceId);
        ApprovalInstance instance = new ApprovalInstance();
        instance.setActionType(ApprovalActionType.PUBLISH.getCode());
        instance.setSnapshotJson(Map.of("resourceSnapshot", candidate));
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);
        when(approvalInstanceService.getById(instanceId)).thenReturn(instance);

        handler.restoreState(SKILL_ID, PublishStatus.PUBLISHED);

        verify(skillMapper).updateApprovalState(
                SKILL_ID, BigInteger.ONE, PublishStatus.PUBLISHED.getCode(), null);
        verify(skillService).releaseSnapshotContents(candidate);
        verify(skillService, never()).releaseSnapshotContents(publishedSnapshot);
    }

    /**
     * 验证删除草稿不触发发布级校验，并使用不含凭据的治理快照。
     */
    @Test
    public void deleteDraftUsesGovernanceSnapshotWithoutPublishValidation() {
        Skill draft = skill(PublishStatus.DRAFT, Map.of());
        Map<String, Object> governance = Map.of(
                "id", SKILL_ID,
                "name", "demo-skill");
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(draft);
        when(skillService.buildGovernanceSnapshot(draft)).thenReturn(governance);

        ApprovalSubmitRequest request = handler.buildSubmitRequest(
                SKILL_ID, ApprovalActionType.DELETE.getCode(), OPERATOR_ID);

        assertSame(governance, request.getSnapshotJson().get("resourceSnapshot"));
        verify(skillService).buildGovernanceSnapshot(draft);
        verify(skillService, never()).buildPublishSnapshot(any());
        verify(skillService, never()).retainSnapshotContents(any());
    }

    /**
     * 验证已发布 Skill 仍须先下线，且不会构建任何删除快照。
     */
    @Test
    public void deletePublishedSkillRequiresOfflineFirst() {
        Skill published = skill(PublishStatus.PUBLISHED, snapshot("sha256:published"));
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);

        assertThrows(BusinessException.class, () -> handler.buildSubmitRequest(
                SKILL_ID, ApprovalActionType.DELETE.getCode(), OPERATOR_ID));

        verify(skillService, never()).buildGovernanceSnapshot(any());
        verify(skillService, never()).buildPublishSnapshot(any());
    }

    /**
     * 删除审批通过或无审批直通时必须使用生命周期专用聚合删除入口。
     */
    @Test
    public void approvedDeleteUsesLifecycleAggregateRemoval() {
        handler.applyApprovedAction(
                ApprovalActionType.DELETE.getCode(), SKILL_ID, Map.of(), OPERATOR_ID);

        verify(skillService).removeLifecycleAggregate(SKILL_ID);
        verify(skillService, never()).removeAggregate(SKILL_ID);
    }

    /**
     * 审批发布仅允许当前审批实例写入冻结快照。
     */
    @Test
    public void approvedPublishUsesApprovalInstanceCompareAndSet() {
        BigInteger instanceId = BigInteger.valueOf(99);
        Map<String, Object> candidate = Map.of("snapshotHash", "candidate-hash");
        Skill pending = skill(PublishStatus.PUBLISH_PENDING, Map.of());
        pending.setCurrentApprovalInstanceId(instanceId);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(pending);
        when(approvalInstanceService.getById(instanceId))
                .thenReturn(approvalInstance(instanceId, ApprovalActionType.PUBLISH, ApprovalInstanceStatus.APPROVED));

        handler.applyApprovedAction(
                ApprovalActionType.PUBLISH.getCode(),
                SKILL_ID,
                candidate,
                OPERATOR_ID,
                instanceId);

        verify(skillService).assertSnapshotHash(candidate);
        verify(skillMapper).publishApproved(
                eq(SKILL_ID),
                eq(BigInteger.ONE),
                eq(instanceId),
                same(candidate),
                any(Date.class),
                eq(OPERATOR_ID),
                eq("candidate-hash"));
    }

    /**
     * 过期审批实例不得覆盖新的 Skill 状态。
     */
    @Test
    public void staleApprovalCallbackIsRejected() {
        Skill pending = skill(PublishStatus.PUBLISH_PENDING, Map.of());
        pending.setCurrentApprovalInstanceId(BigInteger.valueOf(100));
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(pending);
        when(approvalInstanceService.getById(BigInteger.valueOf(99)))
                .thenReturn(approvalInstance(
                        BigInteger.valueOf(99), ApprovalActionType.PUBLISH, ApprovalInstanceStatus.APPROVED));

        BusinessException exception = assertThrows(BusinessException.class, () -> handler.applyApprovedAction(
                ApprovalActionType.PUBLISH.getCode(),
                SKILL_ID,
                Map.of("snapshotHash", "candidate-hash"),
                OPERATOR_ID,
                BigInteger.valueOf(99)));

        assertEquals(409, exception.getHttpStatus());
        verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any());
    }

    /**
     * 同一发布申请的重复通过回调应幂等成功且不重复释放内容。
     */
    @Test
    public void repeatedPublishApprovalIsNoOp() {
        BigInteger instanceId = BigInteger.valueOf(99);
        Map<String, Object> candidate = Map.of("snapshotHash", "candidate-hash");
        Skill published = skill(PublishStatus.PUBLISHED, candidate);
        published.setSnapshotHash("candidate-hash");
        published.setCurrentApprovalInstanceId(instanceId);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);
        when(approvalInstanceService.getById(instanceId))
                .thenReturn(approvalInstance(instanceId, ApprovalActionType.PUBLISH, ApprovalInstanceStatus.APPROVED));

        handler.applyApprovedAction(
                ApprovalActionType.PUBLISH.getCode(), SKILL_ID, candidate, OPERATOR_ID, instanceId);

        verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any());
        verify(skillService, never()).releaseSnapshotContents(any());
    }

    /**
     * 同一下线申请的重复通过回调应幂等成功。
     */
    @Test
    public void repeatedOfflineApprovalIsNoOp() {
        BigInteger instanceId = BigInteger.valueOf(99);
        Skill offline = skill(PublishStatus.OFFLINE, snapshot("sha256:published"));
        offline.setCurrentApprovalInstanceId(instanceId);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(offline);
        when(approvalInstanceService.getById(instanceId))
                .thenReturn(approvalInstance(instanceId, ApprovalActionType.OFFLINE, ApprovalInstanceStatus.APPROVED));

        handler.applyApprovedAction(
                ApprovalActionType.OFFLINE.getCode(), SKILL_ID, Map.of(), OPERATOR_ID, instanceId);

        verify(skillMapper, never()).markOfflineApproved(any(), any(), any());
    }

    /**
     * 同一删除申请在资源已经删除后重复回调应幂等成功。
     */
    @Test
    public void repeatedDeleteApprovalForMissingSkillIsNoOp() {
        BigInteger instanceId = BigInteger.valueOf(99);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(null);
        when(approvalInstanceService.getById(instanceId))
                .thenReturn(approvalInstance(instanceId, ApprovalActionType.DELETE, ApprovalInstanceStatus.APPROVED));
        when(approvalInstanceService.isLatestResourceInstance(
                instanceId, ApprovalResourceType.SKILL.getCode(), SKILL_ID)).thenReturn(true);

        handler.applyApprovedAction(
                ApprovalActionType.DELETE.getCode(), SKILL_ID, Map.of(), OPERATOR_ID, instanceId);

        verify(skillService, never()).removeLifecycleAggregate(any());
    }

    /**
     * 较旧删除申请不能把资源缺失误判为自身已完成。
     */
    @Test
    public void staleDeleteApprovalForMissingSkillIsRejected() {
        BigInteger instanceId = BigInteger.valueOf(99);
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(null);
        when(approvalInstanceService.getById(instanceId))
                .thenReturn(approvalInstance(instanceId, ApprovalActionType.DELETE, ApprovalInstanceStatus.APPROVED));

        BusinessException exception = assertThrows(BusinessException.class, () -> handler.applyApprovedAction(
                ApprovalActionType.DELETE.getCode(), SKILL_ID, Map.of(), OPERATOR_ID, instanceId));

        assertEquals(409, exception.getHttpStatus());
        verify(skillService, never()).removeLifecycleAggregate(any());
    }

    /**
     * 驳回回调首次释放候选引用，之后同一实例重放保持无副作用。
     */
    @Test
    public void repeatedRejectRestoreIsNoOpAfterFirstApplication() {
        BigInteger instanceId = BigInteger.valueOf(99);
        Map<String, Object> candidate = snapshot("sha256:candidate");
        Skill pending = skill(PublishStatus.PUBLISHED, snapshot("sha256:published"));
        pending.setCurrentApprovalInstanceId(instanceId);
        Skill restored = skill(PublishStatus.PUBLISHED, snapshot("sha256:published"));
        ApprovalInstance instance = approvalInstance(
                instanceId, ApprovalActionType.PUBLISH, ApprovalInstanceStatus.REJECTED);
        instance.setSnapshotJson(Map.of("resourceSnapshot", candidate));
        when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(pending, restored);
        when(approvalInstanceService.getById(instanceId)).thenReturn(instance);
        when(approvalInstanceService.isLatestResourceInstance(
                instanceId, ApprovalResourceType.SKILL.getCode(), SKILL_ID)).thenReturn(true);

        handler.restoreState(SKILL_ID, PublishStatus.PUBLISHED, instanceId);
        handler.restoreState(SKILL_ID, PublishStatus.PUBLISHED, instanceId);

        verify(skillService).releaseSnapshotContents(candidate);
        verify(skillMapper).restoreApprovalState(
                SKILL_ID, BigInteger.ONE, instanceId, PublishStatus.PUBLISHED.getCode());
    }

    /**
     * 创建指定生命周期状态的 Skill。
     *
     * @param status 发布状态
     * @param publishedSnapshot 已发布快照
     * @return Skill
     */
    private Skill skill(PublishStatus status, Map<String, Object> publishedSnapshot) {
        Skill skill = new Skill();
        skill.setId(SKILL_ID);
        skill.setTenantId(BigInteger.ONE);
        skill.setName("demo-skill");
        skill.setDisplayName("Demo Skill");
        skill.setPublishStatus(status.getCode());
        skill.setPublishedSnapshotJson(publishedSnapshot);
        return skill;
    }

    /**
     * 创建单二进制资源快照。
     *
     * @param contentRef 内容引用
     * @return 快照
     */
    private Map<String, Object> snapshot(String contentRef) {
        return Map.of("resources", List.of(Map.of(
                "path", "assets/file.bin",
                "contentRef", contentRef)));
    }

    /**
     * 创建与当前 Skill 回调匹配的审批实例。
     *
     * @param instanceId 实例 ID
     * @param action 动作
     * @param status 实例状态
     * @return 审批实例
     */
    private ApprovalInstance approvalInstance(BigInteger instanceId,
                                               ApprovalActionType action,
                                               ApprovalInstanceStatus status) {
        ApprovalInstance instance = new ApprovalInstance();
        instance.setId(instanceId);
        instance.setResourceType(ApprovalResourceType.SKILL.getCode());
        instance.setResourceId(SKILL_ID);
        instance.setActionType(action.getCode());
        instance.setStatus(status.getCode());
        return instance;
    }
}
