package cn.nianxx.thhotel.platform.system.control;

import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionStatus;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleCode;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleStatus;
import cn.nianxx.thhotel.platform.access.domain.PlatformPermissionEntity;
import cn.nianxx.thhotel.platform.access.domain.PlatformRoleEntity;
import cn.nianxx.thhotel.platform.access.repository.PlatformAccessRepository;
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

@SpringBootTest(
        classes = ThHotelApplication.class,
        properties = {
                "spring.datasource.url=jdbc:h2:mem:m006_admin_readonly;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
                "auth.bootstrap.admin.username=m006-admin",
                "auth.bootstrap.admin.password=Admin@123456",
                "auth.bootstrap.admin.display-name=系统管理员",
                "auth.bootstrap.default-hotel-id=HOTEL-TEST",
                "auth.bootstrap.default-hotel-name=测试酒店",
                "auth.bootstrap.default-hotel-time-zone=Asia/Bangkok"
        })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AdminReadonlyControllerTest {

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private PlatformIdentityRepository identityRepository;
    @Autowired
    private PlatformAccessRepository accessRepository;
    @Autowired
    private PlatformHotelRepository hotelRepository;
    @Autowired
    private AuthPasswordService passwordService;

    @BeforeEach
    void ensureViewerUser() {
        PlatformUserEntity user = identityRepository.findUserByUsername("m006-viewer")
                .orElseGet(() -> {
                    LocalDateTime now = LocalDateTime.now();
                    PlatformUserEntity created = new PlatformUserEntity();
                    created.setUsername("m006-viewer");
                    created.setPasswordHash(passwordService.hash("Viewer@123456"));
                    created.setDisplayName("只读用户");
                    created.setUserStatus(PlatformUserStatus.ACTIVE.name());
                    created.setSuperAdmin(false);
                    created.setPasswordChangedAt(now);
                    created.setCreatedAt(now);
                    created.setUpdatedAt(now);
                    identityRepository.insertUser(created);
                    return created;
                });
        PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
                .orElseThrow();
        accessRepository.ensureUserRole(user.getId(), viewerRole.getId());
        hotelRepository.ensureUserHotel(user.getId(), "HOTEL-TEST", true);
    }

    @Test
    void shouldRejectAdminEndpointWhenTokenMissing() throws Exception {
        mockMvc.perform(get("/api/admin/users")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("ADMIN_AUTH_REQUIRED"));
    }

    @Test
    void shouldRejectAdminEndpointWhenTokenInvalid() throws Exception {
        mockMvc.perform(get("/api/admin/users")
                        .header("Authorization", "Bearer invalid-token")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_SESSION_INVALID"));
    }

    @Test
    void shouldAllowSystemAdminToReadAdminLists() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));

        mockMvc.perform(get("/api/admin/users")
                        .header("Authorization", "Bearer " + token)
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items.length()", greaterThanOrEqualTo(1)))
                .andExpect(jsonPath("$.page.page_num").value(1))
                .andExpect(jsonPath("$.page.page_size").value(20));

        mockMvc.perform(get("/api/admin/roles")
                        .header("Authorization", "Bearer " + token)
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].role_code").value(notNullValue()));

        mockMvc.perform(get("/api/admin/permissions")
                        .header("Authorization", "Bearer " + token))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].permission_code").value(notNullValue()));

        mockMvc.perform(get("/api/admin/menus")
                        .header("Authorization", "Bearer " + token)
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[?(@.menu_code=='SYSTEM_SETTINGS')]").exists());

        mockMvc.perform(get("/api/admin/hotels")
                        .header("Authorization", "Bearer " + token)
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].hotel_id").value("HOTEL-TEST"));
    }

    @Test
    void shouldRejectAdminEndpointWhenPermissionMissing() throws Exception {
        String token = tokenFrom(login("m006-viewer", "Viewer@123456"));

        mockMvc.perform(get("/api/admin/users")
                        .header("Authorization", "Bearer " + token)
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("ADMIN_PERMISSION_DENIED"));
    }

    @Test
    void shouldCreateUserAssignHotelsResetPasswordAndRevokeSessionWhenDisabled() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));
        PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
                .orElseThrow();
        String username = "m006-user-" + System.nanoTime();

        MvcResult createResult = mockMvc.perform(post("/api/admin/users")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "%s",
                                  "initial_password": "User@123456",
                                  "display_name": "测试用户",
                                  "user_status": "ACTIVE",
                                  "role_ids": ["%s"],
                                  "hotel_ids": ["HOTEL-TEST"],
                                  "default_hotel_id": "HOTEL-TEST"
                                }
                                """.formatted(username, viewerRole.getId())))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.username").value(username))
                .andExpect(jsonPath("$.roles[0].role_code").value(PlatformRoleCode.RESERVATION_VIEWER.name()))
                .andExpect(jsonPath("$.hotels[0].hotel_id").value("HOTEL-TEST"))
                .andReturn();
        String userId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
        String userToken = tokenFrom(login(username, "User@123456"));

        mockMvc.perform(post("/api/admin/users/{userId}/password-reset", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.user_id").value(userId))
                .andExpect(jsonPath("$.temporary_password").value(notNullValue()));

        mockMvc.perform(put("/api/admin/users/{userId}/roles", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "role_ids": []
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.roles.length()").value(0));

        mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_ids": ["HOTEL-TEST"],
                                  "default_hotel_id": "HOTEL-TEST"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.hotels[0].default_hotel").value(true));

        String disabledHotelId = "M006-DISABLED-" + System.nanoTime();
        mockMvc.perform(post("/api/admin/hotels")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_id": "%s",
                                  "hotel_name": "M006 禁用酒店",
                                  "time_zone": "Asia/Bangkok",
                                  "sort_order": 199
                                }
                                """.formatted(disabledHotelId)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.hotel_status").value("DISABLED"));

        mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_ids": ["HOTEL-TEST", "%s"],
                                  "default_hotel_id": "HOTEL-TEST"
                                }
                                """.formatted(disabledHotelId)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));

        mockMvc.perform(put("/api/admin/users/{userId}", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "display_name": "测试用户已禁用",
                                  "user_status": "DISABLED"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.user_status").value("DISABLED"));

        mockMvc.perform(get("/api/admin/audits")
                        .header("Authorization", "Bearer " + token)
                        .param("target_type", "PLATFORM_USER")
                        .param("target_id", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].target_type").value("PLATFORM_USER"));

        mockMvc.perform(get("/api/auth/me")
                        .header("Authorization", "Bearer " + userToken))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void shouldRejectDisabledRoleAssignmentToUser() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));
        PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
                .orElseThrow();
        String username = "m006-role-user-" + System.nanoTime();
        MvcResult createUserResult = mockMvc.perform(post("/api/admin/users")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "%s",
                                  "initial_password": "User@123456",
                                  "display_name": "角色校验用户",
                                  "user_status": "ACTIVE",
                                  "role_ids": ["%s"],
                                  "hotel_ids": ["HOTEL-TEST"],
                                  "default_hotel_id": "HOTEL-TEST"
                                }
                                """.formatted(username, viewerRole.getId())))
                .andExpect(status().isOk())
                .andReturn();
        String userId = objectMapper.readTree(createUserResult.getResponse().getContentAsString()).path("id").asText();

        MvcResult disabledRoleResult = mockMvc.perform(post("/api/admin/roles")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "role_code": "%s",
                                  "role_name": "M006 停用角色",
                                  "role_status": "DISABLED"
                                }
                                """.formatted("M006_DISABLED_ROLE_" + System.nanoTime())))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.role_status").value(PlatformRoleStatus.DISABLED.name()))
                .andReturn();
        String disabledRoleId = objectMapper.readTree(disabledRoleResult.getResponse().getContentAsString())
                .path("id").asText();

        mockMvc.perform(put("/api/admin/users/{userId}/roles", userId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "role_ids": ["%s"]
                                }
                                """.formatted(disabledRoleId)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
    }

    @Test
    void shouldManageCustomRoleAndRejectBuiltinRoleMutation() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));
        String roleCode = "M006_CUSTOM_" + System.nanoTime();
        PlatformPermissionEntity permission = accessRepository.findPermissionByCode("SYSTEM_USER_MANAGE").orElseThrow();

        MvcResult createResult = mockMvc.perform(post("/api/admin/roles")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "role_code": "%s",
                                  "role_name": "M006 自定义角色",
                                  "role_status": "ACTIVE"
                                }
                                """.formatted(roleCode)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.role_code").value(roleCode))
                .andExpect(jsonPath("$.system_builtin").value(false))
                .andReturn();
        String roleId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();

        mockMvc.perform(put("/api/admin/roles/{roleId}/permissions", roleId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "permission_ids": ["%s"]
                                }
                                """.formatted(permission.getId())))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.permissions[0].permission_code").value("SYSTEM_USER_MANAGE"));

        PlatformPermissionEntity disabledPermission = createDisabledPermission("M006_DISABLED_PERMISSION_");
        mockMvc.perform(put("/api/admin/roles/{roleId}/permissions", roleId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "permission_ids": ["%s"]
                                }
                                """.formatted(disabledPermission.getId())))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));

        PlatformRoleEntity builtinRole = accessRepository.findRoleByCode(PlatformRoleCode.SYSTEM_ADMIN.name())
                .orElseThrow();
        mockMvc.perform(put("/api/admin/roles/{roleId}", builtinRole.getId().toString())
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "role_name": "不允许修改",
                                  "role_status": "ACTIVE"
                                }
                                """))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
    }

    @Test
    void shouldCreateUnknownMenuAndKeepKnownRouteFlagFalse() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));
        String menuCode = "M006_MENU_" + System.nanoTime();

        MvcResult createResult = mockMvc.perform(post("/api/admin/menus")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "menu_code": "%s",
                                  "menu_name": "未来菜单",
                                  "menu_type": "PAGE",
                                  "route_path": "/future/not-ready",
                                  "permission_code": "SYSTEM_USER_MANAGE",
                                  "visible": true,
                                  "menu_status": "ACTIVE"
                                }
                                """.formatted(menuCode)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.menu_code").value(menuCode))
                .andExpect(jsonPath("$.known_route").value(false))
                .andReturn();
        String menuId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();

        mockMvc.perform(put("/api/admin/menus/{menuId}", menuId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "menu_name": "未来菜单更新",
                                  "menu_type": "PAGE",
                                  "route_path": "/system/audits",
                                  "permission_code": "SYSTEM_USER_MANAGE",
                                  "visible": true,
                                  "menu_status": "ACTIVE"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.menu_code").value(menuCode))
                .andExpect(jsonPath("$.known_route").value(true));

        PlatformPermissionEntity disabledPermission = createDisabledPermission("M006_MENU_DISABLED_PERMISSION_");
        mockMvc.perform(post("/api/admin/menus")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "menu_code": "%s",
                                  "menu_name": "停用权限菜单",
                                  "menu_type": "PAGE",
                                  "route_path": "/system/users",
                                  "permission_code": "%s",
                                  "visible": true,
                                  "menu_status": "ACTIVE"
                                }
                                """.formatted("M006_MENU_DISABLED_" + System.nanoTime(),
                                        disabledPermission.getPermissionCode())))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
    }

    @Test
    void shouldCreateDisabledHotelAndRejectBreakingSingleActiveHotelRule() throws Exception {
        String token = tokenFrom(login("m006-admin", "Admin@123456"));
        String hotelId = "M006-HOTEL-" + System.nanoTime();

        mockMvc.perform(post("/api/admin/hotels")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_id": "%s",
                                  "hotel_name": "M006 测试酒店",
                                  "time_zone": "Asia/Bangkok",
                                  "sort_order": 99
                                }
                                """.formatted(hotelId)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.hotel_id").value(hotelId))
                .andExpect(jsonPath("$.hotel_status").value("DISABLED"));

        mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", hotelId)
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_status": "ACTIVE"
                                }
                                """))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));

        mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", "HOTEL-TEST")
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "hotel_status": "DISABLED"
                                }
                                """))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
    }

    private MvcResult login(String username, String password) throws Exception {
        return mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "%s",
                                  "password": "%s"
                                }
                                """.formatted(username, password)))
                .andExpect(status().isOk())
                .andReturn();
    }

    private String tokenFrom(MvcResult result) throws Exception {
        JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
        return json.path("access_token").asText();
    }

    private PlatformPermissionEntity createDisabledPermission(String codePrefix) {
        LocalDateTime now = LocalDateTime.now();
        PlatformPermissionEntity permission = new PlatformPermissionEntity();
        permission.setPermissionCode(codePrefix + System.nanoTime());
        permission.setPermissionName("M006 停用权限");
        permission.setPermissionGroup("SYSTEM");
        permission.setPermissionStatus(PlatformPermissionStatus.DISABLED.name());
        permission.setSystemBuiltin(false);
        permission.setCreatedAt(now);
        permission.setUpdatedAt(now);
        accessRepository.insertPermission(permission);
        return permission;
    }
}
