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

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
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.result.MockMvcResultMatchers.content;
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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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 = {
                "auth.bootstrap.admin.username=m003-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",
                "auth.session.ttl-minutes=720"
        })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AuthControllerTest {

    private static final String UTC_INSTANT_PATTERN = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$";

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void shouldLoginBootstrapAdminAndReturnCurrentUserContext() throws Exception {
        MvcResult loginResult = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "m003-admin",
                                  "password": "Admin@123456"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.access_token").value(notNullValue()))
                .andExpect(jsonPath("$.token_type").value("Bearer"))
                .andExpect(jsonPath("$.expires_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.user.username").value("m003-admin"))
                .andExpect(jsonPath("$.user.display_name").value("系统管理员"))
                .andExpect(jsonPath("$.user.super_admin").value(true))
                .andExpect(jsonPath("$.default_hotel_id").value("HOTEL-TEST"))
                .andExpect(jsonPath("$.hotels[0].hotel_id").value("HOTEL-TEST"))
                .andExpect(jsonPath("$.hotels[0].hotel_name").value("测试酒店"))
                .andExpect(jsonPath("$.hotels[0].time_zone").value("Asia/Bangkok"))
                .andExpect(jsonPath("$.permissions").value(contains(
                        "SOURCE_MESSAGE_READ",
                        "SOURCE_MESSAGE_ORIGINAL_READ",
                        "RESERVATION_ORDER_READ",
                        "RESERVATION_TASK_READ",
                        "RESERVATION_TASK_EDIT",
                        "RESERVATION_TASK_CONFIRM",
                        "RESERVATION_OPERA_SIM_EXECUTE",
                        "RESERVATION_AUDIT_READ",
                        "HOTEL_SWITCH",
                        "SYSTEM_AUTH_READ",
                        "SYSTEM_USER_MANAGE",
                        "SYSTEM_ROLE_MANAGE",
                        "SYSTEM_MENU_MANAGE",
                        "HOTEL_MANAGE",
                        "SYSTEM_ADMIN_CONSOLE_ACCESS",
                        "SYSTEM_DEBUG_EML_RUN")))
                .andExpect(jsonPath("$.menus[0].menu_code").value("RESERVATION_ORDERS"))
                .andExpect(jsonPath("$.menus[0].route_path").value("/reservation/orders"))
                .andExpect(jsonPath("$.menus[0].icon_key").value("pi pi-list"))
                .andExpect(jsonPath("$.menus[1].menu_code").value("RESERVATION_TASKS"))
                .andExpect(jsonPath("$.menus[1].route_path").value("/reservation/tasks"))
                .andExpect(jsonPath("$.menus[1].icon_key").value("pi pi-check-square"))
                .andExpect(jsonPath("$.menus[2].menu_code").value("DEBUG_EML_SUPERAGENT"))
                .andExpect(jsonPath("$.menus[2].route_path").value("/debug/eml-superagent"))
                .andExpect(jsonPath("$.menus[2].icon_key").value("pi pi-upload"))
                .andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"))
                .andExpect(jsonPath("$.menus[3].route_path").value("/system"))
                .andExpect(jsonPath("$.menus[3].icon_key").value("pi pi-cog"))
                .andReturn();

        String token = tokenFrom(loginResult);

        mockMvc.perform(get("/api/auth/me")
                        .header("Authorization", "Bearer " + token))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.user.username").value("m003-admin"))
                .andExpect(jsonPath("$.default_hotel_id").value("HOTEL-TEST"))
                .andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"));
    }

    @Test
    void shouldRejectInvalidLoginWithoutLeakingAccountState() throws Exception {
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "m003-admin",
                                  "password": "wrong-password"
                                }
                                """))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_INVALID_CREDENTIALS"))
                .andExpect(jsonPath("$.message").value("用户名或密码错误。"))
                .andExpect(content().string(not("DISABLED")))
                .andExpect(content().string(not("not found")));
    }

    @Test
    void shouldRequireTokenForAuthMeAndRevokeSessionOnLogout() throws Exception {
        mockMvc.perform(get("/api/auth/me"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));

        String token = tokenFrom(login());

        mockMvc.perform(post("/api/auth/logout")
                        .header("Authorization", "Bearer " + token))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));

        mockMvc.perform(post("/api/auth/logout")
                        .header("Authorization", "Bearer " + token))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));

        mockMvc.perform(get("/api/auth/me")
                        .header("Authorization", "Bearer " + token))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_SESSION_INVALID"));
    }

    @Test
    void shouldKeepExistingBusinessEndpointsCompatibleWhenTokenIsMissingOrInvalid() throws Exception {
        mockMvc.perform(get("/api/reservation/orders")
                        .param("hotel_id", "HOTEL-TEST")
                        .param("page_num", "1")
                        .param("page_size", "1"))
                .andExpect(status().isOk());

        mockMvc.perform(get("/api/reservation/orders")
                        .header("Authorization", "Bearer invalid-token")
                        .param("hotel_id", "HOTEL-TEST")
                        .param("page_num", "1")
                        .param("page_size", "1"))
                .andExpect(status().isOk());
    }

    private MvcResult login() throws Exception {
        return mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "m003-admin",
                                  "password": "Admin@123456"
                                }
                                """))
                .andExpect(status().isOk())
                .andReturn();
    }

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