package org.dromara.system.service.impl;

import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.system.domain.SysUser;
import org.dromara.system.mapper.*;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.redisson.api.RedissonClient;
import org.springframework.context.support.GenericApplicationContext;

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.*;

@ExtendWith(MockitoExtension.class)
@Tag("dev")
class SysUserServiceImplPasswordRecoveryTest {

    private static GenericApplicationContext applicationContext;

    @Mock private SysUserMapper userMapper;
    @Mock private SysDeptMapper deptMapper;
    @Mock private SysRoleMapper roleMapper;
    @Mock private SysPostMapper postMapper;
    @Mock private SysUserRoleMapper userRoleMapper;
    @Mock private SysUserPostMapper userPostMapper;
    @Captor private ArgumentCaptor<LambdaQueryWrapper<SysUser>> queryCaptor;
    @Captor private ArgumentCaptor<SysUser> userCaptor;

    @BeforeAll
    static void initializeInfrastructure() {
        if (TableInfoHelper.getTableInfo(SysUser.class) == null) {
            TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), SysUser.class);
        }
        applicationContext = new GenericApplicationContext();
        applicationContext.registerBean(RedissonClient.class, () -> mock(RedissonClient.class));
        applicationContext.refresh();
        new SpringUtil().setApplicationContext(applicationContext);
    }

    @AfterAll
    static void closeInfrastructure() {
        applicationContext.close();
    }

    @ParameterizedTest
    @CsvSource({
        "alice@example.com,email",
        "13305376054,phonenumber",
        "alice,userName"
    })
    void resetPasswordByVerificationCode_supportsAllAccountIdentifiers(
            String username, String expectedColumn) {
        SysUserServiceImpl service = newService();
        SysUser user = user(10L, username);
        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
        when(userMapper.selectOne(any())).thenReturn(user);
        when(userMapper.updateById(any(SysUser.class))).thenReturn(1);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
            redis.when(() -> RedisUtils.deleteObject(cacheKey)).thenReturn(true);

            boolean result = service.resetPasswordByVerificationCode(username, "123456", "newPassword");

            assertThat(result).isTrue();
            verify(userMapper).selectOne(queryCaptor.capture());
            assertThat(queryCaptor.getValue().getSqlSegment()).contains(expectedColumn);
            assertThat(queryCaptor.getValue().getParamNameValuePairs()).containsValue(username);
            verify(userMapper).updateById(userCaptor.capture());
            assertThat(userCaptor.getValue().getUserId()).isEqualTo(10L);
            assertThat(BCrypt.checkpw("newPassword", userCaptor.getValue().getPassword())).isTrue();
            redis.verify(() -> RedisUtils.deleteObject(cacheKey));
        }
    }

    @Test
    void resetPasswordByVerificationCode_rejectsWrongCodeWithoutUpdatingOrDeleting() {
        SysUserServiceImpl service = newService();
        String username = "13305376054";
        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
        when(userMapper.selectOne(any())).thenReturn(user(10L, username));

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");

            assertThatThrownBy(() ->
                service.resetPasswordByVerificationCode(username, "000000", "newPassword"))
                .isInstanceOf(UserException.class);

            verify(userMapper, never()).updateById(any(SysUser.class));
            redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
        }
    }

    @Test
    void resetPasswordByVerificationCode_rejectsExpiredCodeWithoutUpdating() {
        SysUserServiceImpl service = newService();
        String username = "alice@example.com";
        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
        when(userMapper.selectOne(any())).thenReturn(user(10L, username));

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn(null);

            assertThatThrownBy(() ->
                service.resetPasswordByVerificationCode(username, "123456", "newPassword"))
                .isInstanceOf(CaptchaExpireException.class);

            verify(userMapper, never()).updateById(any(SysUser.class));
            redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
        }
    }

    @Test
    void resetPasswordByVerificationCode_keepsCodeWhenDatabaseUpdateFails() {
        SysUserServiceImpl service = newService();
        String username = "alice";
        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
        when(userMapper.selectOne(any())).thenReturn(user(10L, username));
        when(userMapper.updateById(any(SysUser.class))).thenReturn(0);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");

            assertThatThrownBy(() ->
                service.resetPasswordByVerificationCode(username, "123456", "newPassword"))
                .isInstanceOf(ServiceException.class);

            redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
        }
    }

    @Test
    void resetPasswordByVerificationCode_ignoresLoginBasedDataPermissionDuringUpdate() {
        SysUserServiceImpl service = newService();
        String username = "alice";
        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
        when(userMapper.selectOne(any())).thenReturn(user(10L, username));
        when(userMapper.updateById(any(SysUser.class))).thenAnswer(invocation -> {
            assertThat(InterceptorIgnoreHelper.willIgnoreDataPermission("SysUserMapper.updateById"))
                .isTrue();
            return 1;
        });

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
            redis.when(() -> RedisUtils.deleteObject(cacheKey)).thenReturn(true);

            assertThat(service.resetPasswordByVerificationCode(
                username, "123456", "newPassword")).isTrue();
        }
    }

    @Test
    void resetPasswordByVerificationCode_rejectsUnknownAccountBeforeReadingCode() {
        SysUserServiceImpl service = newService();
        when(userMapper.selectOne(any())).thenReturn(null);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            assertThatThrownBy(() ->
                service.resetPasswordByVerificationCode("missing", "123456", "newPassword"))
                .isInstanceOf(UserException.class);

            redis.verifyNoInteractions();
            verify(userMapper, never()).updateById(any(SysUser.class));
        }
    }

    private SysUserServiceImpl newService() {
        return new SysUserServiceImpl(
            userMapper, deptMapper, roleMapper, postMapper, userRoleMapper, userPostMapper);
    }

    private SysUser user(Long userId, String username) {
        SysUser user = new SysUser();
        user.setUserId(userId);
        user.setUserName(username);
        user.setEmail(username);
        user.setPhonenumber(username);
        return user;
    }
}
