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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.kt.ai.exception.JobNotFoundException;
import com.kt.ai.model.dto.response.JobStatusResponse;
import com.kt.ai.model.enums.JobStatus;
import com.kt.ai.service.CacheService;
import com.kt.ai.service.JobStatusService;
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.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

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

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

    // Constants
    private static final String VALID_JOB_ID = "job-123";
    private static final String INVALID_JOB_ID = "job-999";
    private static final String VALID_MESSAGE = "AI 추천 생성 중";

    @Mock
    private CacheService cacheService;

    @Mock
    private ObjectMapper objectMapper;

    @InjectMocks
    private JobStatusService jobStatusService;

    private JobStatusResponse sampleJobStatusResponse;

    @BeforeEach
    void setUp() {
        sampleJobStatusResponse = JobStatusResponse.builder()
                .jobId(VALID_JOB_ID)
                .status(JobStatus.PROCESSING)
                .progress(50)
                .message(VALID_MESSAGE)
                .createdAt(LocalDateTime.now())
                .build();
    }

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

    @Test
    @DisplayName("Given existing job, When get status, Then return job status")
    void givenExistingJob_whenGetStatus_thenReturnJobStatus() {
        // Given
        Map<String, Object> cachedData = createCachedJobStatusData();
        when(cacheService.getJobStatus(VALID_JOB_ID)).thenReturn(cachedData);
        when(objectMapper.convertValue(cachedData, JobStatusResponse.class))
                .thenReturn(sampleJobStatusResponse);

        // When
        JobStatusResponse result = jobStatusService.getJobStatus(VALID_JOB_ID);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getJobId()).isEqualTo(VALID_JOB_ID);
        assertThat(result.getStatus()).isEqualTo(JobStatus.PROCESSING);
        assertThat(result.getProgress()).isEqualTo(50);
        assertThat(result.getMessage()).isEqualTo(VALID_MESSAGE);

        verify(cacheService, times(1)).getJobStatus(VALID_JOB_ID);
        verify(objectMapper, times(1)).convertValue(cachedData, JobStatusResponse.class);
    }

    @Test
    @DisplayName("Given non-existing job, When get status, Then throw JobNotFoundException")
    void givenNonExistingJob_whenGetStatus_thenThrowJobNotFoundException() {
        // Given
        when(cacheService.getJobStatus(INVALID_JOB_ID)).thenReturn(null);

        // When & Then
        assertThatThrownBy(() -> jobStatusService.getJobStatus(INVALID_JOB_ID))
                .isInstanceOf(JobNotFoundException.class)
                .hasMessageContaining(INVALID_JOB_ID);

        verify(cacheService, times(1)).getJobStatus(INVALID_JOB_ID);
        verify(objectMapper, never()).convertValue(any(), eq(JobStatusResponse.class));
    }

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

    @Test
    @DisplayName("Given PENDING status, When update, Then save with 0% progress")
    void givenPendingStatus_whenUpdate_thenSaveWithZeroProgress() {
        // Given
        doNothing().when(cacheService).saveJobStatus(eq(VALID_JOB_ID), any(JobStatusResponse.class));

        // When
        jobStatusService.updateJobStatus(VALID_JOB_ID, JobStatus.PENDING, "대기 중");

        // Then
        ArgumentCaptor<JobStatusResponse> captor = ArgumentCaptor.forClass(JobStatusResponse.class);
        verify(cacheService, times(1)).saveJobStatus(eq(VALID_JOB_ID), captor.capture());

        JobStatusResponse saved = captor.getValue();
        assertThat(saved.getJobId()).isEqualTo(VALID_JOB_ID);
        assertThat(saved.getStatus()).isEqualTo(JobStatus.PENDING);
        assertThat(saved.getProgress()).isEqualTo(0);
        assertThat(saved.getMessage()).isEqualTo("대기 중");
        assertThat(saved.getCreatedAt()).isNotNull();
    }

    @Test
    @DisplayName("Given PROCESSING status, When update, Then save with 50% progress")
    void givenProcessingStatus_whenUpdate_thenSaveWithFiftyProgress() {
        // Given
        doNothing().when(cacheService).saveJobStatus(eq(VALID_JOB_ID), any(JobStatusResponse.class));

        // When
        jobStatusService.updateJobStatus(VALID_JOB_ID, JobStatus.PROCESSING, VALID_MESSAGE);

        // Then
        ArgumentCaptor<JobStatusResponse> captor = ArgumentCaptor.forClass(JobStatusResponse.class);
        verify(cacheService, times(1)).saveJobStatus(eq(VALID_JOB_ID), captor.capture());

        JobStatusResponse saved = captor.getValue();
        assertThat(saved.getJobId()).isEqualTo(VALID_JOB_ID);
        assertThat(saved.getStatus()).isEqualTo(JobStatus.PROCESSING);
        assertThat(saved.getProgress()).isEqualTo(50);
        assertThat(saved.getMessage()).isEqualTo(VALID_MESSAGE);
        assertThat(saved.getCreatedAt()).isNotNull();
    }

    @Test
    @DisplayName("Given COMPLETED status, When update, Then save with 100% progress")
    void givenCompletedStatus_whenUpdate_thenSaveWithHundredProgress() {
        // Given
        doNothing().when(cacheService).saveJobStatus(eq(VALID_JOB_ID), any(JobStatusResponse.class));

        // When
        jobStatusService.updateJobStatus(VALID_JOB_ID, JobStatus.COMPLETED, "AI 추천 완료");

        // Then
        ArgumentCaptor<JobStatusResponse> captor = ArgumentCaptor.forClass(JobStatusResponse.class);
        verify(cacheService, times(1)).saveJobStatus(eq(VALID_JOB_ID), captor.capture());

        JobStatusResponse saved = captor.getValue();
        assertThat(saved.getJobId()).isEqualTo(VALID_JOB_ID);
        assertThat(saved.getStatus()).isEqualTo(JobStatus.COMPLETED);
        assertThat(saved.getProgress()).isEqualTo(100);
        assertThat(saved.getMessage()).isEqualTo("AI 추천 완료");
        assertThat(saved.getCreatedAt()).isNotNull();
    }

    @Test
    @DisplayName("Given FAILED status, When update, Then save with 0% progress")
    void givenFailedStatus_whenUpdate_thenSaveWithZeroProgress() {
        // Given
        doNothing().when(cacheService).saveJobStatus(eq(VALID_JOB_ID), any(JobStatusResponse.class));

        // When
        jobStatusService.updateJobStatus(VALID_JOB_ID, JobStatus.FAILED, "AI API 호출 실패");

        // Then
        ArgumentCaptor<JobStatusResponse> captor = ArgumentCaptor.forClass(JobStatusResponse.class);
        verify(cacheService, times(1)).saveJobStatus(eq(VALID_JOB_ID), captor.capture());

        JobStatusResponse saved = captor.getValue();
        assertThat(saved.getJobId()).isEqualTo(VALID_JOB_ID);
        assertThat(saved.getStatus()).isEqualTo(JobStatus.FAILED);
        assertThat(saved.getProgress()).isEqualTo(0);
        assertThat(saved.getMessage()).isEqualTo("AI API 호출 실패");
        assertThat(saved.getCreatedAt()).isNotNull();
    }

    // ========== Helper Methods ==========

    /**
     * Cache에 저장된 Job 상태 데이터 생성 (LinkedHashMap 형태)
     */
    private Map<String, Object> createCachedJobStatusData() {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("jobId", VALID_JOB_ID);
        data.put("status", JobStatus.PROCESSING.name());
        data.put("progress", 50);
        data.put("message", VALID_MESSAGE);
        data.put("createdAt", LocalDateTime.now().toString());
        return data;
    }
}
