package com.walkguide.integration;

import org.junit.jupiter.api.Disabled;

import com.fasterxml.jackson.databind.JsonNode;
import com.walkguide.dto.request.LoginRequest;
import com.walkguide.dto.request.RefreshTokenRequest;
import com.walkguide.dto.request.RegisterRequest;
import org.junit.jupiter.api.*;
import org.springframework.http.MediaType;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration Test — Alur Auth (Register / Login / Refresh / Logout).
 *
 * Menggunakan Testcontainers PostgreSQL nyata + @SpringBootTest penuh.
 * Tidak ada mocking — semua hit database container sungguhan.
 *
 * CATATAN IDE: Error "cannot find symbol setEmail/setPassword/dll" di NetBeans
 * adalah FALSE POSITIVE — Lombok @Data tidak diproses oleh NetBeans LSP.
 * Test ini berjalan normal dengan: mvn test
 */
@DisplayName("Integration Test — Auth Flow (Testcontainers)")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Disabled("Requires Docker/Testcontainers")
class AuthIntegrationTest extends AbstractIntegrationTest {

    private static final String USER_EMAIL     = "integ_user@walkguide.test";
    private static final String USER_PASS      = "password123";
    private static final String GUARDIAN_EMAIL = "integ_guardian@walkguide.test";
    private static final String GUARDIAN_PASS  = "guardian123";

    // Shared state antar test (ordered)
    private static String userAccessToken;
    private static String userRefreshToken;
    private static String guardianAccessToken;

    // ==========================================================================
    // 1. Register
    // ==========================================================================

    @Test
    @Order(1)
    @DisplayName("POST /register — ROLE_USER sukses dapat access + refresh token")
    void register_roleUser_success() throws Exception {
        RegisterRequest req = new RegisterRequest();
        req.setEmail(USER_EMAIL);
        req.setPassword(USER_PASS);
        req.setDisplayName("Integration User");
        req.setRole("USER");

        String responseBody = mockMvc.perform(post("/api/v1/auth/register")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Registrasi berhasil"))
                .andExpect(jsonPath("$.data.accessToken").isNotEmpty())
                .andExpect(jsonPath("$.data.refreshToken").isNotEmpty())
                .andExpect(jsonPath("$.data.role").value("ROLE_USER"))
                .andReturn().getResponse().getContentAsString();

        JsonNode data = objectMapper.readTree(responseBody).path("data");
        userAccessToken  = data.path("accessToken").asText();
        userRefreshToken = data.path("refreshToken").asText();

        assertThat(userAccessToken).isNotBlank();
        assertThat(userRefreshToken).isNotBlank();
    }

    @Test
    @Order(2)
    @DisplayName("POST /register — ROLE_GUARDIAN sukses")
    void register_roleGuardian_success() throws Exception {
        RegisterRequest req = new RegisterRequest();
        req.setEmail(GUARDIAN_EMAIL);
        req.setPassword(GUARDIAN_PASS);
        req.setDisplayName("Integration Guardian");
        req.setRole("GUARDIAN");

        String responseBody = mockMvc.perform(post("/api/v1/auth/register")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data.role").value("ROLE_GUARDIAN"))
                .andReturn().getResponse().getContentAsString();

        guardianAccessToken = objectMapper.readTree(responseBody)
                .path("data").path("accessToken").asText();
        assertThat(guardianAccessToken).isNotBlank();
    }

    @Test
    @Order(3)
    @DisplayName("POST /register — email duplikat harus return error (bukan 200)")
    void register_duplicateEmail_returnsError() throws Exception {
        RegisterRequest req = new RegisterRequest();
        req.setEmail(USER_EMAIL); // sudah ada dari Order(1)
        req.setPassword("anotherpass");
        req.setDisplayName("Duplicate");
        req.setRole("USER");

        mockMvc.perform(post("/api/v1/auth/register")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isGreaterThanOrEqualTo(400));
    }

    @Test
    @Order(4)
    @DisplayName("POST /register — body kosong harus return 400 validation error")
    void register_emptyBody_returns400() throws Exception {
        mockMvc.perform(post("/api/v1/auth/register")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest());
    }

    // ==========================================================================
    // 2. Login
    // ==========================================================================

    @Test
    @Order(5)
    @DisplayName("POST /login — credentials valid dapat token baru")
    void login_validCredentials_returnsNewToken() throws Exception {
        LoginRequest req = new LoginRequest();
        req.setEmail(USER_EMAIL);
        req.setPassword(USER_PASS);

        mockMvc.perform(post("/api/v1/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Login berhasil"))
                .andExpect(jsonPath("$.data.accessToken").isNotEmpty())
                .andExpect(jsonPath("$.data.role").value("ROLE_USER"));
    }

    @Test
    @Order(6)
    @DisplayName("POST /login — email tidak terdaftar harus return error")
    void login_unknownEmail_returnsError() throws Exception {
        LoginRequest req = new LoginRequest();
        req.setEmail("tidakada@walkguide.test");
        req.setPassword("irrelevant");

        mockMvc.perform(post("/api/v1/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isGreaterThanOrEqualTo(400));
    }

    @Test
    @Order(7)
    @DisplayName("POST /login — password salah harus return error")
    void login_wrongPassword_returnsError() throws Exception {
        LoginRequest req = new LoginRequest();
        req.setEmail(USER_EMAIL);
        req.setPassword("wrongpassword");

        mockMvc.perform(post("/api/v1/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isGreaterThanOrEqualTo(400));
    }

    // ==========================================================================
    // 3. Refresh Token
    // ==========================================================================

    @Test
    @Order(8)
    @DisplayName("POST /refresh — refresh token valid dapat access token baru")
    void refresh_validToken_returnsNewAccessToken() throws Exception {
        assertThat(userRefreshToken).isNotBlank();

        RefreshTokenRequest req = new RefreshTokenRequest();
        req.setRefreshToken(userRefreshToken);

        mockMvc.perform(post("/api/v1/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Token diperbarui"))
                .andExpect(jsonPath("$.data.accessToken").isNotEmpty());
    }

    @Test
    @Order(9)
    @DisplayName("POST /refresh — token palsu harus return error")
    void refresh_fakeToken_returnsError() throws Exception {
        RefreshTokenRequest req = new RefreshTokenRequest();
        req.setRefreshToken("totally-invalid-refresh-token-xyz");

        mockMvc.perform(post("/api/v1/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isGreaterThanOrEqualTo(400));
    }

    // ==========================================================================
    // 4. Logout
    // ==========================================================================

    @Test
    @Order(10)
    @DisplayName("POST /logout — dengan token valid harus return 200 logout berhasil")
    void logout_withValidToken_returns200() throws Exception {
        assertThat(userAccessToken).isNotBlank();

        mockMvc.perform(post("/api/v1/auth/logout")
                        .header("Authorization", bearerToken(userAccessToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Logout berhasil"));
    }

    @Test
    @Order(11)
    @DisplayName("POST /logout — tanpa token harus return 403")
    void logout_withoutToken_returns403() throws Exception {
        mockMvc.perform(post("/api/v1/auth/logout"))
                .andExpect(status().isForbidden());
    }

    // ==========================================================================
    // 5. RBAC
    // ==========================================================================

    @Test
    @Order(12)
    @DisplayName("RBAC — USER token tidak bisa akses /api/v1/guardian/** → 403")
    void rbac_userCannotAccessGuardianEndpoint() throws Exception {
        // Login ulang karena token dari Order(1) sudah di-logout di Order(10)
        String freshUserToken = loginAndGetToken(USER_EMAIL, USER_PASS);

        mockMvc.perform(get("/api/v1/guardian/dashboard")
                        .header("Authorization", bearerToken(freshUserToken)))
                .andExpect(status().isForbidden());
    }

    @Test
    @Order(13)
    @DisplayName("RBAC — GUARDIAN token tidak bisa akses /api/v1/user/** → 403")
    void rbac_guardianCannotAccessUserEndpoint() throws Exception {
        mockMvc.perform(get("/api/v1/user/profile")
                        .header("Authorization", bearerToken(guardianAccessToken)))
                .andExpect(status().isForbidden());
    }

    @Test
    @Order(14)
    @DisplayName("RBAC — Request tanpa token ke protected endpoint → 403")
    void rbac_noToken_protectedEndpoint_returns403() throws Exception {
        mockMvc.perform(get("/api/v1/user/profile"))
                .andExpect(status().isForbidden());
    }

    // ==========================================================================
    // 6. Endpoint publik
    // ==========================================================================

    @Test
    @Order(15)
    @DisplayName("GET /ping — endpoint publik, tidak perlu auth → 200 pong")
    void ping_publicEndpoint_returns200() throws Exception {
        mockMvc.perform(get("/api/v1/auth/ping"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data").value("pong"));
    }

    @Test
    @Order(16)
    @DisplayName("GET /swagger-ui.html — endpoint publik → 2xx atau redirect")
    void swaggerUi_publicEndpoint_accessible() throws Exception {
        mockMvc.perform(get("/swagger-ui.html"))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isBetween(200, 399));
    }
}