package com.cf.imes.module.system.controller.admin.auth;

import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.ip.core.service.IPQueryService;
import com.cf.imes.framework.ip.core.service.dto.IPQueryDataRespDTO;
import com.cf.imes.framework.security.config.SecurityProperties;
import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.framework.security.test.WithMockLoginUser;
import com.cf.imes.framework.test.core.ut.BaseWebUnitTest;
import com.cf.imes.framework.web.config.ChenfengWebAutoConfiguration;
import com.cf.imes.module.infra.api.logger.ApiErrorLogApi;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.common.WithMockLoginUserExtension;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.permission.MenuDO;
import com.cf.imes.module.system.dal.dataobject.permission.RoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.service.auth.AdminAuthService;
import com.cf.imes.module.system.service.organ.OrganService;
import com.cf.imes.module.system.service.permission.MenuService;
import com.cf.imes.module.system.service.permission.PermissionService;
import com.cf.imes.module.system.service.permission.RoleService;
import com.cf.imes.module.system.service.sms.SmsCodeService;
import com.cf.imes.module.system.service.tokenconfig.TokenConfigService;
import com.cf.imes.module.system.service.user.AdminUserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 *
 *
 * @author Gqr
 * @since 2026/1/12 16:50
 */
@WebMvcTest(controllers = AuthController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
@Import(ChenfengWebAutoConfiguration.class)
@ExtendWith(WithMockLoginUserExtension.class)
@TestPropertySource(properties = {
        "spring.application.name=test-app"
})
class AuthControllerTest extends BaseWebUnitTest {
    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private AdminAuthService authService;

    @MockitoBean
    private AdminUserService userService;

    @MockitoBean
    private RoleService roleService;

    @MockitoBean
    private MenuService menuService;

    @MockitoBean
    private PermissionService permissionService;

    @MockitoBean
    private SecurityProperties securityProperties;

    @MockitoBean
    private SmsCodeService smsCodeService;

    @MockitoBean
    private OrganService organService;

    @MockitoBean
    private IPQueryService ipQueryService;

    @MockitoBean
    private ApiErrorLogApi apiErrorLogApi;

    @MockitoBean
    private TokenConfigService tokenConfigService;

    @Autowired
    private ObjectMapper objectMapper;

    private AuthLoginReqVO reqVO;
    private AuthLoginRespVO respVO;
    private AuthLoginSmsCheckReqVO smsCheckReq;
    private AuthSmsSendReqVO smsSendReqVO;
    private SmsCodeSendReqDTO smsCodeSendReqDTO;
    private LoginUser loginUser;

    @BeforeEach
    void setUp() {
        reqVO = new AuthLoginReqVO();
        reqVO.setUsername("testuser");
        reqVO.setPassword("12345678");

        respVO = new AuthLoginRespVO();
        respVO.setAccessToken("fake-token");

        smsCheckReq = new AuthLoginSmsCheckReqVO();
        smsCheckReq.setMobile("13800000000");
        smsCheckReq.setSmsCaptchaVerification("123456");

        smsSendReqVO = new AuthSmsSendReqVO();
        smsSendReqVO.setMobile("13800000000");
        smsSendReqVO.setScene(SmsSceneEnum.USER_LOGIN_CAPTCHAVERIFICATION.getScene());

        smsCodeSendReqDTO = new SmsCodeSendReqDTO();
        smsCodeSendReqDTO.setMobile("13800000000");
        smsCodeSendReqDTO.setScene(SmsSceneEnum.USER_LOGIN_CAPTCHAVERIFICATION.getScene());

        loginUser = new LoginUser();
        loginUser.setId(1L);
        loginUser.setOrganId(100L);
    }

    @Test
    void testManageEndPointLogin() throws Exception {
        when(authService.login(reqVO, true)).thenReturn(respVO);

        mockMvc.perform(post("/admin-api/system/auth/manage/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(reqVO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.accessToken").value("fake-token"));

        verify(authService, times(1)).login(reqVO, true);
    }

    @Test
    void testLogin() throws Exception {
        when(authService.login(reqVO, false)).thenReturn(respVO);

        mockMvc.perform(post("/admin-api/system/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(reqVO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.accessToken").value("fake-token"));

        verify(authService, times(1)).login(reqVO, false);
    }

    @Test
    void testLoginCheck() throws Exception {
        when(authService.loginCheck("13800000000")).thenReturn(true);

        mockMvc.perform(get("/admin-api/system/auth/login/check")
                        .param("mobile", "13800000000"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(true));

        verify(authService, times(1)).loginCheck("13800000000");
    }

    @Test
    void testLoginSmsCheck() throws Exception {
        when(authService.loginSmsCheck(smsCheckReq)).thenReturn(true);

        mockMvc.perform(post("/admin-api/system/auth/login/sms/check")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(smsCheckReq)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(true));

        verify(authService, times(1)).loginSmsCheck(smsCheckReq);
    }

    @Test
    void testGetOrganIdByAppToken() throws Exception {
        when(tokenConfigService.getOrganIdByAppToken("test-app-token")).thenReturn(100L);

        mockMvc.perform(post("/admin-api/system/auth/app-token/organ-id")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"appToken\":\"test-app-token\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(100L));

        verify(tokenConfigService, times(1)).getOrganIdByAppToken("test-app-token");
    }

    @Test
    void testLogout_withToken() throws Exception {
        // mock token header/parameter
        when(securityProperties.getTokenHeader()).thenReturn("Authorization");
        when(securityProperties.getTokenParameter()).thenReturn("token");

        // 模拟 SecurityFrameworkUtils.obtainAuthorization 静态方法
        try (MockedStatic<SecurityFrameworkUtils> utilities = mockStatic(SecurityFrameworkUtils.class)) {
            utilities.when(() -> SecurityFrameworkUtils.obtainAuthorization(any(HttpServletRequest.class),
                            eq("Authorization"), eq("token")))
                    .thenReturn("fake-token");

            mockMvc.perform(post("/admin-api/system/auth/logout"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").value(true));

            verify(authService, times(1)).logout("fake-token", LoginLogTypeEnum.LOGOUT_SELF.getType());
        }
    }

    @Test
    void testLogout_withoutToken() throws Exception {
        when(securityProperties.getTokenHeader()).thenReturn("Authorization");
        when(securityProperties.getTokenParameter()).thenReturn("token");

        try (MockedStatic<SecurityFrameworkUtils> utilities = mockStatic(SecurityFrameworkUtils.class)) {
            utilities.when(() -> SecurityFrameworkUtils.obtainAuthorization(any(HttpServletRequest.class),
                            eq("Authorization"), eq("token")))
                    .thenReturn(null); // 没有 token

            mockMvc.perform(post("/admin-api/system/auth/logout"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").value(true));

            // 没有 token 时，不调用 logout
            verify(authService, times(0)).logout(anyString(), anyInt());
        }
    }

    @Test
    void testSendLoginSmsCode() throws Exception {
        // authService.sendSmsCode 是 void
        doNothing().when(authService).sendSmsCode(smsSendReqVO);

        mockMvc.perform(post("/admin-api/system/auth/send-sms-code")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(smsSendReqVO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(true));

        verify(authService, times(1)).sendSmsCode(smsSendReqVO);
    }

    @Test
    void testSendSmsCode() throws Exception {
        // smsCodeService.sendSmsCode 是 void
        doNothing().when(smsCodeService).sendSmsCode(any());

        mockMvc.perform(put("/admin-api/system/auth/sms-code")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(smsCodeSendReqDTO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(true));

        // 验证 sendSmsCode 被调用
        ArgumentCaptor<SmsCodeSendReqDTO> captor = ArgumentCaptor.forClass(SmsCodeSendReqDTO.class);
        verify(smsCodeService, times(1)).sendSmsCode(captor.capture());

        SmsCodeSendReqDTO sent = captor.getValue();
        assertEquals("13800000000", sent.getMobile());
        assertNotNull(sent.getCreateIp()); // createIp 已被设置
    }

    @Test
    void testGetMesJwtToken() throws Exception {
        when(authService.getMesJwtToken()).thenReturn("fake-jwt-token");

        mockMvc.perform(get("/admin-api/system/auth/mes/token"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value("fake-jwt-token"));

        verify(authService, times(1)).getMesJwtToken();
    }

    @Test
    @WithMockLoginUser(userId = 1L, organId = 100L)
    void testGetPermissionInfo_success() throws Exception {
        AdminUserDO user = new AdminUserDO();
        user.setId(1L);
        user.setOrganId(100L);
        when(userService.getUser(1L)).thenReturn(user);

        OrganizationDO organization = new OrganizationDO();
        when(organService.validOrgan(100L)).thenReturn(organization);

        when(ipQueryService.serviceEnable()).thenReturn(false);

        Set<Long> roleIds = new HashSet<>(Arrays.asList(10L, 20L));
        when(permissionService.getUserRoleIdListByUserId(1L)).thenReturn(roleIds);

        RoleDO role1 = new RoleDO();
        role1.setId(10L);
        role1.setStatus(1);
        RoleDO role2 = new RoleDO();
        role2.setId(20L);
        role2.setStatus(1);
        when(roleService.getRoleList1(roleIds))
                .thenReturn(new ArrayList<>(Arrays.asList(role1, role2)));

        Set<Long> menuIds = new HashSet<>(Arrays.asList(1000L, 2000L));
        when(permissionService.getRoleMenuListByRoleId2(anySet(), eq(100L))).thenReturn(menuIds);

        MenuDO menu1 = new MenuDO();
        menu1.setId(1000L);
        menu1.setStatus(1);
        MenuDO menu2 = new MenuDO();
        menu2.setId(2000L);
        menu2.setStatus(1);
        when(menuService.getCustomEndPointMenuList(menuIds))
                .thenReturn(new ArrayList<>(Arrays.asList(menu1, menu2)));

        mockMvc.perform(get("/admin-api/system/auth/get-permission-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").exists());

        // 验证调用顺序/次数（可选）
        verify(userService, times(1)).getUser(1L);
        verify(organService, times(1)).validOrgan(100L);
        verify(roleService, times(1)).getRoleList1(roleIds);
        verify(menuService, times(1)).getCustomEndPointMenuList(menuIds);
    }

    @Test
    @WithMockLoginUser(userId = 1L, organId = 100L, userType = 2)
    void testGetPermissionInfo_success_branch() throws Exception {
        AdminUserDO user = new AdminUserDO();
        user.setId(1L);
        user.setOrganId(100L);
        when(userService.getUser(1L)).thenReturn(user);

        OrganizationDO organization = new OrganizationDO();
        when(organService.validOrgan(100L)).thenReturn(organization);

        IPQueryDataRespDTO ipResp = new IPQueryDataRespDTO();
        ipResp.setProv("浙江");
        ipResp.setCity("杭州");
        ipResp.setArea("西湖");
        when(ipQueryService.serviceEnable()).thenReturn(true);
        when(ipQueryService.querySource(anyString())).thenReturn(ipResp);

        Set<Long> roleIds = new HashSet<>(Arrays.asList(10L, 20L));
        when(permissionService.getUserRoleIdListByUserId(1L)).thenReturn(roleIds);

        RoleDO role1 = new RoleDO();
        role1.setId(10L);
        role1.setStatus(1);
        RoleDO role2 = new RoleDO();
        role2.setId(20L);
        role2.setStatus(1);
        when(roleService.getRoleList1(roleIds))
                .thenReturn(new ArrayList<>(Arrays.asList(role1, role2)));

        Set<Long> menuIds = new HashSet<>(Arrays.asList(1000L, 2000L));
        when(permissionService.getRoleMenuListByRoleId2(anySet(), eq(100L))).thenReturn(menuIds);

        MenuDO menu1 = new MenuDO();
        menu1.setId(1000L);
        menu1.setStatus(1);
        MenuDO menu2 = new MenuDO();
        menu2.setId(2000L);
        menu2.setStatus(1);
        when(menuService.getManageEndPointMenuList(menuIds))
                .thenReturn(new ArrayList<>(Arrays.asList(menu1, menu2)));

        mockMvc.perform(get("/admin-api/system/auth/get-permission-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").exists());

        // 验证调用顺序/次数（可选）
        verify(userService, times(1)).getUser(1L);
        verify(organService, times(1)).validOrgan(100L);
        verify(ipQueryService, times(1)).querySource(anyString());
        verify(roleService, times(1)).getRoleList1(roleIds);
        verify(menuService, times(1)).getManageEndPointMenuList(menuIds);
    }

    @Test
    @WithMockLoginUser(userId = 1L, organId = 100L)
    void testGetPermissionInfo_success_roleEmpty() throws Exception {
        AdminUserDO user = new AdminUserDO();
        user.setId(1L);
        user.setOrganId(100L);
        when(userService.getUser(1L)).thenReturn(user);

        OrganizationDO organization = new OrganizationDO();
        when(organService.validOrgan(100L)).thenReturn(organization);

        when(ipQueryService.serviceEnable()).thenReturn(false);

        Set<Long> roleIds = new HashSet<>();
        when(permissionService.getUserRoleIdListByUserId(1L)).thenReturn(roleIds);

        mockMvc.perform(get("/admin-api/system/auth/get-permission-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.roles").isEmpty());

        // 验证调用顺序/次数（可选）
        verify(userService, times(1)).getUser(1L);
        verify(organService, times(1)).validOrgan(100L);
        verify(ipQueryService, never()).querySource(anyString());
        verify(roleService, never()).getRoleList1(roleIds);
        verify(menuService, never()).getManageEndPointMenuList(anySet());
    }

    @Test
    @WithMockLoginUser(userId = 1L)
    void testGetPermissionInfo_userNull() throws Exception {
            when(userService.getUser(1L)).thenReturn(null);

            mockMvc.perform(get("/admin-api/system/auth/get-permission-info"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isEmpty()); // 返回 null

            verify(userService, times(1)).getUser(1L);
    }

    @Test
    void testGetPermissionInfo_noLoginUser() throws Exception {
        try (MockedStatic<SecurityFrameworkUtils> utilities = mockStatic(SecurityFrameworkUtils.class)) {
            utilities.when(SecurityFrameworkUtils::getLoginUser).thenReturn(null);

            mockMvc.perform(get("/admin-api/system/auth/get-permission-info"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(GlobalErrorCodeConstants.UNAUTHORIZED.getCode()));
        }
    }
}
