package com.walkguide.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.walkguide.dto.request.CallNotifyRequest;
import com.walkguide.dto.request.CallTokenRequest;
import com.walkguide.dto.response.AgoraTokenResponse;
import com.walkguide.entity.User;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.UserRepository;
import com.walkguide.security.SecurityHelper;
import com.walkguide.service.AgoraTokenService;
import com.walkguide.service.FcmService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.walkguide.security.JwtAuthFilter;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.util.Map;
import java.util.Optional;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@AutoConfigureMockMvc(addFilters = false)
@WebMvcTest(CallController.class)
@WithMockUser(username = "1", roles = "USER")
@DisplayName("CallController Unit Tests")
class CallControllerTest {

    @MockBean private JwtAuthFilter jwtAuthFilter;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private AgoraTokenService agoraTokenService;

    @MockBean
    private FcmService fcmService;

    @MockBean
    private UserRepository userRepository;

    private User sampleCaller;
    private User sampleReceiver;

    @BeforeEach
    void setUp() {
        sampleCaller = User.builder()
                .id(1L)
                .email("caller@test.com")
                .displayName("Caller User")
                .fcmToken("fcm-caller-token")
                .build();

        sampleReceiver = User.builder()
                .id(2L)
                .email("receiver@test.com")
                .displayName("Receiver Guardian")
                .fcmToken("fcm-receiver-token")
                .build();
    }

    // ===== GENERATE TOKEN =====

    @Test
    @DisplayName("POST /api/v1/shared/call/token - valid request harus return Agora token")
    void generateToken_validRequest_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            CallTokenRequest req = new CallTokenRequest();
            req.setReceiverId(2L);

            AgoraTokenResponse tokenResp = AgoraTokenResponse.builder()
                    .token("agora-rtc-token-xyz")
                    .channelName("call_1_2_1234567890")
                    .uid(1001)
                    .build();

            when(agoraTokenService.generateToken(1L, 2L)).thenReturn(tokenResp);

            mockMvc.perform(post("/api/v1/shared/call/token")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Token Agora berhasil digenerate"))
                    .andExpect(jsonPath("$.data.token").value("agora-rtc-token-xyz"))
                    .andExpect(jsonPath("$.data.channelName").value("call_1_2_1234567890"));

            verify(agoraTokenService).generateToken(1L, 2L);
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/token - receiverId null harus return 400")
    void generateToken_nullReceiverId_shouldReturn400() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            // CallTokenRequest dengan receiverId null — @Valid harus menolak
            CallTokenRequest req = new CallTokenRequest();
            // receiverId tidak di-set (null)

            mockMvc.perform(post("/api/v1/shared/call/token")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isBadRequest());

            verify(agoraTokenService, never()).generateToken(anyLong(), anyLong());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/token - service throw harus return 500")
    void generateToken_serviceThrows_shouldReturn500() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            CallTokenRequest req = new CallTokenRequest();
            req.setReceiverId(99L);

            when(agoraTokenService.generateToken(1L, 99L))
                    .thenThrow(new RuntimeException("Receiver tidak ditemukan"));

            mockMvc.perform(post("/api/v1/shared/call/token")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isInternalServerError());
        }
    }

    // ===== NOTIFY CALL =====

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - receiver punya FCM token harus kirim notifikasi")
    void notifyCall_receiverHasFcmToken_shouldReturn200AndSendFcm() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller));
            when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver));
            doNothing().when(fcmService).sendHighPriority(anyString(), anyString(), anyString(), anyMap());

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("call_1_2_1234567890");
            req.setAgoraToken("agora-token-xyz");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Notifikasi panggilan berhasil dikirim"));

            verify(fcmService).sendHighPriority(
                    eq("fcm-receiver-token"),
                    eq("📞 Panggilan Masuk"),
                    contains("Caller User"),
                    anyMap()
            );
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - receiver tidak punya FCM token harus return 200 tanpa FCM")
    void notifyCall_receiverNoFcmToken_shouldReturn200WithWarningMessage() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            User receiverNoFcm = User.builder()
                    .id(2L)
                    .email("receiver@test.com")
                    .displayName("Receiver")
                    .fcmToken(null)  // tidak punya FCM token
                    .build();

            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller));
            when(userRepository.findById(2L)).thenReturn(Optional.of(receiverNoFcm));

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("call_1_2_abc");
            req.setAgoraToken("token-xyz");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value(
                            "Panggilan dikirim (receiver mungkin tidak menerima push notification)"));

            // FCM tidak boleh dipanggil karena tidak ada token
            verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - receiver FCM token blank harus return 200 tanpa FCM")
    void notifyCall_receiverBlankFcmToken_shouldReturn200WithoutFcm() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            User receiverBlankFcm = User.builder()
                    .id(2L)
                    .email("receiver@test.com")
                    .displayName("Receiver")
                    .fcmToken("   ")  // blank
                    .build();

            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller));
            when(userRepository.findById(2L)).thenReturn(Optional.of(receiverBlankFcm));

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("channel-abc");
            req.setAgoraToken("token");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value(
                            "Panggilan dikirim (receiver mungkin tidak menerima push notification)"));

            verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - caller tidak ditemukan harus return 404")
    void notifyCall_callerNotFound_shouldReturn404() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(1L))
                    .thenThrow(new ResourceNotFoundException("Caller not found"));

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("channel-abc");
            req.setAgoraToken("token");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isNotFound());

            verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - receiver tidak ditemukan harus return 404")
    void notifyCall_receiverNotFound_shouldReturn404() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller));
            when(userRepository.findById(2L))
                    .thenThrow(new ResourceNotFoundException("Receiver not found"));

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("channel-abc");
            req.setAgoraToken("token");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isNotFound());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/notify - caller displayName null harus pakai email sebagai pengganti")
    void notifyCall_callerNoDisplayName_shouldUseEmailAsFallback() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            User callerNoName = User.builder()
                    .id(1L)
                    .email("noreply@test.com")
                    .displayName(null)  // tidak ada displayName
                    .build();

            when(userRepository.findById(1L)).thenReturn(Optional.of(callerNoName));
            when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver));
            doNothing().when(fcmService).sendHighPriority(anyString(), anyString(), anyString(), anyMap());

            CallNotifyRequest req = new CallNotifyRequest();
            req.setReceiverId(2L);
            req.setChannelName("channel-abc");
            req.setAgoraToken("token");
            req.setReceiverUid(1002);

            mockMvc.perform(post("/api/v1/shared/call/notify")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk());

            // Pastikan body notifikasi menggunakan email sebagai fallback
            verify(fcmService).sendHighPriority(
                    anyString(),
                    anyString(),
                    contains("noreply@test.com"),
                    anyMap()
            );
        }
    }

    // ===== END CALL =====

    @Test
    @DisplayName("POST /api/v1/shared/call/end - otherId valid dan punya FCM token harus kirim notifikasi berakhir")
    void endCall_otherHasFcmToken_shouldSendEndNotification() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver));
            doNothing().when(fcmService).sendToToken(anyString(), anyString(), anyString(), anyMap());

            Map<String, Long> body = Map.of("otherId", 2L);

            mockMvc.perform(post("/api/v1/shared/call/end")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(body)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Call ended"));

            verify(fcmService).sendToToken(
                    eq("fcm-receiver-token"),
                    eq("Panggilan Berakhir"),
                    eq("Panggilan telah berakhir"),
                    anyMap()
            );
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/end - otherId null harus return 200 tanpa kirim FCM")
    void endCall_nullOtherId_shouldReturn200WithoutFcm() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            Map<String, Long> body = Map.of();  // tidak ada otherId

            mockMvc.perform(post("/api/v1/shared/call/end")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(body)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Call ended"));

            verify(fcmService, never()).sendToToken(anyString(), anyString(), anyString(), anyMap());
        }
    }

    @Test
    @DisplayName("POST /api/v1/shared/call/end - other tidak punya FCM token harus return 200 tanpa FCM")
    void endCall_otherNoFcmToken_shouldReturn200WithoutFcm() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            User otherNoFcm = User.builder()
                    .id(2L)
                    .email("other@test.com")
                    .fcmToken(null)
                    .build();
            when(userRepository.findById(2L)).thenReturn(Optional.of(otherNoFcm));

            Map<String, Long> body = Map.of("otherId", 2L);

            mockMvc.perform(post("/api/v1/shared/call/end")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(body)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Call ended"));

            verify(fcmService, never()).sendToToken(anyString(), anyString(), anyString(), anyMap());
        }
    }

    @Test
    @WithMockUser(username = "2", roles = "GUARDIAN")
    @DisplayName("POST /api/v1/shared/call/end - Guardian juga bisa end call")
    void endCall_asGuardian_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L);
            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller));
            doNothing().when(fcmService).sendToToken(anyString(), anyString(), anyString(), anyMap());

            Map<String, Long> body = Map.of("otherId", 1L);

            mockMvc.perform(post("/api/v1/shared/call/end")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(body)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Call ended"));
        }
    }
}
