package com.kt.ai.test.unit.service;

import com.kt.ai.service.CacheService;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.lenient;

/**
 * CacheService 단위 테스트
 *
 * @author AI Service Team
 * @since 1.0.0
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("CacheService 단위 테스트")
class CacheServiceUnitTest {

    // Constants
    private static final String VALID_KEY = "test:key";
    private static final String VALID_VALUE = "test-value";
    private static final long VALID_TTL = 3600L;
    private static final String VALID_JOB_ID = "job-123";
    private static final String VALID_EVENT_ID = "evt-001";
    private static final String VALID_INDUSTRY = "음식점";
    private static final String VALID_REGION = "강남구";

    @Mock
    private RedisTemplate<String, Object> redisTemplate;

    @Mock
    private ValueOperations<String, Object> valueOperations;

    @InjectMocks
    private CacheService cacheService;

    @BeforeEach
    void setUp() {
        // TTL 값 설정
        ReflectionTestUtils.setField(cacheService, "recommendationTtl", 86400L);
        ReflectionTestUtils.setField(cacheService, "jobStatusTtl", 86400L);
        ReflectionTestUtils.setField(cacheService, "trendTtl", 3600L);

        // RedisTemplate Mock 설정 (lenient를 사용하여 모든 테스트에서 사용하지 않아도 됨)
        lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations);
    }

    // ========== set() 메서드 테스트 ==========

    @Test
    @DisplayName("Given valid key and value, When set, Then success")
    void givenValidKeyAndValue_whenSet_thenSuccess() {
        // Given
        doNothing().when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class));

        // When
        cacheService.set(VALID_KEY, VALID_VALUE, VALID_TTL);

        // Then
        verify(valueOperations, times(1))
                .set(VALID_KEY, VALID_VALUE, VALID_TTL, TimeUnit.SECONDS);
    }

    @Test
    @DisplayName("Given Redis exception, When set, Then log error and continue")
    void givenRedisException_whenSet_thenLogErrorAndContinue() {
        // Given
        doThrow(new RuntimeException("Redis connection failed"))
                .when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class));

        // When & Then (예외가 전파되지 않아야 함)
        cacheService.set(VALID_KEY, VALID_VALUE, VALID_TTL);

        verify(valueOperations, times(1))
                .set(VALID_KEY, VALID_VALUE, VALID_TTL, TimeUnit.SECONDS);
    }

    // ========== get() 메서드 테스트 ==========

    @Test
    @DisplayName("Given existing key, When get, Then return value")
    void givenExistingKey_whenGet_thenReturnValue() {
        // Given
        when(valueOperations.get(VALID_KEY)).thenReturn(VALID_VALUE);

        // When
        Object result = cacheService.get(VALID_KEY);

        // Then
        assertThat(result).isEqualTo(VALID_VALUE);
        verify(valueOperations, times(1)).get(VALID_KEY);
    }

    @Test
    @DisplayName("Given non-existing key, When get, Then return null")
    void givenNonExistingKey_whenGet_thenReturnNull() {
        // Given
        when(valueOperations.get(VALID_KEY)).thenReturn(null);

        // When
        Object result = cacheService.get(VALID_KEY);

        // Then
        assertThat(result).isNull();
        verify(valueOperations, times(1)).get(VALID_KEY);
    }

    @Test
    @DisplayName("Given Redis exception, When get, Then return null")
    void givenRedisException_whenGet_thenReturnNull() {
        // Given
        when(valueOperations.get(VALID_KEY))
                .thenThrow(new RuntimeException("Redis connection failed"));

        // When
        Object result = cacheService.get(VALID_KEY);

        // Then
        assertThat(result).isNull();
        verify(valueOperations, times(1)).get(VALID_KEY);
    }

    // ========== delete() 메서드 테스트 ==========

    @Test
    @DisplayName("Given valid key, When delete, Then invoke RedisTemplate delete")
    void givenValidKey_whenDelete_thenInvokeRedisTemplateDelete() {
        // Given - No specific setup needed

        // When
        cacheService.delete(VALID_KEY);

        // Then
        verify(redisTemplate, times(1)).delete(VALID_KEY);
    }

    // ========== saveJobStatus() 메서드 테스트 ==========

    @Test
    @DisplayName("Given valid job status, When save, Then success")
    void givenValidJobStatus_whenSave_thenSuccess() {
        // Given
        Object jobStatus = "PROCESSING";
        doNothing().when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class));

        // When
        cacheService.saveJobStatus(VALID_JOB_ID, jobStatus);

        // Then
        verify(valueOperations, times(1))
                .set("ai:job:status:" + VALID_JOB_ID, jobStatus, 86400L, TimeUnit.SECONDS);
    }

    // ========== getJobStatus() 메서드 테스트 ==========

    @Test
    @DisplayName("Given existing job, When get status, Then return status")
    void givenExistingJob_whenGetStatus_thenReturnStatus() {
        // Given
        Object expectedStatus = "COMPLETED";
        when(valueOperations.get("ai:job:status:" + VALID_JOB_ID)).thenReturn(expectedStatus);

        // When
        Object result = cacheService.getJobStatus(VALID_JOB_ID);

        // Then
        assertThat(result).isEqualTo(expectedStatus);
        verify(valueOperations, times(1)).get("ai:job:status:" + VALID_JOB_ID);
    }

    @Test
    @DisplayName("Given non-existing job, When get status, Then return null")
    void givenNonExistingJob_whenGetStatus_thenReturnNull() {
        // Given
        when(valueOperations.get("ai:job:status:" + VALID_JOB_ID)).thenReturn(null);

        // When
        Object result = cacheService.getJobStatus(VALID_JOB_ID);

        // Then
        assertThat(result).isNull();
        verify(valueOperations, times(1)).get("ai:job:status:" + VALID_JOB_ID);
    }

    // ========== saveRecommendation() 메서드 테스트 ==========

    @Test
    @DisplayName("Given valid recommendation, When save, Then success")
    void givenValidRecommendation_whenSave_thenSuccess() {
        // Given
        Object recommendation = "recommendation-data";
        doNothing().when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class));

        // When
        cacheService.saveRecommendation(VALID_EVENT_ID, recommendation);

        // Then
        verify(valueOperations, times(1))
                .set("ai:recommendation:" + VALID_EVENT_ID, recommendation, 86400L, TimeUnit.SECONDS);
    }

    // ========== getRecommendation() 메서드 테스트 ==========

    @Test
    @DisplayName("Given existing recommendation, When get, Then return recommendation")
    void givenExistingRecommendation_whenGet_thenReturnRecommendation() {
        // Given
        Object expectedRecommendation = "recommendation-data";
        when(valueOperations.get("ai:recommendation:" + VALID_EVENT_ID))
                .thenReturn(expectedRecommendation);

        // When
        Object result = cacheService.getRecommendation(VALID_EVENT_ID);

        // Then
        assertThat(result).isEqualTo(expectedRecommendation);
        verify(valueOperations, times(1)).get("ai:recommendation:" + VALID_EVENT_ID);
    }

    // ========== saveTrend() 메서드 테스트 ==========

    @Test
    @DisplayName("Given valid trend, When save, Then success")
    void givenValidTrend_whenSave_thenSuccess() {
        // Given
        Object trend = "trend-data";
        doNothing().when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class));

        // When
        cacheService.saveTrend(VALID_INDUSTRY, VALID_REGION, trend);

        // Then
        verify(valueOperations, times(1))
                .set("ai:trend:" + VALID_INDUSTRY + ":" + VALID_REGION, trend, 3600L, TimeUnit.SECONDS);
    }

    // ========== getTrend() 메서드 테스트 ==========

    @Test
    @DisplayName("Given existing trend, When get, Then return trend")
    void givenExistingTrend_whenGet_thenReturnTrend() {
        // Given
        Object expectedTrend = "trend-data";
        when(valueOperations.get("ai:trend:" + VALID_INDUSTRY + ":" + VALID_REGION))
                .thenReturn(expectedTrend);

        // When
        Object result = cacheService.getTrend(VALID_INDUSTRY, VALID_REGION);

        // Then
        assertThat(result).isEqualTo(expectedTrend);
        verify(valueOperations, times(1))
                .get("ai:trend:" + VALID_INDUSTRY + ":" + VALID_REGION);
    }
}
