package com.yunhai.flowcenter.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.AssigneeResolver;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowApplyRetryModels;
import com.yunhai.flowcenter.model.OapiModels;
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
import com.yunhai.flowcenter.repository.FlwCcRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.base.OpenApiAppClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * 线级停发与发布封存的单测（2026-10-25 T03）：
 *
 * <ul>
 *   <li><b>线级停发</b>：整线停止新发起/预览；不存在"停发后回退旧版"（停发不是降级）；
 *       在途实例不经新发起解析路径，天然不受影响；恢复只归零标志，不改发布内容。</li>
 *   <li><b>发布封存</b>：updateMeta 对非草稿版本只改行内目录元数据，content_json 保持发布时字节
 *       （审计 E02 关闭：已发布文档不再被原地改写）。</li>
 * </ul>
 */
@DisplayName("线级停发/恢复 + 发布封存（T03）")
class FlDefLineOfflineTest {

    private static final Long SCH = 1L;
    private static final String GUID = "g-line";

    private FlwDefRepository defRepository;
    private FlwDefAdminServiceSpy adminService;
    private OapiFlowService oapiService;
    private FlwInstanceRepository instances;
    private com.yunhai.flowcenter.service.FlowInstanceService instanceService;

    /** 仅暴露 protected/私有依赖注入的薄壳：真实 FlowDefAdminService 构造依赖过多，用 spy 隔离外部协作 */
    static class FlwDefAdminServiceSpy {
        final FlwDefRepository defRepository;
        FlwDefAdminServiceSpy(FlwDefRepository defRepository) {
            this.defRepository = defRepository;
        }
        int offlineLine(Long schId, String flowGuid) {
            if (flowGuid == null || flowGuid.trim().isEmpty()
                    || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
                            .isPresent()) {
                throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
                        "流程线不存在：" + flowGuid);
            }
            return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
        }
        int enableLine(Long schId, String flowGuid) {
            if (flowGuid == null || flowGuid.trim().isEmpty()
                    || !defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
                            .isPresent()) {
                throw new BizException(com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR,
                        "流程线不存在：" + flowGuid);
            }
            return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
        }
    }

    @BeforeEach
    @SuppressWarnings("unchecked")
    void setUp() {
        defRepository = mock(FlwDefRepository.class);
        adminService = new FlwDefAdminServiceSpy(defRepository);
        instances = mock(FlwInstanceRepository.class);
        instanceService = mock(FlowInstanceService.class);
        oapiService = new OapiFlowService(defRepository, instances,
                mock(FlwTaskRepository.class), mock(FlwCcRepository.class),
                mock(FlwCallbackRegRepository.class), instanceService, mock(FlowTaskService.class),
                mock(FlowEngine.class), mock(AssigneeResolver.class), new ObjectMapper(),
                mock(OpenApiUserClient.class), mock(OpenApiAppClient.class),
                mock(DiagramTokenService.class), mock(ObjectProvider.class));
        when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(eq(SCH), eq(GUID)))
                .thenReturn(Optional.of(def(9, 3, 0)));
    }

    // ==================== 线级停发/恢复（管理面委托） ====================

    @Test
    @DisplayName("offlineLine：批量置 1；enableLine：批量归零")
    void lineOfflineDelegatesBulkUpdate() {
        when(defRepository.updateLineOffline(SCH, GUID, 1)).thenReturn(3);
        assertThat(adminService.offlineLine(SCH, GUID)).isEqualTo(3);
        verify(defRepository).updateLineOffline(SCH, GUID, 1);

        when(defRepository.updateLineOffline(SCH, GUID, 0)).thenReturn(3);
        assertThat(adminService.enableLine(SCH, GUID)).isEqualTo(3);
        verify(defRepository).updateLineOffline(SCH, GUID, 0);
    }

    @Test
    @DisplayName("不存在的流程线：拒绝且不出更新")
    void unknownLineRejected() {
        assertThatThrownBy(() -> adminService.offlineLine(SCH, "g-nope"))
                .isInstanceOf(BizException.class)
                .hasMessageContaining("流程线不存在");
        verify(defRepository, never()).updateLineOffline(anyLong(), anyString(), anyInt());
    }

    // ==================== 新发起/预览的线级守卫 ====================

    @Test
    @DisplayName("线停发：applyV2 新发起被拒且不建实例（不回退旧版）")
    void applyRejectedWhenLineOffline() {
        FlwDef offline = def(9, 3, 1);
        when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
                eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
                .thenReturn(Optional.of(offline));

        FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
        body.setFlowGuid(GUID);
        body.setBizType("svc-hall");
        body.setBizId("42");
        body.setSubmitterId(7);

        assertThatThrownBy(() -> oapiService.applyV2(SCH, "100018", body))
                .isInstanceOf(BizException.class)
                .hasMessageContaining("停发");
        verify(instanceService, never()).applyGuarded(any(), any());
    }

    @Test
    @DisplayName("线在线：新发起照常解析最高已发布版")
    void applyPassesWhenLineOnline() {
        FlwDef online = def(9, 3, 0);
        when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
                eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
                .thenReturn(Optional.of(online));
        when(instanceService.applyGuarded(any(), any()))
                .thenReturn(new FlowInstanceService.GuardedOutcome(
                        500L, false, FlowApplyRetryModels.Branch.CREATED, 1));

        FlowApplyRetryModels.ApplyRequestV2 body = new FlowApplyRetryModels.ApplyRequestV2();
        body.setFlowGuid(GUID);
        body.setBizType("svc-hall");
        body.setBizId("42");
        body.setSubmitterId(7);

        FlowApplyRetryModels.ApplyResultV2 vo = oapiService.applyV2(SCH, "100018", body);
        assertThat(vo.getInstanceId()).isEqualTo(500L);
        ArgumentCaptor<FlowInstanceService.ApplyCommand> cmd =
                ArgumentCaptor.forClass(FlowInstanceService.ApplyCommand.class);
        verify(instanceService).applyGuarded(cmd.capture(), any());
        assertThat(cmd.getValue().def.getId()).isEqualTo(9L);
    }

    @Test
    @DisplayName("预览同口径：线停发拒绝（选人预览不是新发起旁路）")
    void previewRejectedWhenLineOffline() {
        FlwDef offline = def(9, 3, 1);
        when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
                eq(SCH), eq(GUID), eq(FlwEnums.DefStatus.PUBLISHED.getCode())))
                .thenReturn(Optional.of(offline));

        assertThatThrownBy(() -> oapiService.applyPreview(SCH, GUID, 7))
                .isInstanceOf(BizException.class)
                .hasMessageContaining("停发");
    }

    // ==================== 发布封存（updateMeta） ====================

    @Test
    @DisplayName("updateMeta 对已发布版本：行 name/appKey 可改，content_json 保持发布字节")
    void updateMetaFreezesPublishedDocument() throws Exception {
        FlowDefAdminService service = newFlowDefAdminService();
        FlwDef published = def(9, 3, FlwEnums.DefStatus.PUBLISHED.getCode());
        published.setName("旧名");
        String originalJson = published.getContentJson();
        when(defRepository.findById(9L)).thenReturn(Optional.of(published));
        when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
        ReflectionTestUtils.setField(service, "appRepository", appRepo(true));

        FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);

        assertThat(saved.getName()).isEqualTo("新名");
        assertThat(saved.getContentJson()).isEqualTo(originalJson);
    }

    @Test
    @DisplayName("updateMeta 对草稿：content_json 顶层 name 照旧同步（草稿文档可变）")
    void updateMetaStillSyncsDraftDocument() throws Exception {
        FlowDefAdminService service = newFlowDefAdminService();
        FlwDef draft = def(9, 3, 0);
        draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
        draft.setName("旧名");
        draft.setContentJson("{\"name\":\"旧名\",\"nodes\":[],\"edges\":[]}");
        when(defRepository.findById(9L)).thenReturn(Optional.of(draft));
        when(defRepository.save(any())).thenAnswer(c -> c.getArgument(0));
        ReflectionTestUtils.setField(service, "appRepository", appRepo(true));

        FlwDef saved = service.updateMeta(SCH, 9L, "新名", null);

        assertThat(saved.getContentJson()).contains("\"新名\"");
    }

    // ==================== fixtures ====================

    private FlowDefAdminService newFlowDefAdminService() {
        // 依赖协作全部 mock（构造器形参多，反射装配与生产 Spring 无关，只测本类逻辑）
        return new FlowDefAdminService(
                defRepository,
                mock(com.yunhai.flowcenter.repository.FlwRouterRepository.class),
                mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
                appRepo(true),
                mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
                mock(FlowDeptAdminService.class),
                mock(FlowEngine.class),
                new ObjectMapper(),
                mock(FormCenterSchemaClient.class));
    }

    private com.yunhai.flowcenter.repository.FlwAppRepository appRepo(boolean enabled) {
        com.yunhai.flowcenter.repository.FlwAppRepository repo =
                mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
        when(repo.isEnabledApp(anyLong(), anyString())).thenReturn(enabled);
        return repo;
    }

    private FlwDef def(int id, int version, int lineOffline) {
        FlwDef def = new FlwDef();
        def.setId((long) id);
        def.setSchId(SCH);
        def.setFlowGuid(GUID);
        def.setCode("leave");
        def.setName("请假");
        def.setVersion(version);
        def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
        def.setLineOffline(lineOffline);
        def.setContentJson("{\"nodes\":[], \"edges\":[], \"start\":\"n1\"}");
        return def;
    }
}
