package com.malk.benteler.service;

import com.alibaba.fastjson.JSON;
import com.malk.benteler.config.BentelerYidaConf;
import com.malk.benteler.dto.EiamBatchItemResult;
import com.malk.benteler.dto.EiamBatchOperation;
import com.malk.benteler.dto.EiamBatchResult;
import com.malk.benteler.dto.EiamBatchStage;
import com.malk.benteler.dto.EiamFormSyncResult;
import com.malk.benteler.dto.EiamUpdateUserItem;
import com.malk.server.aliwork.YDAuth;
import com.malk.server.aliwork.YDConf;
import com.malk.server.aliwork.YDParam;
import com.malk.server.common.McException;
import com.malk.service.aliwork.YDClient_Form;
import com.malk.service.aliwork.YDService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link BentelerYidaSyncService} 单实例同步闭环测试。
 */
public class BentelerYidaSyncServiceTest {

    private YDClient_Form ydClientForm;
    private YDService ydService;
    private EiamLocalService eiamLocalService;
    private BentelerYidaConf conf;
    private BentelerYidaSyncService service;

    @Before
    public void setUp() {
        ydClientForm = mock(YDClient_Form.class);
        ydService = mock(YDService.class);
        eiamLocalService = mock(EiamLocalService.class);
        conf = TestBentelerYidaConf.create();
        YDConf ydConf = new YDConf();
        ydConf.setAppType("APP_TEST");
        ydConf.setSystemToken("TOKEN_TEST");
        service = new BentelerYidaSyncService(ydClientForm, ydService, ydConf, conf,
                new BentelerYidaFormMapper(conf), eiamLocalService);
    }

    @Test
    public void syncCreate_queriesInstanceAndWritesBackFullRowsAndStatistics() {
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getOnboardingOrganizationalUnitIdFieldId(), "ou_technical");
        formData.put(conf.getOnboardingTableFieldId(), Arrays.asList(
                createRow("人员一", "13800000001"),
                createRow("人员二", "13800000002")));
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(eiamLocalService.batchCreate(any())).thenReturn(batchResult(
                EiamBatchOperation.CREATE, true, false));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenReturn(Collections.emptyMap());

        EiamFormSyncResult result = service.syncCreate("form_test");

        assertTrue(result.isWritebackSuccess());
        assertEquals(2, result.getTotal());
        assertEquals(1, result.getSuccessCount());
        ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
        verify(ydClientForm).updateForm(any(YDAuth.class), anyString(),
                jsonCaptor.capture(), anyMap());
        Map<String, Object> update = JSON.parseObject(jsonCaptor.getValue(), Map.class);
        List<Map<String, Object>> rows = (List<Map<String, Object>>) update.get(
                conf.getOnboardingTableFieldId());
        assertEquals(2, rows.size());
        assertEquals("成功", rows.get(0).get(conf.getDetailStatusFieldId()));
        assertEquals("失败", rows.get(1).get(conf.getDetailStatusFieldId()));
        assertEquals(2, ((Number) update.get(conf.getTotalFieldId())).intValue());
        assertEquals("人员二 - 执行失败", update.get(conf.getFailureMessageFieldId()));
    }

    @Test
    public void syncCreate_writebackFailureReturnsExecutionResultWithoutRetryingEiam() {
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getOnboardingOrganizationalUnitIdFieldId(), "ou_technical");
        formData.put(conf.getOnboardingTableFieldId(), Collections.singletonList(
                createRow("人员一", "13800000001")));
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(eiamLocalService.batchCreate(any())).thenReturn(batchResult(
                EiamBatchOperation.CREATE, true));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenThrow(new IllegalStateException("writeback failed"));

        EiamFormSyncResult result = service.syncCreate("form_test");

        assertFalse(result.isWritebackSuccess());
        assertEquals(1, result.getSuccessCount());
        verify(eiamLocalService).batchCreate(any());
    }

    @Test
    @SuppressWarnings("unchecked")
    public void syncUpdate_readsEiamOrganizationalUnitIdDirectly() {
        Map<String, Object> row = new HashMap<>();
        row.put(conf.getUpdateEmployeeFieldId() + "_id",
                Collections.singletonList("user_1"));
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getUpdateContentFieldId(),
                Collections.singletonList(BentelerYidaFormMapper.REPLACE_PRIMARY_ORG));
        formData.put(conf.getUpdateOrganizationalUnitIdFieldId(), "ou_target");
        formData.put(conf.getUpdateTableFieldId(), Collections.singletonList(row));
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(eiamLocalService.batchUpdate(any())).thenReturn(batchResult(
                EiamBatchOperation.UPDATE, true));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenReturn(Collections.emptyMap());

        EiamFormSyncResult result = service.syncUpdate("form_update");

        assertEquals(1, result.getSuccessCount());
        ArgumentCaptor<List> itemsCaptor = ArgumentCaptor.forClass(List.class);
        verify(eiamLocalService).batchUpdate(itemsCaptor.capture());
        List<EiamUpdateUserItem> items = (List<EiamUpdateUserItem>) itemsCaptor.getValue();
        assertEquals("user_1", items.get(0).getUserId());
        assertEquals("ou_target", items.get(0).getPrimaryOrganizationalUnitId());
    }

    @Test
    public void syncUpdate_replaceAndAddDepartment_rejectsBeforeEiamCall() {
        Map<String, Object> row = new HashMap<>();
        row.put(conf.getUpdateEmployeeFieldId() + "_id",
                Collections.singletonList("user_1"));
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getUpdateContentFieldId(), Arrays.asList(
                BentelerYidaFormMapper.REPLACE_PRIMARY_ORG,
                BentelerYidaFormMapper.ADD_ORG));
        formData.put(conf.getUpdateOrganizationalUnitIdFieldId(), "ou_target");
        formData.put(conf.getUpdateTableFieldId(), Collections.singletonList(row));
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));

        try {
            service.syncUpdate("form_update_conflict");
        } catch (McException ex) {
            assertEquals("EIAM_UPDATE_CONTENT_CONFLICT", ex.getCode());
            assertEquals("替换部门和增加部门不能同时选择", ex.getMessage());
            verify(eiamLocalService, never()).batchUpdate(any());
            return;
        }
        throw new AssertionError("替换部门和增加部门同时选择时应拒绝请求");
    }

    @Test
    @SuppressWarnings("unchecked")
    public void syncUpdate_phoneOnly_doesNotRequireOrganizationalUnit() {
        Map<String, Object> row = new HashMap<>();
        row.put(conf.getUpdateEmployeeFieldId() + "_id",
                Collections.singletonList("user_1"));
        row.put(conf.getUpdatePhoneFieldId(), "17612168216");
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getUpdateContentFieldId(),
                Collections.singletonList(BentelerYidaFormMapper.UPDATE_PHONE));
        formData.put(conf.getUpdateTableFieldId(), Collections.singletonList(row));
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(eiamLocalService.batchUpdate(any())).thenReturn(batchResult(
                EiamBatchOperation.UPDATE, true));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenReturn(Collections.emptyMap());

        EiamFormSyncResult result = service.syncUpdate("form_update_phone");

        assertEquals(1, result.getSuccessCount());
        ArgumentCaptor<List> itemsCaptor = ArgumentCaptor.forClass(List.class);
        verify(eiamLocalService).batchUpdate(itemsCaptor.capture());
        List<EiamUpdateUserItem> items = (List<EiamUpdateUserItem>) itemsCaptor.getValue();
        assertEquals("17612168216", items.get(0).getUsername());
        assertEquals(null, items.get(0).getPrimaryOrganizationalUnitId());
    }

    @Test
    public void syncDelete_exactlyFiftyRowsFallsBackWhenDetailQueryThrowsNpe() {
        List<Map<String, Object>> inlineRows = new ArrayList<>();
        for (int index = 0; index < 50; index++) {
            Map<String, Object> row = new HashMap<>();
            row.put(conf.getOffboardingEmployeeFieldId() + "_id",
                    Collections.singletonList("user_" + index));
            inlineRows.add(row);
        }
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getOffboardingTableFieldId(), inlineRows);
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(ydService.queryDetails(any(YDParam.class))).thenThrow(new NullPointerException());
        when(eiamLocalService.batchDelete(any())).thenReturn(batchResult(
                EiamBatchOperation.DELETE, successFlags(50)));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenReturn(Collections.emptyMap());

        EiamFormSyncResult result = service.syncDelete("form_delete");

        assertEquals(50, result.getTotal());
        verify(ydService).queryDetails(any(YDParam.class));
    }

    @Test
    public void syncDelete_lessThanFiftyRowsUsesInlineDetails() {
        List<Map<String, Object>> inlineRows = new ArrayList<>();
        for (int index = 0; index < 49; index++) {
            Map<String, Object> row = new HashMap<>();
            row.put(conf.getOffboardingEmployeeFieldId() + "_id",
                    Collections.singletonList("user_" + index));
            inlineRows.add(row);
        }
        Map<String, Object> formData = new HashMap<>();
        formData.put(conf.getOffboardingTableFieldId(), inlineRows);
        when(ydClientForm.getForm(any(YDAuth.class), anyString(), isNull()))
                .thenReturn(instance(formData));
        when(eiamLocalService.batchDelete(any())).thenReturn(batchResult(
                EiamBatchOperation.DELETE, successFlags(49)));
        when(ydClientForm.updateForm(any(YDAuth.class), anyString(), anyString(), anyMap()))
                .thenReturn(Collections.emptyMap());

        EiamFormSyncResult result = service.syncDelete("form_delete_49");

        assertEquals(49, result.getTotal());
        verify(ydService, never()).queryDetails(any(YDParam.class));
    }

    private Map<String, Object> createRow(String name, String phone) {
        Map<String, Object> row = new HashMap<>();
        row.put(conf.getOnboardingNameFieldId(), name);
        row.put(conf.getOnboardingPhoneFieldId(), phone);
        return row;
    }

    private Map<String, Object> instance(Map<String, Object> formData) {
        Map<String, Object> instance = new HashMap<>();
        instance.put("formData", formData);
        return instance;
    }

    private boolean[] successFlags(int size) {
        boolean[] flags = new boolean[size];
        Arrays.fill(flags, true);
        return flags;
    }

    private EiamBatchResult batchResult(EiamBatchOperation operation, boolean... successFlags) {
        List<EiamBatchItemResult> items = new ArrayList<>();
        for (int index = 0; index < successFlags.length; index++) {
            items.add(EiamBatchItemResult.builder().index(index).success(successFlags[index])
                    .stage(EiamBatchStage.COMPLETED).build());
        }
        return EiamBatchResult.of(operation, items);
    }
}
