package com.xly.erp.module.usr.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.common.security.JwtUtil;
import com.xly.erp.module.usr.dto.CreateUserDTO;
import com.xly.erp.module.usr.dto.LoginDTO;
import com.xly.erp.module.usr.dto.UpdateUserDTO;
import com.xly.erp.module.usr.entity.User;
import com.xly.erp.module.usr.security.LoginAttemptStore;
import com.xly.erp.module.usr.vo.LoginVO;
import com.xly.erp.module.usr.entity.UserPermission;
import com.xly.erp.module.usr.mapper.PermissionCategoryMapper;
import com.xly.erp.module.usr.mapper.StaffMapper;
import com.xly.erp.module.usr.mapper.UserMapper;
import com.xly.erp.module.usr.mapper.UserPermissionMapper;
import com.xly.erp.module.usr.service.impl.UserServiceImpl;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

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

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.anyList;
import static org.mockito.ArgumentMatchers.eq;
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 UserServiceImplTest {

    private UserMapper userMapper;
    private UserPermissionMapper userPermissionMapper;
    private StaffMapper staffMapper;
    private PermissionCategoryMapper permissionCategoryMapper;
    private BCryptPasswordEncoder encoder;
    private JwtUtil jwtUtil;
    private LoginAttemptStore loginAttemptStore;
    private UserServiceImpl service;

    @BeforeEach
    void setUp() {
        userMapper = mock(UserMapper.class);
        userPermissionMapper = mock(UserPermissionMapper.class);
        staffMapper = mock(StaffMapper.class);
        permissionCategoryMapper = mock(PermissionCategoryMapper.class);
        encoder = new BCryptPasswordEncoder();
        jwtUtil = new JwtUtil("f8d4be76bff13bf32fa33ca0b14a4b152ad01ca5719f57df18ec4ecf2370b235");
        loginAttemptStore = new LoginAttemptStore();
        TenantProperties tenant = new TenantProperties();
        tenant.setBrandsId("XLY");
        tenant.setSubsidiaryId("XLY");
        StubSecurityProperties stub = new StubSecurityProperties();
        stub.setStubUserNo("STUB_ADMIN");

        service = new UserServiceImpl(userMapper, userPermissionMapper, staffMapper,
                permissionCategoryMapper, tenant, stub, encoder, jwtUtil, loginAttemptStore);

        lenient().when(userMapper.insert(any(User.class))).thenAnswer(inv -> {
            User u = inv.getArgument(0);
            u.setIIncrement(456);
            return 1;
        });
        lenient().when(userPermissionMapper.insert(any(UserPermission.class))).thenReturn(1);
    }

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

    @Test
    void createWithValidDto_persistsUser_andUserPermissions() {
        when(staffMapper.existsActiveById(7)).thenReturn(true);
        when(permissionCategoryMapper.countActiveByIds(List.of(11, 22))).thenReturn(2);

        CreateUserDTO dto = baseDto();
        dto.setIStaffId(7);
        dto.setPermissionCategoryIds(List.of(11, 22));

        Map<String, Object> result = service.create(dto);

        assertThat(result).containsEntry("iIncrement", 456).containsEntry("sUserNo", "u001");
        ArgumentCaptor<User> userCap = ArgumentCaptor.forClass(User.class);
        verify(userMapper).insert(userCap.capture());
        User saved = userCap.getValue();
        assertThat(saved.getSBrandsId()).isEqualTo("XLY");
        assertThat(saved.getSCreatedBy()).isNull();
        assertThat(saved.getTCreateDate()).isNotNull();
        assertThat(saved.getSPasswordHash()).startsWith("$2a$");
        assertThat(saved.getBDeleted()).isFalse();
        assertThat(saved.getIStaffId()).isEqualTo(7);
        assertThat(saved.getBCanModifyDocs()).isFalse();

        ArgumentCaptor<UserPermission> permCap = ArgumentCaptor.forClass(UserPermission.class);
        verify(userPermissionMapper, times(2)).insert(permCap.capture());
        List<UserPermission> rels = permCap.getAllValues();
        assertThat(rels).extracting(UserPermission::getICategoryId).containsExactly(11, 22);
        assertThat(rels).allMatch(r -> r.getIUserId().equals(456));
        assertThat(rels).allMatch(r -> r.getSBrandsId().equals("XLY"));
    }

    @Test
    void createWithoutPermissionCategoryIds_skipsUserPermissionInserts() {
        CreateUserDTO dto = baseDto();
        dto.setPermissionCategoryIds(null);

        service.create(dto);

        verify(userMapper, times(1)).insert(any(User.class));
        verify(userPermissionMapper, never()).insert(any(UserPermission.class));
    }

    @Test
    void createWithInvalidUserType_throws40001() {
        CreateUserDTO dto = baseDto();
        dto.setSUserType("火星");
        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
        verify(userMapper, never()).insert(any(User.class));
    }

    @Test
    void createWithInvalidLanguage_throws40001() {
        CreateUserDTO dto = baseDto();
        dto.setSLanguage("ja");
        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
        verify(userMapper, never()).insert(any(User.class));
    }

    @Test
    void createWithStaffNotFound_throws40022() {
        when(staffMapper.existsActiveById(99)).thenReturn(false);
        CreateUserDTO dto = baseDto();
        dto.setIStaffId(99);
        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40022);
        verify(userMapper, never()).insert(any(User.class));
    }

    @Test
    void createWithSomeInvalidPermissionIds_throws40023() {
        when(permissionCategoryMapper.countActiveByIds(List.of(1, 2, 3))).thenReturn(2);
        CreateUserDTO dto = baseDto();
        dto.setPermissionCategoryIds(List.of(1, 2, 3));
        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40023);
        verify(userMapper, never()).insert(any(User.class));
    }

    @Test
    void createWithDuplicateUserNo_throws40020() {
        when(userMapper.insert(any(User.class)))
                .thenThrow(new DuplicateKeyException("uk_user_no"));
        CreateUserDTO dto = baseDto();
        assertThatThrownBy(() -> service.create(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40020);
    }

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

        service.create(dto);

        ArgumentCaptor<User> cap = ArgumentCaptor.forClass(User.class);
        verify(userMapper).insert(cap.capture());
        assertThat(cap.getValue().getSCreatedBy()).isEqualTo("ALICE");
    }

    @Test
    void updateWithValidDto_invokesUpdateById_andRebuildsPermissions() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(staffMapper.existsActiveById(7)).thenReturn(true);
        when(permissionCategoryMapper.countActiveByIds(List.of(11, 22))).thenReturn(2);
        when(userMapper.updateById(any(User.class))).thenReturn(1);
        when(userPermissionMapper.deleteByUserId(10)).thenReturn(3);

        UpdateUserDTO dto = baseUpdateDto();
        dto.setIStaffId(7);
        dto.setPermissionCategoryIds(List.of(11, 22));

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

        assertThat(result).isEqualTo(10);
        ArgumentCaptor<User> uc = ArgumentCaptor.forClass(User.class);
        verify(userMapper).updateById(uc.capture());
        User passed = uc.getValue();
        assertThat(passed.getIIncrement()).isEqualTo(10);
        assertThat(passed.getSUserNo()).isEqualTo("u_new");
        assertThat(passed.getSPasswordHash()).isNull();
        assertThat(passed.getSCreatedBy()).isNull();
        assertThat(passed.getTCreateDate()).isNull();
        assertThat(passed.getSBrandsId()).isNull();

        verify(userPermissionMapper, times(1)).deleteByUserId(10);
        verify(userPermissionMapper, times(2)).insert(any(UserPermission.class));
    }

    @Test
    void updateWithTargetNotFound_throws40400() {
        when(userMapper.selectById(99)).thenReturn(null);
        assertThatThrownBy(() -> service.update(99, baseUpdateDto()))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40400);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithTargetAlreadyDeleted_throws40400() {
        User deleted = stubExistingUser(10);
        deleted.setBDeleted(true);
        when(userMapper.selectById(10)).thenReturn(deleted);
        assertThatThrownBy(() -> service.update(10, baseUpdateDto()))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40400);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithBCanModifyDocsNull_setsFalseInEntity() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(userMapper.updateById(any(User.class))).thenReturn(1);
        UpdateUserDTO dto = baseUpdateDto();
        dto.setBCanModifyDocs(null);

        service.update(10, dto);

        ArgumentCaptor<User> uc = ArgumentCaptor.forClass(User.class);
        verify(userMapper).updateById(uc.capture());
        assertThat(uc.getValue().getBCanModifyDocs()).isFalse();
    }

    @Test
    void updateWithInvalidUserType_throws40001() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        UpdateUserDTO dto = baseUpdateDto();
        dto.setSUserType("火星");
        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithInvalidLanguage_throws40001() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        UpdateUserDTO dto = baseUpdateDto();
        dto.setSLanguage("ja");
        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithStaffNotFound_throws40022() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(staffMapper.existsActiveById(99)).thenReturn(false);
        UpdateUserDTO dto = baseUpdateDto();
        dto.setIStaffId(99);
        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40022);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithSomeInvalidPermissionIds_throws40023() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(permissionCategoryMapper.countActiveByIds(List.of(1, 2, 3))).thenReturn(2);
        UpdateUserDTO dto = baseUpdateDto();
        dto.setPermissionCategoryIds(List.of(1, 2, 3));
        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40023);
        verify(userMapper, never()).updateById(any(User.class));
    }

    @Test
    void updateWithDuplicateUserNo_throws40020() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(userMapper.updateById(any(User.class)))
                .thenThrow(new DuplicateKeyException("uk_user_no"));
        UpdateUserDTO dto = baseUpdateDto();
        assertThatThrownBy(() -> service.update(10, dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40020);
        verify(userPermissionMapper, never()).deleteByUserId(any());
    }

    @Test
    void updateWithEmptyPermissionIds_clearsExisting() {
        when(userMapper.selectById(10)).thenReturn(stubExistingUser(10));
        when(userMapper.updateById(any(User.class))).thenReturn(1);
        when(userPermissionMapper.deleteByUserId(10)).thenReturn(2);
        UpdateUserDTO dto = baseUpdateDto();
        dto.setPermissionCategoryIds(null);

        service.update(10, dto);

        verify(userPermissionMapper, times(1)).deleteByUserId(10);
        verify(userPermissionMapper, never()).insert(any(UserPermission.class));
    }

    @Test
    void listWithDefaults_invokesMapperWithUserNameContainsEmpty() {
        when(userMapper.pageWithFilter(eq("u.sUserName"), eq("contains"), eq(""), eq(0), eq(20)))
                .thenReturn(List.of());
        when(userMapper.countWithFilter(eq("u.sUserName"), eq("contains"), eq(""))).thenReturn(0L);

        Map<String, Object> r = service.list(null, null, null, null, null);

        assertThat(r).containsEntry("total", 0L).containsEntry("pageNum", 1).containsEntry("pageSize", 20);
    }

    @Test
    void listWithEmptyValue_skipsFilterCondition() {
        when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
        when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);

        service.list("用户号", "等于", "", 1, 20);

        ArgumentCaptor<Object> valueCap = ArgumentCaptor.forClass(Object.class);
        verify(userMapper).pageWithFilter(eq("u.sUserNo"), eq("equals"), valueCap.capture(), eq(0), eq(20));
        assertThat(valueCap.getValue()).isEqualTo("");
    }

    @Test
    void listWithKeywordTrim() {
        when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
        when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);

        service.list("用户名", "包含", "  abc  ", 1, 10);

        verify(userMapper).pageWithFilter(eq("u.sUserName"), eq("contains"), eq("abc"), eq(0), eq(10));
    }

    @Test
    void listReturnsEmptyRecords_whenMapperReturnsEmptyPage() {
        when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
        when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);

        Map<String, Object> r = service.list("用户名", "包含", "xyz", 1, 20);

        assertThat((List<?>) r.get("records")).isEmpty();
        assertThat(r).containsEntry("total", 0L);
    }

    @Test
    void listWithInvalidField_throws40001() {
        assertThatThrownBy(() -> service.list("未知字段", "包含", "x", 1, 20))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
    }

    @Test
    void listWithInvalidMatch_throws40001() {
        assertThatThrownBy(() -> service.list("用户名", "未知方式", "x", 1, 20))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
    }

    @Test
    void listWithIncompatibleFieldMatch_throws40001() {
        assertThatThrownBy(() -> service.list("作废", "包含", "true", 1, 20))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
    }

    @Test
    void listWithPageSizeExceeds100_throws40002() {
        assertThatThrownBy(() -> service.list("用户名", "包含", "", 1, 101))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40002);
    }

    @Test
    void listWithInvalidLoginDateFormat_throws40001() {
        assertThatThrownBy(() -> service.list("登录日期", "等于", "abc", 1, 20))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40001);
    }

    @Test
    void listWithBooleanFieldEqualsTrue_passesIntegerOne() {
        when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
        when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);

        service.list("作废", "等于", "true", 1, 20);

        verify(userMapper).pageWithFilter(eq("u.bDeleted"), eq("equals"), eq(1), eq(0), eq(20));
    }

    @Test
    void loginWithValidCredentials_returnsTokens_andUpdatesLastLoginDate() {
        User u = new User();
        u.setIIncrement(100);
        u.setSUserNo("u100");
        u.setSUserName("login_ok");
        u.setSUserType("普通用户");
        u.setSLanguage("zh");
        u.setSPasswordHash(encoder.encode("666666"));
        u.setBDeleted(false);
        when(userMapper.selectByUserName("login_ok")).thenReturn(u);

        LoginDTO dto = new LoginDTO();
        dto.setSUserName("login_ok");
        dto.setPassword("666666");
        dto.setVersion("标准版");

        LoginVO vo = service.login(dto);

        assertThat(vo.getAccessToken()).isNotBlank();
        assertThat(vo.getRefreshToken()).isNotBlank();
        assertThat(vo.getRefreshToken()).isNotEqualTo(vo.getAccessToken());
        assertThat(vo.getExpiresIn()).isEqualTo(28800);
        assertThat(vo.getUser().getIIncrement()).isEqualTo(100);
        assertThat(vo.getUser().getSUserNo()).isEqualTo("u100");
        verify(userMapper).updateLastLoginDate(eq(100), any());
    }

    @Test
    void loginWithUserNotFound_throws40101() {
        when(userMapper.selectByUserName("ghost")).thenReturn(null);
        LoginDTO dto = loginDto("ghost", "x");
        assertThatThrownBy(() -> service.login(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40101);
    }

    @Test
    void loginWithDeletedUser_throws40102() {
        User u = stubLoginUser("dead");
        u.setBDeleted(true);
        when(userMapper.selectByUserName("dead")).thenReturn(u);
        LoginDTO dto = loginDto("dead", "666666");
        assertThatThrownBy(() -> service.login(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40102);
    }

    @Test
    void loginWithWrongPassword_incrementsCounter_throws40101() {
        when(userMapper.selectByUserName("u_wrong")).thenReturn(stubLoginUser("u_wrong"));
        LoginDTO dto = loginDto("u_wrong", "wrong");
        assertThatThrownBy(() -> service.login(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 40101);
    }

    @Test
    void loginAfterMaxAttemptsReached_throws42301() {
        when(userMapper.selectByUserName("u_lock")).thenReturn(stubLoginUser("u_lock"));
        LoginDTO dto = loginDto("u_lock", "wrong");
        for (int i = 0; i < LoginAttemptStore.MAX_ATTEMPTS - 1; i++) {
            try { service.login(dto); } catch (BizException ignored) {}
        }
        // 第 5 次失败应触发锁定 → 42301
        assertThatThrownBy(() -> service.login(dto))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 42301);
    }

    @Test
    void loginWhileLocked_throws42301() {
        when(userMapper.selectByUserName("u_locked2")).thenReturn(stubLoginUser("u_locked2"));
        LoginDTO bad = loginDto("u_locked2", "wrong");
        for (int i = 0; i < LoginAttemptStore.MAX_ATTEMPTS; i++) {
            try { service.login(bad); } catch (BizException ignored) {}
        }
        // 锁定后即使密码正确也走 42301
        LoginDTO good = loginDto("u_locked2", "666666");
        assertThatThrownBy(() -> service.login(good))
                .isInstanceOf(BizException.class)
                .hasFieldOrPropertyWithValue("code", 42301);
    }

    @Test
    void loginSuccess_clearsFailureCounter() {
        when(userMapper.selectByUserName("u_clear")).thenReturn(stubLoginUser("u_clear"));
        try { service.login(loginDto("u_clear", "wrong")); } catch (BizException ignored) {}
        try { service.login(loginDto("u_clear", "wrong")); } catch (BizException ignored) {}
        // 现在 2 次失败；正确登录后清空
        service.login(loginDto("u_clear", "666666"));
        // 之后再 4 次错误应不锁（计数已重置）
        for (int i = 0; i < LoginAttemptStore.MAX_ATTEMPTS - 1; i++) {
            try { service.login(loginDto("u_clear", "wrong")); } catch (BizException ignored) {}
        }
        // MAX-1 次错误后仍未锁
        assertThat(loginAttemptStore.isLocked("u_clear")).isEmpty();
    }

    private LoginDTO loginDto(String userName, String password) {
        LoginDTO dto = new LoginDTO();
        dto.setSUserName(userName);
        dto.setPassword(password);
        dto.setVersion("标准版");
        return dto;
    }

    private User stubLoginUser(String userName) {
        User u = new User();
        u.setIIncrement(200);
        u.setSUserNo("u200");
        u.setSUserName(userName);
        u.setSUserType("普通用户");
        u.setSLanguage("zh");
        u.setSPasswordHash(encoder.encode("666666"));
        u.setBDeleted(false);
        return u;
    }

    private UpdateUserDTO baseUpdateDto() {
        UpdateUserDTO dto = new UpdateUserDTO();
        dto.setSUserNo("u_new");
        dto.setSUserName("用户新");
        dto.setSUserType("普通用户");
        dto.setSLanguage("zh");
        dto.setBCanModifyDocs(false);
        return dto;
    }

    private User stubExistingUser(Integer id) {
        User u = new User();
        u.setIIncrement(id);
        u.setSUserNo("u_orig");
        u.setSUserName("原用户");
        u.setSUserType("普通用户");
        u.setSLanguage("zh");
        u.setSPasswordHash("$2a$10$origHash");
        u.setSCreatedBy("ORIG_USER");
        u.setBDeleted(false);
        return u;
    }

    private CreateUserDTO baseDto() {
        CreateUserDTO dto = new CreateUserDTO();
        dto.setSUserNo("u001");
        dto.setSUserName("用户1");
        dto.setSUserType("普通用户");
        dto.setSLanguage("zh");
        dto.setBCanModifyDocs(false);
        return dto;
    }
}
