package com.walkguide.service;

import com.walkguide.dto.request.VoiceCommandUpdateRequest;
import com.walkguide.dto.response.VoiceCommandResponse;
import com.walkguide.entity.PairingRelation;
import com.walkguide.entity.User;
import com.walkguide.entity.VoiceCommandConfig;
import com.walkguide.enums.PairingStatus;
import com.walkguide.enums.VoiceCommandKey;
import com.walkguide.exception.PairingException;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.PairingRelationRepository;
import com.walkguide.repository.VoiceCommandConfigRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("VoiceCommandService Unit Tests")
class VoiceCommandServiceTest {

    @Mock
    VoiceCommandConfigRepository voiceCommandConfigRepository;

    @Mock
    PairingRelationRepository pairingRelationRepository;

    @Mock
    FcmService fcmService;

    @InjectMocks
    VoiceCommandService voiceCommandService;

    private User guardianUser;
    private User blindUser;
    private PairingRelation activePairing;
    private VoiceCommandConfig vcOpenWalkguide;
    private VoiceCommandConfig vcCallGuardian;
    private VoiceCommandConfig vcSendSos;

    @BeforeEach
    void setUp() {
        guardianUser = User.builder()
                .id(1L)
                .email("guardian@test.com")
                .role("ROLE_GUARDIAN")
                .displayName("Guardian Test")
                .fcmToken("guardian-fcm-token")
                .build();

        blindUser = User.builder()
                .id(2L)
                .email("user@test.com")
                .role("ROLE_USER")
                .displayName("User Test")
                .fcmToken("user-fcm-token-xyz")
                .build();

        activePairing = PairingRelation.builder()
                .id(10L)
                .guardian(guardianUser)
                .user(blindUser)
                .status(PairingStatus.ACTIVE)
                .build();

        vcOpenWalkguide = VoiceCommandConfig.builder()
                .id(100L)
                .userId(2L)
                .commandKey(VoiceCommandKey.OPEN_WALKGUIDE)
                .triggerPhrase("buka walkguide")
                .enabled(true)
                .build();

        vcCallGuardian = VoiceCommandConfig.builder()
                .id(101L)
                .userId(2L)
                .commandKey(VoiceCommandKey.CALL_GUARDIAN)
                .triggerPhrase("telepon guardian")
                .enabled(true)
                .build();

        vcSendSos = VoiceCommandConfig.builder()
                .id(102L)
                .userId(2L)
                .commandKey(VoiceCommandKey.SEND_SOS)
                .triggerPhrase("kirim sos")
                .enabled(false)
                .build();
    }

    // ===== getAll TESTS =====

    @Test
    @DisplayName("getAll - harus return semua voice command milik user sebagai list response")
    void getAll_shouldReturnAllVoiceCommandsForUser() {
        when(voiceCommandConfigRepository.findByUserId(2L))
                .thenReturn(List.of(vcOpenWalkguide, vcCallGuardian, vcSendSos));

        List<VoiceCommandResponse> result = voiceCommandService.getAll(2L);

        assertThat(result).hasSize(3);

        VoiceCommandResponse first = result.get(0);
        assertThat(first.getId()).isEqualTo(100L);
        assertThat(first.getCommandKey()).isEqualTo("OPEN_WALKGUIDE");
        assertThat(first.getTriggerPhrase()).isEqualTo("buka walkguide");
        assertThat(first.getEnabled()).isTrue();
        assertThat(first.getDescription()).isEqualTo("Buka menu WalkGuide");

        VoiceCommandResponse second = result.get(1);
        assertThat(second.getCommandKey()).isEqualTo("CALL_GUARDIAN");
        assertThat(second.getDescription()).isEqualTo("Telepon Guardian");

        VoiceCommandResponse third = result.get(2);
        assertThat(third.getCommandKey()).isEqualTo("SEND_SOS");
        assertThat(third.getEnabled()).isFalse();
        assertThat(third.getDescription()).isEqualTo("Kirim sinyal darurat SOS");
    }

    @Test
    @DisplayName("getAll - harus return list kosong jika user tidak punya voice command")
    void getAll_emptyList_whenUserHasNoVoiceCommands() {
        when(voiceCommandConfigRepository.findByUserId(99L)).thenReturn(List.of());

        List<VoiceCommandResponse> result = voiceCommandService.getAll(99L);

        assertThat(result).isEmpty();
        verify(voiceCommandConfigRepository).findByUserId(99L);
    }

    @Test
    @DisplayName("getAll - description harus ada untuk semua VoiceCommandKey yang terdefinisi")
    void getAll_shouldProvideDescriptionForAllDefinedKeys() {
        // Buat config dengan setiap key yang ada
        List<VoiceCommandConfig> allConfigs = List.of(
                buildVc(1L, 2L, VoiceCommandKey.OPEN_WALKGUIDE, "cmd1"),
                buildVc(2L, 2L, VoiceCommandKey.START_WALKGUIDE, "cmd2"),
                buildVc(3L, 2L, VoiceCommandKey.STOP_WALKGUIDE, "cmd3"),
                buildVc(4L, 2L, VoiceCommandKey.CALL_GUARDIAN, "cmd4"),
                buildVc(5L, 2L, VoiceCommandKey.OPEN_NOTIFICATION, "cmd5"),
                buildVc(6L, 2L, VoiceCommandKey.READ_ALL_NOTIF, "cmd6"),
                buildVc(7L, 2L, VoiceCommandKey.OPEN_SOS, "cmd7"),
                buildVc(8L, 2L, VoiceCommandKey.SEND_SOS, "cmd8"),
                buildVc(9L, 2L, VoiceCommandKey.WHERE_AM_I, "cmd9"),
                buildVc(10L, 2L, VoiceCommandKey.OPEN_ACTIVITY, "cmd10"),
                buildVc(11L, 2L, VoiceCommandKey.OPEN_NAVIGATION, "cmd11"),
                buildVc(12L, 2L, VoiceCommandKey.OPEN_SETTINGS, "cmd12"),
                buildVc(13L, 2L, VoiceCommandKey.REPEAT_LAST, "cmd13"),
                buildVc(14L, 2L, VoiceCommandKey.STOP_TTS, "cmd14")
        );

        when(voiceCommandConfigRepository.findByUserId(2L)).thenReturn(allConfigs);

        List<VoiceCommandResponse> result = voiceCommandService.getAll(2L);

        assertThat(result).hasSize(14);
        // Semua description tidak boleh kosong atau null
        result.forEach(r -> assertThat(r.getDescription())
                .as("Description untuk key %s tidak boleh kosong", r.getCommandKey())
                .isNotNull()
                .isNotBlank());
    }

    // ===== updateByGuardian TESTS =====

    @Test
    @DisplayName("updateByGuardian - harus update triggerPhrase dan enabled jika semua field diisi")
    void updateByGuardian_allFields_shouldUpdateAll() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("OPEN_WALKGUIDE");
        req.setTriggerPhrase("halo walkguide");
        req.setEnabled(false);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.OPEN_WALKGUIDE))
                .thenReturn(Optional.of(vcOpenWalkguide));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);

        assertThat(result.getCommandKey()).isEqualTo("OPEN_WALKGUIDE");
        assertThat(result.getTriggerPhrase()).isEqualTo("halo walkguide");
        assertThat(result.getEnabled()).isFalse();
        assertThat(result.getDescription()).isEqualTo("Buka menu WalkGuide");
    }

    @Test
    @DisplayName("updateByGuardian - triggerPhrase null tidak boleh mengubah nilai yang sudah ada")
    void updateByGuardian_nullTriggerPhrase_shouldKeepExistingPhrase() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("CALL_GUARDIAN");
        req.setTriggerPhrase(null);
        req.setEnabled(false);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.CALL_GUARDIAN))
                .thenReturn(Optional.of(vcCallGuardian));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);

        // triggerPhrase tidak berubah
        assertThat(result.getTriggerPhrase()).isEqualTo("telepon guardian");
        assertThat(result.getEnabled()).isFalse();
    }

    @Test
    @DisplayName("updateByGuardian - triggerPhrase blank tidak boleh mengubah nilai yang sudah ada")
    void updateByGuardian_blankTriggerPhrase_shouldKeepExistingPhrase() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("CALL_GUARDIAN");
        req.setTriggerPhrase("   ");  // blank string
        req.setEnabled(null);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.CALL_GUARDIAN))
                .thenReturn(Optional.of(vcCallGuardian));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);

        assertThat(result.getTriggerPhrase()).isEqualTo("telepon guardian");
    }

    @Test
    @DisplayName("updateByGuardian - enabled null tidak boleh mengubah nilai yang sudah ada")
    void updateByGuardian_nullEnabled_shouldKeepExistingEnabled() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("SEND_SOS");
        req.setTriggerPhrase("darurat sekarang");
        req.setEnabled(null);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.SEND_SOS))
                .thenReturn(Optional.of(vcSendSos));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);

        // enabled tetap false (nilai awal vcSendSos)
        assertThat(result.getEnabled()).isFalse();
        assertThat(result.getTriggerPhrase()).isEqualTo("darurat sekarang");
    }

    @Test
    @DisplayName("updateByGuardian - guardian tanpa pairing aktif harus throw PairingException")
    void updateByGuardian_noActivePairing_shouldThrowPairingException() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("OPEN_WALKGUIDE");

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
                .isInstanceOf(PairingException.class)
                .hasMessageContaining("Tidak ada user yang dipair");

        verify(voiceCommandConfigRepository, never()).findByUserIdAndCommandKey(any(), any());
        verify(voiceCommandConfigRepository, never()).save(any());
    }

    @Test
    @DisplayName("updateByGuardian - commandKey tidak ditemukan harus throw ResourceNotFoundException")
    void updateByGuardian_voiceCommandNotFound_shouldThrowResourceNotFoundException() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("WHERE_AM_I");

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.WHERE_AM_I))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
                .isInstanceOf(ResourceNotFoundException.class)
                .hasMessageContaining("Voice command tidak ditemukan");

        verify(voiceCommandConfigRepository, never()).save(any());
    }

    @Test
    @DisplayName("updateByGuardian - commandKey enum invalid harus throw IllegalArgumentException")
    void updateByGuardian_invalidEnumKey_shouldThrowIllegalArgumentException() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("INVALID_COMMAND_XYZ");

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));

        assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
                .isInstanceOf(IllegalArgumentException.class);

        verify(voiceCommandConfigRepository, never()).save(any());
    }

    @Test
    @DisplayName("updateByGuardian - harus menyimpan entitas yang sudah dimodifikasi ke repository")
    void updateByGuardian_shouldPersistChangesToRepository() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("SEND_SOS");
        req.setTriggerPhrase("minta tolong segera");
        req.setEnabled(true);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.SEND_SOS))
                .thenReturn(Optional.of(vcSendSos));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        voiceCommandService.updateByGuardian(1L, req);

        ArgumentCaptor<VoiceCommandConfig> captor = ArgumentCaptor.forClass(VoiceCommandConfig.class);
        verify(voiceCommandConfigRepository).save(captor.capture());

        VoiceCommandConfig saved = captor.getValue();
        assertThat(saved.getCommandKey()).isEqualTo(VoiceCommandKey.SEND_SOS);
        assertThat(saved.getTriggerPhrase()).isEqualTo("minta tolong segera");
        assertThat(saved.getEnabled()).isTrue();
    }

    @Test
    @DisplayName("updateByGuardian - harus mengirim FCM notification ke user setelah update berhasil")
    void updateByGuardian_shouldSendFcmNotificationToUser() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("OPEN_WALKGUIDE");
        req.setTriggerPhrase("ayo walkguide");
        req.setEnabled(true);

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.OPEN_WALKGUIDE))
                .thenReturn(Optional.of(vcOpenWalkguide));
        when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        voiceCommandService.updateByGuardian(1L, req);

        verify(fcmService).sendToToken(
                eq("user-fcm-token-xyz"),
                eq("Voice Command Diperbarui"),
                eq("Guardian mengubah perintah suara kamu"),
                argThat(data -> "SETTINGS_UPDATED".equals(data.get("type"))
                        && "VOICE_COMMAND".equals(data.get("settingType")))
        );
    }

    @Test
    @DisplayName("updateByGuardian - FCM tidak boleh dipanggil jika update gagal karena pairing tidak ada")
    void updateByGuardian_noPairing_fcmShouldNotBeCalled() {
        VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
        req.setCommandKey("OPEN_WALKGUIDE");

        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
                .isInstanceOf(PairingException.class);

        verifyNoInteractions(fcmService);
    }

    // ===== Helper =====

    private VoiceCommandConfig buildVc(Long id, Long userId, VoiceCommandKey key, String phrase) {
        return VoiceCommandConfig.builder()
                .id(id)
                .userId(userId)
                .commandKey(key)
                .triggerPhrase(phrase)
                .enabled(true)
                .build();
    }
}
