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

import com.kt.event.common.dto.PageResponse;
import com.kt.event.participation.application.dto.ParticipationRequest;
import com.kt.event.participation.application.dto.ParticipationResponse;
import com.kt.event.participation.application.service.ParticipationService;
import com.kt.event.participation.domain.participant.Participant;
import com.kt.event.participation.domain.participant.ParticipantRepository;
import com.kt.event.participation.exception.ParticipationException.*;
import com.kt.event.participation.infrastructure.kafka.KafkaProducerService;
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.List;
import java.util.Optional;

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

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

    @Mock
    private ParticipantRepository participantRepository;

    @Mock
    private KafkaProducerService kafkaProducerService;

    @InjectMocks
    private ParticipationService participationService;

    // 테스트 데이터 상수
    private static final String VALID_EVENT_ID = "evt_20250124_001";
    private static final String VALID_PARTICIPANT_ID = "prt_20250124_001";
    private static final String VALID_NAME = "홍길동";
    private static final String VALID_PHONE = "010-1234-5678";
    private static final String VALID_EMAIL = "hong@test.com";

    @Test
    @DisplayName("정상적인 참여 요청이면 참여자가 저장되고 Kafka 이벤트가 발행된다")
    void givenValidRequest_whenParticipate_thenSaveAndPublishEvent() {
        // Given
        ParticipationRequest request = createValidRequest();
        Participant savedParticipant = createValidParticipant();

        given(participantRepository.existsByEventIdAndPhoneNumber(VALID_EVENT_ID, VALID_PHONE))
                .willReturn(false);
        given(participantRepository.findMaxIdByEventId(VALID_EVENT_ID))
                .willReturn(Optional.of(0L));
        given(participantRepository.save(any(Participant.class)))
                .willReturn(savedParticipant);
        willDoNothing().given(kafkaProducerService)
                .publishParticipantRegistered(any());

        // When
        ParticipationResponse response = participationService.participate(VALID_EVENT_ID, request);

        // Then
        assertThat(response).isNotNull();
        assertThat(response.getParticipantId()).isEqualTo(VALID_PARTICIPANT_ID);
        assertThat(response.getName()).isEqualTo(VALID_NAME);
        assertThat(response.getPhoneNumber()).isEqualTo(VALID_PHONE);

        then(participantRepository).should(times(1)).save(any(Participant.class));
        then(kafkaProducerService).should(times(1)).publishParticipantRegistered(any());
    }

    @Test
    @DisplayName("중복 참여 시 DuplicateParticipationException이 발생한다")
    void givenDuplicatePhone_whenParticipate_thenThrowException() {
        // Given
        ParticipationRequest request = createValidRequest();

        given(participantRepository.existsByEventIdAndPhoneNumber(VALID_EVENT_ID, VALID_PHONE))
                .willReturn(true);

        // When & Then
        assertThatThrownBy(() -> participationService.participate(VALID_EVENT_ID, request))
                .isInstanceOf(DuplicateParticipationException.class)
                .hasMessageContaining("이미 참여하신 이벤트입니다");

        then(participantRepository).should(never()).save(any());
        then(kafkaProducerService).should(never()).publishParticipantRegistered(any());
    }

    @Test
    @DisplayName("매장 방문 참여자는 보너스 응모권이 2개가 된다")
    void givenStoreVisited_whenParticipate_thenBonusEntriesIsTwo() {
        // Given
        ParticipationRequest request = ParticipationRequest.builder()
                .name(VALID_NAME)
                .phoneNumber(VALID_PHONE)
                .email(VALID_EMAIL)
                .storeVisited(true)
                .agreeMarketing(true)
                .agreePrivacy(true)
                .build();

        Participant savedParticipant = Participant.builder()
                .participantId(VALID_PARTICIPANT_ID)
                .eventId(VALID_EVENT_ID)
                .name(VALID_NAME)
                .phoneNumber(VALID_PHONE)
                .email(VALID_EMAIL)
                .storeVisited(true)
                .bonusEntries(2)
                .agreeMarketing(true)
                .agreePrivacy(true)
                .isWinner(false)
                .build();

        given(participantRepository.existsByEventIdAndPhoneNumber(VALID_EVENT_ID, VALID_PHONE))
                .willReturn(false);
        given(participantRepository.findMaxIdByEventId(VALID_EVENT_ID))
                .willReturn(Optional.of(0L));
        given(participantRepository.save(any(Participant.class)))
                .willReturn(savedParticipant);

        // When
        ParticipationResponse response = participationService.participate(VALID_EVENT_ID, request);

        // Then
        assertThat(response.getBonusEntries()).isEqualTo(2);
        assertThat(response.getStoreVisited()).isTrue();
    }

    @Test
    @DisplayName("참여자 목록 조회 시 페이징이 적용된다")
    void givenPageable_whenGetParticipants_thenReturnPagedList() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        List<Participant> participants = List.of(
                createValidParticipant(),
                createAnotherParticipant()
        );
        Page<Participant> participantPage = new PageImpl<>(participants, pageable, 2);

        given(participantRepository.findByEventIdOrderByCreatedAtDesc(VALID_EVENT_ID, pageable))
                .willReturn(participantPage);

        // When
        PageResponse<ParticipationResponse> response = participationService
                .getParticipants(VALID_EVENT_ID, null, pageable);

        // Then
        assertThat(response.getContent()).hasSize(2);
        assertThat(response.getTotalElements()).isEqualTo(2);
        assertThat(response.getTotalPages()).isEqualTo(1);
        assertThat(response.isFirst()).isTrue();
        assertThat(response.isLast()).isTrue();
    }

    @Test
    @DisplayName("매장 방문 필터 적용 시 필터링된 참여자 목록이 조회된다")
    void givenStoreVisitedFilter_whenGetParticipants_thenReturnFilteredList() {
        // Given
        Boolean storeVisited = true;
        Pageable pageable = PageRequest.of(0, 10);
        List<Participant> participants = List.of(createValidParticipant());
        Page<Participant> participantPage = new PageImpl<>(participants, pageable, 1);

        given(participantRepository.findByEventIdAndStoreVisitedOrderByCreatedAtDesc(
                VALID_EVENT_ID, storeVisited, pageable))
                .willReturn(participantPage);

        // When
        PageResponse<ParticipationResponse> response = participationService
                .getParticipants(VALID_EVENT_ID, storeVisited, pageable);

        // Then
        assertThat(response.getContent()).hasSize(1);
        assertThat(response.getContent().get(0).getStoreVisited()).isTrue();

        then(participantRepository).should(times(1))
                .findByEventIdAndStoreVisitedOrderByCreatedAtDesc(VALID_EVENT_ID, storeVisited, pageable);
    }

    @Test
    @DisplayName("참여자 상세 조회 시 정상적으로 반환된다")
    void givenValidParticipantId_whenGetParticipant_thenReturnParticipant() {
        // Given
        Participant participant = createValidParticipant();

        given(participantRepository.findByEventIdAndParticipantId(VALID_EVENT_ID, VALID_PARTICIPANT_ID))
                .willReturn(Optional.of(participant));

        // When
        ParticipationResponse response = participationService
                .getParticipant(VALID_EVENT_ID, VALID_PARTICIPANT_ID);

        // Then
        assertThat(response).isNotNull();
        assertThat(response.getParticipantId()).isEqualTo(VALID_PARTICIPANT_ID);
        assertThat(response.getName()).isEqualTo(VALID_NAME);
    }

    @Test
    @DisplayName("존재하지 않는 참여자 조회 시 ParticipantNotFoundException이 발생한다")
    void givenInvalidParticipantId_whenGetParticipant_thenThrowException() {
        // Given
        String invalidParticipantId = "prt_20250124_999";

        given(participantRepository.findByEventIdAndParticipantId(VALID_EVENT_ID, invalidParticipantId))
                .willReturn(Optional.empty());
        given(participantRepository.countByEventId(VALID_EVENT_ID))
                .willReturn(1L); // 이벤트에 다른 참여자가 있음을 나타냄

        // When & Then
        assertThatThrownBy(() -> participationService.getParticipant(VALID_EVENT_ID, invalidParticipantId))
                .isInstanceOf(ParticipantNotFoundException.class)
                .hasMessageContaining("참여자를 찾을 수 없습니다");
    }

    // 헬퍼 메서드
    private ParticipationRequest createValidRequest() {
        return ParticipationRequest.builder()
                .name(VALID_NAME)
                .phoneNumber(VALID_PHONE)
                .email(VALID_EMAIL)
                .storeVisited(true)
                .agreeMarketing(true)
                .agreePrivacy(true)
                .build();
    }

    private Participant createValidParticipant() {
        return Participant.builder()
                .participantId(VALID_PARTICIPANT_ID)
                .eventId(VALID_EVENT_ID)
                .name(VALID_NAME)
                .phoneNumber(VALID_PHONE)
                .email(VALID_EMAIL)
                .storeVisited(true)
                .bonusEntries(2)
                .agreeMarketing(true)
                .agreePrivacy(true)
                .isWinner(false)
                .build();
    }

    private Participant createAnotherParticipant() {
        return Participant.builder()
                .participantId("prt_20250124_002")
                .eventId(VALID_EVENT_ID)
                .name("김철수")
                .phoneNumber("010-9876-5432")
                .email("kim@test.com")
                .storeVisited(false)
                .bonusEntries(1)
                .agreeMarketing(false)
                .agreePrivacy(true)
                .isWinner(false)
                .build();
    }
}
