package com.xly.erp.module.mod.service;

import com.xly.erp.common.config.StubSecurityProperties;
import com.xly.erp.common.config.TenantProperties;
import com.xly.erp.common.exception.BizException;
import com.xly.erp.module.mod.dto.CreateModuleDTO;
import com.xly.erp.module.mod.dto.UpdateModuleDTO;
import com.xly.erp.module.mod.entity.Module;
import com.xly.erp.module.mod.mapper.ModuleMapper;
import com.xly.erp.module.mod.service.impl.ModuleServiceImpl;
import com.xly.erp.module.mod.vo.ModuleTreeVO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Collections;
import java.util.List;

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.Mockito.lenient;
import static org.mockito.Mockito.mock;
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 ModuleServiceImplTest {

    private ModuleMapper moduleMapper;
    private ModuleServiceImpl service;

    @BeforeEach
    void setUp() {
        moduleMapper = mock(ModuleMapper.class);
        TenantProperties tenant = new TenantProperties();
        tenant.setBrandsId("XLY");
        tenant.setSubsidiaryId("XLY");
        StubSecurityProperties stub = new StubSecurityProperties();
        stub.setStubUserNo("STUB_ADMIN");
        service = new ModuleServiceImpl(moduleMapper, tenant, stub);

        lenient().when(moduleMapper.insert(any(Module.class))).thenAnswer(inv -> {
            Module m = inv.getArgument(0);
            m.setIIncrement(99);
            return 1;
        });
    }

    @AfterEach
    void clearContext() {
        SecurityContextHolder.clearContext();
    }

    @Test
    void createWithValidDto_persistsWithStandardCols() {
        CreateModuleDTO dto = baseDto();

        Integer id = service.create(dto);

        assertThat(id).isEqualTo(99);
        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper, times(1)).insert(captor.capture());
        Module saved = captor.getValue();
        assertThat(saved.getSBrandsId()).isEqualTo("XLY");
        assertThat(saved.getSSubsidiaryId()).isEqualTo("XLY");
        assertThat(saved.getTCreateDate()).isNotNull();
        assertThat(saved.getSCreatedBy()).isNull();
        assertThat(saved.getBDeleted()).isFalse();
        assertThat(saved.getBShowPermission()).isFalse();
        assertThat(saved.getISortOrder()).isZero();
        verify(moduleMapper, never()).findActiveFlagById(any());
    }

    @Test
    void createWithParentNotFound_throws40021() {
        CreateModuleDTO dto = baseDto();
        dto.setIParentId(42);
        when(moduleMapper.findActiveFlagById(42)).thenReturn(null);

        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40021);
        verify(moduleMapper, never()).insert(any(Module.class));
    }

    @Test
    void createWithInvalidDisplayType_throws40010() {
        CreateModuleDTO dto = baseDto();
        dto.setSDisplayType("未知");

        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40010);
        verify(moduleMapper, never()).insert(any(Module.class));
    }

    @Test
    void createWithNullParentId_skipsParentCheck() {
        CreateModuleDTO dto = baseDto();
        dto.setIParentId(null);

        service.create(dto);

        verify(moduleMapper, never()).findActiveFlagById(any());
        verify(moduleMapper, times(1)).insert(any(Module.class));
    }

    @Test
    void mapperDuplicateKey_throws40020() {
        CreateModuleDTO dto = baseDto();
        when(moduleMapper.insert(any(Module.class)))
                .thenThrow(new DuplicateKeyException("uk_procedure_name"));

        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40020);
    }

    @Test
    void usesAuthenticatedUserNoAsCreatedBy() {
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken("ALICE", null, Collections.emptyList()));
        CreateModuleDTO dto = baseDto();

        service.create(dto);

        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper).insert(captor.capture());
        assertThat(captor.getValue().getSCreatedBy()).isEqualTo("ALICE");
    }

    @Test
    void updateWithValidDto_invokesUpdateById_withEditableFieldsOnly() {
        Module original = stubExistingModule(10);
        when(moduleMapper.selectById(10)).thenReturn(original);
        when(moduleMapper.updateById(any(Module.class))).thenReturn(1);

        UpdateModuleDTO dto = baseUpdateDto();
        dto.setSModuleNameZh("新名");

        Integer result = service.update(10, dto);
        assertThat(result).isEqualTo(10);

        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper).updateById(captor.capture());
        Module passed = captor.getValue();
        assertThat(passed.getIIncrement()).isEqualTo(10);
        assertThat(passed.getSModuleNameZh()).isEqualTo("新名");
        assertThat(passed.getSDisplayType()).isEqualTo("手机端");
        assertThat(passed.getSProcedureName()).isNull();
        assertThat(passed.getSCreatedBy()).isNull();
        assertThat(passed.getTCreateDate()).isNull();
        assertThat(passed.getSBrandsId()).isNull();
        assertThat(passed.getSSubsidiaryId()).isNull();
        assertThat(passed.getBDeleted()).isNull();
    }

    @Test
    void updateWithTargetNotFound_throws40400() {
        when(moduleMapper.selectById(99)).thenReturn(null);
        UpdateModuleDTO dto = baseUpdateDto();

        assertThatThrownBy(() -> service.update(99, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40400);
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void updateWithBShowPermissionNull_setsFalseInEntity() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.updateById(any(Module.class))).thenReturn(1);
        UpdateModuleDTO dto = baseUpdateDto();
        dto.setBShowPermission(null);

        service.update(10, dto);

        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper).updateById(captor.capture());
        assertThat(captor.getValue().getBShowPermission()).isFalse();
    }

    @Test
    void updateWithInvalidDisplayType_throws40010() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        UpdateModuleDTO dto = baseUpdateDto();
        dto.setSDisplayType("未知");

        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40010);
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void updateWithSelfParentId_throws40021() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        UpdateModuleDTO dto = baseUpdateDto();
        dto.setIParentId(10);

        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40021)
                .hasMessageContaining("自身");
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void updateWithMissingParent_throws40021() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.existsActiveById(42)).thenReturn(false);
        UpdateModuleDTO dto = baseUpdateDto();
        dto.setIParentId(42);

        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40021)
                .hasMessageContaining("父模块不存在");
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void updateWithCyclicParent_throws40021() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.existsActiveById(20)).thenReturn(true);
        when(moduleMapper.selectParentIdById(20)).thenReturn(10);
        UpdateModuleDTO dto = baseUpdateDto();
        dto.setIParentId(20);

        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40021)
                .hasMessageContaining("环路");
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void deleteWithValidId_softDeletes_andSetsAuditFields() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.hasActiveChildren(10)).thenReturn(false);
        when(moduleMapper.updateById(any(Module.class))).thenReturn(1);

        service.delete(10);

        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper).updateById(captor.capture());
        Module passed = captor.getValue();
        assertThat(passed.getIIncrement()).isEqualTo(10);
        assertThat(passed.getBDeleted()).isTrue();
        assertThat(passed.getTDeletedDate()).isNotNull();
        assertThat(passed.getSDeletedBy()).isNull();
        assertThat(passed.getSProcedureName()).isNull();
        assertThat(passed.getSCreatedBy()).isNull();
        assertThat(passed.getSBrandsId()).isNull();
    }

    @Test
    void deleteWithTargetNotFound_throws40400() {
        when(moduleMapper.selectById(99)).thenReturn(null);

        assertThatThrownBy(() -> service.delete(99))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40400);
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void deleteWithTargetAlreadyDeleted_throws40400() {
        Module deleted = stubExistingModule(10);
        deleted.setBDeleted(true);
        when(moduleMapper.selectById(10)).thenReturn(deleted);

        assertThatThrownBy(() -> service.delete(10))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40400);
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void deleteWithActiveChildren_throws40901() {
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.hasActiveChildren(10)).thenReturn(true);

        assertThatThrownBy(() -> service.delete(10))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40901);
        verify(moduleMapper, never()).updateById(any(Module.class));
    }

    @Test
    void deleteSetsDeletedByFromAuthenticatedUser() {
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken("BOB", null, Collections.emptyList()));
        when(moduleMapper.selectById(10)).thenReturn(stubExistingModule(10));
        when(moduleMapper.hasActiveChildren(10)).thenReturn(false);
        when(moduleMapper.updateById(any(Module.class))).thenReturn(1);

        service.delete(10);

        ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
        verify(moduleMapper).updateById(captor.capture());
        assertThat(captor.getValue().getSDeletedBy()).isEqualTo("BOB");
    }

    @Test
    void listTree_emptyKeyword_invokesMapperWithEmptyString_returnsAssembledTree() {
        Module root1 = treeRow(1, "根1", null, 0);
        Module root2 = treeRow(2, "根2", null, 0);
        Module child1 = treeRow(3, "子1", 1, 0);
        Module child2 = treeRow(4, "子2", 1, 1);
        Module grand1 = treeRow(5, "孙1", 3, 0);
        when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(root1, root2, child1, child2, grand1));

        List<ModuleTreeVO> result = service.listTree("");

        assertThat(result).extracting(ModuleTreeVO::getIIncrement).containsExactly(1, 2);
        assertThat(result.get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(3, 4);
        assertThat(result.get(0).getChildren().get(0).getChildren()).extracting(ModuleTreeVO::getIIncrement).containsExactly(5);
        assertThat(result.get(1).getChildren()).isEmpty();
    }

    @Test
    void listTree_nullKeyword_treatedAsEmpty() {
        when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of());
        service.listTree(null);
        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        verify(moduleMapper).selectActiveByKeyword(captor.capture());
        assertThat(captor.getValue()).isEqualTo("");
    }

    @Test
    void listTree_blankKeyword_treatedAsEmpty() {
        when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of());
        service.listTree("   ");
        verify(moduleMapper).selectActiveByKeyword("");
    }

    @Test
    void listTree_keywordTooLong_throws40001() {
        String longKw = "x".repeat(101);
        assertThatThrownBy(() -> service.listTree(longKw))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
        verify(moduleMapper, never()).selectActiveByKeyword(any());
    }

    @Test
    void listTree_returnsEmptyListWhenNoMatch() {
        when(moduleMapper.selectActiveByKeyword("xyz")).thenReturn(List.of());
        List<ModuleTreeVO> result = service.listTree("xyz");
        assertThat(result).isEmpty();
    }

    @Test
    void listTree_orphansBecomeRootsInForest() {
        Module orphan = treeRow(3, "孤儿", 99, 0);
        when(moduleMapper.selectActiveByKeyword("")).thenReturn(List.of(orphan));

        List<ModuleTreeVO> result = service.listTree("");

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getIIncrement()).isEqualTo(3);
        assertThat(result.get(0).getChildren()).isEmpty();
    }

    @Test
    void listTree_keywordIsTrimmedBeforeQuery() {
        when(moduleMapper.selectActiveByKeyword("系统")).thenReturn(List.of());
        service.listTree("  系统  ");
        verify(moduleMapper).selectActiveByKeyword("系统");
    }

    private Module treeRow(int id, String name, Integer parentId, int sortOrder) {
        Module m = new Module();
        m.setIIncrement(id);
        m.setSModuleNameZh(name);
        m.setSDisplayType("手机端");
        m.setSManageDeptEn("IT");
        m.setIParentId(parentId);
        m.setISortOrder(sortOrder);
        return m;
    }

    private UpdateModuleDTO baseUpdateDto() {
        UpdateModuleDTO dto = new UpdateModuleDTO();
        dto.setSDisplayType("手机端");
        dto.setSModuleType("业务模块");
        dto.setSManageDeptEn("IT");
        dto.setSModuleNameZh("更新后");
        dto.setBShowPermission(false);
        return dto;
    }

    private Module stubExistingModule(Integer id) {
        Module m = new Module();
        m.setIIncrement(id);
        m.setSProcedureName("sp_test_existing");
        m.setSCreatedBy("ORIG_USER");
        m.setBDeleted(false);
        return m;
    }

    private CreateModuleDTO baseDto() {
        CreateModuleDTO dto = new CreateModuleDTO();
        dto.setSDisplayType("手机端");
        dto.setSProcedureName("sp_test_unit");
        dto.setSModuleType("业务模块");
        dto.setSManageDeptEn("IT");
        dto.setSModuleNameZh("单测模块");
        return dto;
    }
}
