package com.walkguide.integration;

import org.junit.jupiter.api.Disabled;

import com.fasterxml.jackson.databind.JsonNode;
import com.walkguide.dto.request.*;
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 — Fitur Inti User: Location, Obstacle, SOS, Notification, WalkGuide.
 *
 * Menggunakan Testcontainers PostgreSQL nyata + full Spring context.
 *
 * FIELD NAMES SESUAI DTO AKTUAL:
 * - LocationUpdateRequest  : lat, lng (bukan latitude/longitude)
 * - ObstacleLogRequest     : label, confidence (Double), direction, estimatedDist (bukan distance)
 * - SosRequest             : triggerType, lat, lng
 * - SendNotificationRequest: notifType, content (bukan title/message)
 * - UserSettingsUpdateRequest: ttsLanguage, ttsSpeed (Double, bukan float)
 *
 * CATATAN IDE: Error field/method di NetBeans adalah FALSE POSITIVE (Lombok-NetBeans).
 * Berjalan normal dengan: mvn test
 */
@DisplayName("Integration Test — User Core Features (Testcontainers)")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Disabled("Requires Docker/Testcontainers")
class UserFeatureIntegrationTest extends AbstractIntegrationTest {

    private static final String USER_EMAIL     = "feature_user@walkguide.test";
    private static final String USER_PASS      = "userpass123";
    private static final String GUARDIAN_EMAIL = "feature_guardian@walkguide.test";
    private static final String GUARDIAN_PASS  = "guardpass123";

    private static String userToken;
    private static String guardianToken;
    private static String uniqueUserId;
    private static Long   sosEventId;
    private static Long   notificationId;

    // ==========================================================================
    // Setup — register & pair
    // ==========================================================================

    @Test
    @Order(1)
    @DisplayName("Setup — Register & Pair Guardian ↔ User")
    void setup_registerAndPair() throws Exception {
        JsonNode userData = registerAndGetFullResponse(USER_EMAIL, USER_PASS, "USER");
        userToken    = userData.path("accessToken").asText();
        uniqueUserId = userData.path("uniqueUserId").asText();

        JsonNode guardianData = registerAndGetFullResponse(GUARDIAN_EMAIL, GUARDIAN_PASS, "GUARDIAN");
        guardianToken = guardianData.path("accessToken").asText();

        // Guardian invite User
        InviteUserRequest inviteReq = new InviteUserRequest();
        inviteReq.setUniqueUserId(uniqueUserId);

        String inviteResponse = mockMvc.perform(post("/api/v1/shared/pairing/invite")
                        .header("Authorization", bearerToken(guardianToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(inviteReq)))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();

        Long pairingId = objectMapper.readTree(inviteResponse)
                .path("data").path("pairingId").asLong();

        // User accept
        // PairingResponseRequest: pairingId, accept
        PairingResponseRequest acceptReq = new PairingResponseRequest();
        acceptReq.setPairingId(pairingId);
        acceptReq.setAccept(true);

        mockMvc.perform(post("/api/v1/shared/pairing/respond")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(acceptReq)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.status").value("ACTIVE"));
    }

    // ==========================================================================
    // Location
    // PENTING: LocationUpdateRequest field = lat, lng (BUKAN latitude/longitude)
    // ==========================================================================

    @Test
    @Order(2)
    @DisplayName("POST /user/location — update lokasi GPS berhasil")
    void updateLocation_validCoords_returns200() throws Exception {
        // LocationUpdateRequest fields: lat, lng, accuracy, speed, heading
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.2575);
        req.setLng(112.7521);

        mockMvc.perform(post("/api/v1/user/location")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Lokasi diperbarui"));
    }

    @Test
    @Order(3)
    @DisplayName("POST /user/location — body kosong tetap valid (semua field optional)")
    void updateLocation_emptyBody_returns200OrError() throws Exception {
        // LocationUpdateRequest tidak ada @NotNull — bisa kirim kosong
        mockMvc.perform(post("/api/v1/user/location")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(result ->
                        assertThat(result.getResponse().getStatus())
                                .isBetween(200, 599)); // terima apapun, asal tidak crash 5xx karena NPE
    }

    @Test
    @Order(4)
    @DisplayName("GET /guardian/user-location — Guardian dapat melihat lokasi terakhir User")
    void guardianGetUserLocation_afterUpdate_returnsLocation() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/user-location")
                        .header("Authorization", bearerToken(guardianToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Lokasi terakhir user"));
    }

    @Test
    @Order(5)
    @DisplayName("GET /guardian/location-history — Guardian dapat riwayat lokasi terpaginasi")
    void guardianGetLocationHistory_returns200WithPage() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/location-history")
                        .header("Authorization", bearerToken(guardianToken))
                        .param("page", "0")
                        .param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Riwayat lokasi"));
    }

    // ==========================================================================
    // Obstacle
    // PENTING: ObstacleLogRequest fields = label, confidence (Double), direction,
    //          estimatedDist (String) — TIDAK ADA field "distance"
    // ==========================================================================

    @Test
    @Order(6)
    @DisplayName("POST /user/obstacle — log obstacle berhasil")
    void logObstacle_validRequest_returns200() throws Exception {
        // ObstacleLogRequest fields: label, confidence (Double), direction, estimatedDist, lat, lng
        ObstacleLogRequest req = new ObstacleLogRequest();
        req.setLabel("person");
        req.setConfidence(0.87);          // Double, bukan float
        req.setDirection("depan");
        req.setEstimatedDist("1.5m");     // String, bukan float
        req.setLat(-7.2575);
        req.setLng(112.7521);

        mockMvc.perform(post("/api/v1/user/obstacle")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Obstacle dicatat"));
    }

    @Test
    @Order(7)
    @DisplayName("GET /guardian/obstacle-logs — Guardian lihat obstacle logs User")
    void guardianGetObstacleLogs_returns200() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/obstacle-logs")
                        .header("Authorization", bearerToken(guardianToken))
                        .param("page", "0").param("size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Log obstacle user"));
    }

    // ==========================================================================
    // WalkGuide Start / Stop
    // ==========================================================================

    @Test
    @Order(8)
    @DisplayName("POST /user/walkguide/start — log WALKGUIDE_START berhasil")
    void walkGuideStart_returns200() throws Exception {
        mockMvc.perform(post("/api/v1/user/walkguide/start")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("WalkGuide dimulai"));
    }

    @Test
    @Order(9)
    @DisplayName("POST /user/walkguide/stop — log WALKGUIDE_STOP berhasil")
    void walkGuideStop_returns200() throws Exception {
        mockMvc.perform(post("/api/v1/user/walkguide/stop")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("WalkGuide dihentikan"));
    }

    @Test
    @Order(10)
    @DisplayName("GET /user/activity-logs — User lihat log aktivitas terpaginasi")
    void userActivityLogs_returns200WithEntries() throws Exception {
        mockMvc.perform(get("/api/v1/user/activity-logs")
                        .header("Authorization", bearerToken(userToken))
                        .param("page", "0").param("size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Log aktivitas"))
                .andExpect(result -> {
                    String body = result.getResponse().getContentAsString();
                    JsonNode json = objectMapper.readTree(body);
                    int totalElements = json.path("data").path("totalElements").asInt(0);
                    assertThat(totalElements).isGreaterThanOrEqualTo(2); // min: start + stop
                });
    }

    @Test
    @Order(11)
    @DisplayName("GET /guardian/activity-logs — Guardian lihat log aktivitas User")
    void guardianActivityLogs_returns200() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/activity-logs")
                        .header("Authorization", bearerToken(guardianToken))
                        .param("page", "0").param("size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Log aktivitas user"));
    }

    // ==========================================================================
    // SOS
    // PENTING: SosRequest fields = triggerType, lat, lng
    // ==========================================================================

    @Test
    @Order(12)
    @DisplayName("POST /user/sos — User kirim SOS → 200, status TRIGGERED")
    void triggerSos_returns200WithTriggeredStatus() throws Exception {
        // SosRequest fields: triggerType, lat, lng
        SosRequest req = new SosRequest();
        req.setTriggerType("MANUAL");
        req.setLat(-7.2575);
        req.setLng(112.7521);

        String responseBody = mockMvc.perform(post("/api/v1/user/sos")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("SOS dikirim! Guardian sudah diberitahu."))
                .andReturn().getResponse().getContentAsString();

        JsonNode data = objectMapper.readTree(responseBody).path("data");
        sosEventId = data.path("id").asLong();
        String sosStatus = data.path("status").asText();

        assertThat(sosEventId).isGreaterThan(0);
        assertThat(sosStatus).isEqualTo("TRIGGERED");
    }

    @Test
    @Order(13)
    @DisplayName("GET /guardian/sos-events — Guardian lihat SOS events User")
    void guardianGetSosEvents_returns200() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/sos-events")
                        .header("Authorization", bearerToken(guardianToken))
                        .param("page", "0").param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("SOS events"))
                .andExpect(result -> {
                    String body = result.getResponse().getContentAsString();
                    JsonNode json = objectMapper.readTree(body);
                    int total = json.path("data").path("totalElements").asInt(0);
                    assertThat(total).isGreaterThanOrEqualTo(1);
                });
    }

    @Test
    @Order(14)
    @DisplayName("PUT /guardian/sos/{id}/acknowledge — Guardian akui SOS → ACKNOWLEDGED")
    void guardianAcknowledgeSos_returns200WithAcknowledgedStatus() throws Exception {
        assertThat(sosEventId).isGreaterThan(0);

        mockMvc.perform(put("/api/v1/guardian/sos/" + sosEventId + "/acknowledge")
                        .header("Authorization", bearerToken(guardianToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("SOS diakui"))
                .andExpect(jsonPath("$.data.status").value("ACKNOWLEDGED"));
    }

    @Test
    @Order(15)
    @DisplayName("POST /user/sos — tanpa token → 403")
    void triggerSos_withoutToken_returns403() throws Exception {
        SosRequest req = new SosRequest();
        req.setTriggerType("MANUAL");

        mockMvc.perform(post("/api/v1/user/sos")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isForbidden());
    }

    // ==========================================================================
    // Notifications
    // PENTING: SendNotificationRequest fields = notifType, content (BUKAN title/message)
    // ==========================================================================

    @Test
    @Order(16)
    @DisplayName("POST /guardian/notifications/send — Guardian kirim notifikasi ke User")
    void sendNotification_returns200WithNotifResponse() throws Exception {
        // SendNotificationRequest fields: notifType, content, voiceNoteUrl, voiceNoteDuration
        SendNotificationRequest req = new SendNotificationRequest();
        req.setNotifType("TEXT");
        req.setContent("Hati-hati! Ada kendaraan di depanmu");

        String responseBody = mockMvc.perform(post("/api/v1/guardian/notifications/send")
                        .header("Authorization", bearerToken(guardianToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Notifikasi terkirim"))
                .andReturn().getResponse().getContentAsString();

        notificationId = objectMapper.readTree(responseBody)
                .path("data").path("id").asLong();
        assertThat(notificationId).isGreaterThan(0);
    }

    @Test
    @Order(17)
    @DisplayName("GET /user/notifications — User lihat notifikasi yang diterima")
    void getNotifications_returns200WithItems() throws Exception {
        mockMvc.perform(get("/api/v1/user/notifications")
                        .header("Authorization", bearerToken(userToken))
                        .param("page", "0").param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Notifikasi"))
                .andExpect(result -> {
                    String body = result.getResponse().getContentAsString();
                    JsonNode json = objectMapper.readTree(body);
                    int total = json.path("data").path("totalElements").asInt(0);
                    assertThat(total).isGreaterThanOrEqualTo(1);
                });
    }

    @Test
    @Order(18)
    @DisplayName("GET /user/notifications/unread-count — count notifikasi belum dibaca ≥ 1")
    void getUnreadCount_afterSend_isAtLeastOne() throws Exception {
        mockMvc.perform(get("/api/v1/user/notifications/unread-count")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Jumlah notifikasi belum dibaca"))
                .andExpect(result -> {
                    String body = result.getResponse().getContentAsString();
                    int count = objectMapper.readTree(body).path("data").asInt(0);
                    assertThat(count).isGreaterThanOrEqualTo(1);
                });
    }

    @Test
    @Order(19)
    @DisplayName("PUT /user/notifications/{id}/read — mark satu notifikasi sebagai dibaca")
    void markOneRead_returns200() throws Exception {
        assertThat(notificationId).isGreaterThan(0);

        mockMvc.perform(put("/api/v1/user/notifications/" + notificationId + "/read")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Notifikasi ditandai sudah dibaca"));
    }

    @Test
    @Order(20)
    @DisplayName("PUT /user/notifications/mark-all-read — mark semua sebagai dibaca")
    void markAllRead_returns200() throws Exception {
        mockMvc.perform(put("/api/v1/user/notifications/mark-all-read")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Semua notifikasi ditandai sudah dibaca"));
    }

    @Test
    @Order(21)
    @DisplayName("GET /user/notifications/unread-count — setelah mark-all, count = 0")
    void getUnreadCount_afterMarkAll_isZero() throws Exception {
        mockMvc.perform(get("/api/v1/user/notifications/unread-count")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value(0));
    }

    // ==========================================================================
    // User Profile & Settings
    // PENTING: UserSettingsUpdateRequest.ttsSpeed = Double (pakai 1.2, bukan 1.2f)
    // ==========================================================================

    @Test
    @Order(22)
    @DisplayName("GET /user/profile — User lihat profil diri sendiri")
    void getProfile_returns200WithEmail() throws Exception {
        mockMvc.perform(get("/api/v1/user/profile")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data.email").value(USER_EMAIL))
                .andExpect(jsonPath("$.data.uniqueUserId").isNotEmpty());
    }

    @Test
    @Order(23)
    @DisplayName("GET /user/settings — User lihat settings dirinya")
    void getSettings_returns200() throws Exception {
        mockMvc.perform(get("/api/v1/user/settings")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Settings user"));
    }

    @Test
    @Order(24)
    @DisplayName("PUT /user/settings — User update TTS language dan speed")
    void updateSettings_returns200WithUpdatedData() throws Exception {
        // UserSettingsUpdateRequest fields: ttsLanguage, ttsPitch, ttsSpeed (Double), warnNoGuardian, hapticEnabled
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        req.setTtsLanguage("id");
        req.setTtsSpeed(1.2);   // Double, bukan float — tidak pakai suffix 'f'

        mockMvc.perform(put("/api/v1/user/settings")
                        .header("Authorization", bearerToken(userToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("Settings diperbarui"));
    }

    // ==========================================================================
    // RBAC guard pada feature endpoints
    // ==========================================================================

    @Test
    @Order(25)
    @DisplayName("POST /user/location — Guardian token coba akses endpoint User → 403")
    void userEndpoint_withGuardianToken_returns403() throws Exception {
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.2575);
        req.setLng(112.7521);

        mockMvc.perform(post("/api/v1/user/location")
                        .header("Authorization", bearerToken(guardianToken))
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isForbidden());
    }

    @Test
    @Order(26)
    @DisplayName("GET /guardian/dashboard — User token coba akses endpoint Guardian → 403")
    void guardianEndpoint_withUserToken_returns403() throws Exception {
        mockMvc.perform(get("/api/v1/guardian/dashboard")
                        .header("Authorization", bearerToken(userToken)))
                .andExpect(status().isForbidden());
    }
}