package com.cf.imes.module.system.service.sms;

import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.security.test.WithMockLoginUser;
import com.cf.imes.framework.test.core.ut.BaseDbAndRedisUnitTest;

import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.framework.sms.config.SmsCodeConfiguration;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.cf.imes.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.context.annotation.Import;

import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

import java.util.concurrent.TimeUnit;

import static com.cf.imes.framework.test.core.util.AssertUtils.assertServiceException;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.AUTH_MOBILE_NO_CHANGE;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_IS_VALID;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_NOT_CORRECT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_NOT_FOUND;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.USER_MOBILE_EXISTS;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.when;

@Import({SmsCodeServiceImpl.class, SmsCodeConfiguration.class})
public class SmsCodeServiceImplTest extends BaseDbAndRedisUnitTest {

    @Resource
    private SmsCodeServiceImpl smsCodeService;

    @MockitoBean
    private RedisTemplate<String, Object> redisTemplate;

    @MockitoBean
    private ZSetOperations<String, Object> zSetOperations;

    @MockitoBean
    private ValueOperations<String, Object> valueOperations;

    @MockitoBean
    private AdminUserService userService;

    @Resource
    private SmsCodeProperties smsCodeProperties;

    @MockitoBean
    private SmsSendService smsSendService;

    @BeforeEach
    void setup() {
        when(redisTemplate.opsForZSet()).thenReturn(zSetOperations);
        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
    }

    @Test
    @WithMockLoginUser(userId = 1L)
    void testSendSmsCode_userUpdateMobile_noChange() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());

        AdminUserDO user = new AdminUserDO();
        user.setId(1L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), AUTH_MOBILE_NO_CHANGE);
    }

    @Test
    void testSendSmsCode_notLogin() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(null);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), GlobalErrorCodeConstants.UNAUTHORIZED);
    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_userUpdateMobile_exists() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());

        AdminUserDO user = new AdminUserDO();
        user.setId(2L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), USER_MOBILE_EXISTS);
    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_nonUpdateMobile_userNotExist() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(null);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), AUTH_MOBILE_NOT_EXISTS);

    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_userUpdateMobile_success() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(null);

        smsCodeService.sendSmsCode(reqDTO);

        Mockito.verify(smsSendService).sendSingleSms(eq("12345678901"), isNull(), isNull(), anyString(), anyMap());
    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_success() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
        reqDTO.setCreateIp("127.0.0.1");

        AdminUserDO user = new AdminUserDO();
        user.setId(1L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        when(redisTemplate.hasKey(anyString())).thenReturn(false);
        when(zSetOperations.zCard(anyString())).thenReturn(0L);
        when(zSetOperations.add(anyString(), any(), anyDouble())).thenReturn(true);
        when(redisTemplate.expire(anyString(), anyLong(), any(TimeUnit.class))).thenReturn(true);

        when(smsSendService.sendSingleSms(
                Mockito.eq("12345678901"),
                Mockito.isNull(),
                Mockito.isNull(),
                Mockito.anyString(),
                Mockito.anyMap()
        )).thenReturn(123L);  // 返回一个模拟的日志ID

        smsCodeService.sendSmsCode(reqDTO);

        Mockito.verify(smsSendService).sendSingleSms(eq("12345678901"), isNull(), isNull(), anyString(), anyMap());
    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_moblibe_sendToFast() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
        reqDTO.setCreateIp("127.0.0.1");

        AdminUserDO user = new AdminUserDO();
        user.setId(1L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        // 模拟redis校验发送次数为6次
        when(zSetOperations.zCard(anyString())).thenReturn(6L);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_SEND_TOO_FAST);

    }

    @Test
    @WithMockLoginUser
    void testSendSmsCode_ip_sendToFast() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
        reqDTO.setCreateIp("127.0.0.1");

        AdminUserDO user = new AdminUserDO();
        user.setId(1L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        // 模拟redis校验发送次数为6次
        when(zSetOperations.zCard(anyString())).thenReturn(6L);

        SmsCodeServiceImpl spyService = Mockito.spy(smsCodeService);

        // 首次手机号校验发送次数返回true
        Mockito.doReturn(true)
                .doReturn(false)
                .when(spyService)
                .slidingWindowRateLimit(Mockito.anyString(), Mockito.anyInt(), Mockito.anyInt());

        assertServiceException(() -> spyService.sendSmsCode(reqDTO), SMS_CODE_SEND_TOO_FAST);

    }

    @Test
    void testSendSmsCode_codeExists() {
        SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
        reqDTO.setMobile("12345678901");
        reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());

        AdminUserDO user = new AdminUserDO();
        user.setId(1L);

        when(userService.getUserByUsernameAndOrganId(anyString(), any()))
                .thenReturn(user);

        // 模拟验证码已存在
        when(redisTemplate.hasKey(anyString())).thenReturn(true);

        assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_IS_VALID);
    }

    @Test
    void testUseSmsCode_userNotExists() {
        SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
                .mobile("13800000000")
                .build();

        when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(null);

        assertServiceException(() -> smsCodeService.useSmsCode(req), AUTH_MOBILE_NOT_EXISTS);
    }

    @Test
    void testUseSmsCode_codeNotFound() {
        SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
                .mobile("13800000000")
                .scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene())
                .code("123456")
                .build();

        when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(new AdminUserDO());
        when(valueOperations.get(Mockito.anyString())).thenReturn(null);

        assertServiceException(() -> smsCodeService.useSmsCode(req), SMS_CODE_NOT_FOUND);
    }

    @Test
    void testUseSmsCode_codeIncorrect_shouldThrow() {
        SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
                .mobile("13800000000")
                .scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene())
                .code("654321")
                .build();

        when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(new AdminUserDO());
        when(valueOperations.get(Mockito.anyString())).thenReturn("123456");

        assertServiceException(() -> smsCodeService.useSmsCode(req), SMS_CODE_NOT_CORRECT);
    }

    @Test
    void testUseSmsCode_success() {
        SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
                .mobile("13800000000")
                .scene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene())
                .code("123456")
                .build();

        when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(null);
        when(valueOperations.get(Mockito.anyString())).thenReturn("123456");

        smsCodeService.useSmsCode(req);

        Mockito.verify(redisTemplate).delete(Mockito.anyString());
    }

    @Test
    void testValidateSmsCode_userNotExists_shouldThrowAuthMobileNotExists() {
        AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
        reqVO.setMobile("13800000000");

        when(userService.getUserUniqueByUserName("13800000000")).thenReturn(null);

        assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), AUTH_MOBILE_NOT_EXISTS);
    }

    @Test
    void testValidateSmsCode_smsCodeNotFound_shouldThrowSmsCodeNotFound() {
        AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
        reqVO.setMobile("13800000000");

        when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
        when(valueOperations.get(anyString())).thenReturn(null);

        assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), SMS_CODE_NOT_FOUND);

    }

    @Test
    void testValidateSmsCode_smsCodeNotCorrect_shouldThrowSmsCodeNotCorrect() {
        AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
        reqVO.setMobile("13800000000");
        reqVO.setSmsCaptchaVerification("123456");

        when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
        when(valueOperations.get(anyString())).thenReturn("999999");

        assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), SMS_CODE_NOT_CORRECT);

    }

    @Test
    void testValidateSmsCode_smsCodeCorrect_shouldPass() {
        AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
        reqVO.setMobile("13800000000");
        reqVO.setSmsCaptchaVerification("123456");

        when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
        when(valueOperations.get(anyString())).thenReturn("123456");

        assertDoesNotThrow(() -> smsCodeService.validateSmsCode(reqVO));
    }
}
