package com.kt.event.participation.test.unit;

import com.kt.event.common.dto.PageResponse;
import com.kt.event.participation.application.dto.DrawWinnersRequest;
import com.kt.event.participation.application.dto.DrawWinnersResponse;
import com.kt.event.participation.application.dto.ParticipationResponse;
import com.kt.event.participation.application.service.WinnerDrawService;
import com.kt.event.participation.domain.draw.DrawLog;
import com.kt.event.participation.domain.draw.DrawLogRepository;
import com.kt.event.participation.domain.participant.Participant;
import com.kt.event.participation.domain.participant.ParticipantRepository;
import com.kt.event.participation.exception.ParticipationException.*;
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.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.*;

/**
 * WinnerDrawService 단위 테스트
 *
 * @author Digital Garage Team
 * @since 2025-01-24
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("WinnerDrawService 단위 테스트")
class WinnerDrawServiceUnitTest {

    @Mock
    private ParticipantRepository participantRepository;

    @Mock
    private DrawLogRepository drawLogRepository;

    @InjectMocks
    private WinnerDrawService winnerDrawService;

    // 테스트 데이터 상수
    private static final String VALID_EVENT_ID = "evt_20250124_001";
    private static final Integer WINNER_COUNT = 2;

    @Test
    @DisplayName("정상적인 추첨 요청이면 당첨자가 선정되고 로그가 저장된다")
    void givenValidRequest_whenDrawWinners_thenWinnersSelectedAndLogSaved() {
        // Given
        DrawWinnersRequest request = createDrawRequest(WINNER_COUNT, false);
        List<Participant> participants = createParticipantList(5);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(false);
        given(participantRepository.findByEventIdAndIsWinnerFalse(VALID_EVENT_ID))
                .willReturn(participants);
        given(participantRepository.saveAll(anyList())).willAnswer(invocation -> invocation.getArgument(0));
        given(drawLogRepository.save(any(DrawLog.class))).willAnswer(invocation -> invocation.getArgument(0));

        // When
        DrawWinnersResponse response = winnerDrawService.drawWinners(VALID_EVENT_ID, request);

        // Then
        assertThat(response).isNotNull();
        assertThat(response.getEventId()).isEqualTo(VALID_EVENT_ID);
        assertThat(response.getTotalParticipants()).isEqualTo(5);
        assertThat(response.getWinnerCount()).isEqualTo(WINNER_COUNT);
        assertThat(response.getWinners()).hasSize(WINNER_COUNT);
        assertThat(response.getDrawnAt()).isNotNull();

        then(participantRepository).should(times(1)).saveAll(anyList());
        then(drawLogRepository).should(times(1)).save(any(DrawLog.class));
    }

    @Test
    @DisplayName("이미 추첨이 완료된 이벤트면 AlreadyDrawnException이 발생한다")
    void givenAlreadyDrawn_whenDrawWinners_thenThrowException() {
        // Given
        DrawWinnersRequest request = createDrawRequest(WINNER_COUNT, false);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(true);

        // When & Then
        assertThatThrownBy(() -> winnerDrawService.drawWinners(VALID_EVENT_ID, request))
                .isInstanceOf(AlreadyDrawnException.class);

        then(participantRepository).should(never()).findByEventIdAndIsWinnerFalse(anyString());
    }

    @Test
    @DisplayName("참여자 수가 당첨자 수보다 적으면 InsufficientParticipantsException이 발생한다")
    void givenInsufficientParticipants_whenDrawWinners_thenThrowException() {
        // Given
        DrawWinnersRequest request = createDrawRequest(10, false);
        List<Participant> participants = createParticipantList(5);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(false);
        given(participantRepository.findByEventIdAndIsWinnerFalse(VALID_EVENT_ID))
                .willReturn(participants);

        // When & Then
        assertThatThrownBy(() -> winnerDrawService.drawWinners(VALID_EVENT_ID, request))
                .isInstanceOf(InsufficientParticipantsException.class);

        then(participantRepository).should(never()).saveAll(anyList());
        then(drawLogRepository).should(never()).save(any(DrawLog.class));
    }

    @Test
    @DisplayName("매장 방문 보너스 적용 시 가중치가 반영된 추첨이 이루어진다")
    void givenApplyBonus_whenDrawWinners_thenWeightedDraw() {
        // Given
        DrawWinnersRequest request = createDrawRequest(WINNER_COUNT, true);
        List<Participant> participants = createParticipantList(5);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(false);
        given(participantRepository.findByEventIdAndIsWinnerFalse(VALID_EVENT_ID))
                .willReturn(participants);
        given(participantRepository.saveAll(anyList())).willAnswer(invocation -> invocation.getArgument(0));
        given(drawLogRepository.save(any(DrawLog.class))).willAnswer(invocation -> invocation.getArgument(0));

        // When
        DrawWinnersResponse response = winnerDrawService.drawWinners(VALID_EVENT_ID, request);

        // Then
        assertThat(response.getWinnerCount()).isEqualTo(WINNER_COUNT);
        then(drawLogRepository).should(times(1)).save(argThat(log ->
            log.getApplyStoreVisitBonus().equals(true)
        ));
    }

    @Test
    @DisplayName("당첨자 목록 조회 시 순위 순으로 정렬되어 반환된다")
    void givenWinnersExist_whenGetWinners_thenReturnSortedByRank() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        List<Participant> winners = createWinnerList(3);
        Page<Participant> winnerPage = new PageImpl<>(winners, pageable, 3);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(true);
        given(participantRepository.findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(VALID_EVENT_ID, pageable))
                .willReturn(winnerPage);

        // When
        PageResponse<ParticipationResponse> response = winnerDrawService.getWinners(VALID_EVENT_ID, pageable);

        // Then
        assertThat(response.getContent()).hasSize(3);
        assertThat(response.getTotalElements()).isEqualTo(3);
    }

    @Test
    @DisplayName("추첨이 완료되지 않은 이벤트의 당첨자 조회 시 NoWinnersYetException이 발생한다")
    void givenNoDrawYet_whenGetWinners_thenThrowException() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(false);

        // When & Then
        assertThatThrownBy(() -> winnerDrawService.getWinners(VALID_EVENT_ID, pageable))
                .isInstanceOf(NoWinnersYetException.class);

        then(participantRepository).should(never())
                .findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(anyString(), any(Pageable.class));
    }

    @Test
    @DisplayName("당첨자 추첨 시 모든 참여자에게 순위가 할당된다")
    void givenParticipants_whenDrawWinners_thenAllWinnersHaveRank() {
        // Given
        DrawWinnersRequest request = createDrawRequest(3, false);
        List<Participant> participants = createParticipantList(5);

        given(drawLogRepository.existsByEventId(VALID_EVENT_ID)).willReturn(false);
        given(participantRepository.findByEventIdAndIsWinnerFalse(VALID_EVENT_ID))
                .willReturn(participants);
        given(participantRepository.saveAll(anyList())).willAnswer(invocation -> invocation.getArgument(0));
        given(drawLogRepository.save(any(DrawLog.class))).willAnswer(invocation -> invocation.getArgument(0));

        // When
        DrawWinnersResponse response = winnerDrawService.drawWinners(VALID_EVENT_ID, request);

        // Then
        assertThat(response.getWinners()).allSatisfy(winner -> {
            assertThat(winner.getRank()).isNotNull();
            assertThat(winner.getRank()).isBetween(1, 3);
        });
    }

    // 헬퍼 메서드
    private DrawWinnersRequest createDrawRequest(Integer winnerCount, Boolean applyBonus) {
        return DrawWinnersRequest.builder()
                .winnerCount(winnerCount)
                .applyStoreVisitBonus(applyBonus)
                .build();
    }

    private List<Participant> createParticipantList(int count) {
        List<Participant> participants = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            participants.add(Participant.builder()
                    .participantId("prt_20250124_" + String.format("%03d", i))
                    .eventId(VALID_EVENT_ID)
                    .name("참여자" + i)
                    .phoneNumber("010-" + String.format("%04d", 1000 + i) + "-" + String.format("%04d", i))
                    .email("participant" + i + "@test.com")
                    .storeVisited(i % 2 == 0)
                    .bonusEntries(i % 2 == 0 ? 2 : 1)
                    .agreeMarketing(true)
                    .agreePrivacy(true)
                    .isWinner(false)
                    .build());
        }
        return participants;
    }

    private List<Participant> createWinnerList(int count) {
        List<Participant> winners = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            Participant winner = Participant.builder()
                    .participantId("prt_20250124_" + String.format("%03d", i))
                    .eventId(VALID_EVENT_ID)
                    .name("당첨자" + i)
                    .phoneNumber("010-" + String.format("%04d", 1000 + i) + "-" + String.format("%04d", i))
                    .email("winner" + i + "@test.com")
                    .storeVisited(true)
                    .bonusEntries(2)
                    .agreeMarketing(true)
                    .agreePrivacy(true)
                    .isWinner(true)
                    .build();
            winner.markAsWinner(i);
            winners.add(winner);
        }
        return winners;
    }
}
