package com.mosquito.project.job;

import com.mosquito.project.domain.Activity;
import com.mosquito.project.domain.DailyActivityStats;
import com.mosquito.project.persistence.entity.DailyActivityStatsEntity;
import com.mosquito.project.persistence.repository.DailyActivityStatsRepository;
import com.mosquito.project.persistence.repository.LinkClickRepository;
import com.mosquito.project.persistence.repository.UserInviteRepository;
import com.mosquito.project.persistence.repository.UserRewardRepository;
import com.mosquito.project.service.ActivityService;
import org.junit.jupiter.api.BeforeEach;
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 org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class StatisticsAggregationJobCompleteTest {

    @Mock
    private ActivityService activityService;

    @Mock
    private DailyActivityStatsRepository dailyStatsRepository;

    @Mock
    private LinkClickRepository linkClickRepository;

    @Mock
    private UserInviteRepository userInviteRepository;

    @Mock
    private UserRewardRepository userRewardRepository;

    @InjectMocks
    private StatisticsAggregationJob job;

    private LocalDate testDate;

    @BeforeEach
    void setUp() {
        testDate = LocalDate.of(2024, 6, 15);
        // Setup default mock behavior
        when(linkClickRepository.countByActivityId(any())).thenReturn(0L);
        when(linkClickRepository.countUniqueVisitorsByActivityIdAndDateRange(any(), any(), any())).thenReturn(0L);
        when(userInviteRepository.findByActivityId(any())).thenReturn(Collections.emptyList());
        when(userRewardRepository.findByActivityId(any())).thenReturn(Collections.emptyList());
    }

    @Test
    void shouldAggregateDailyStats_whenActivitiesExist() {
        Activity activity1 = createActivity(1L, "Activity 1");
        Activity activity2 = createActivity(2L, "Activity 2");
        List<Activity> activities = List.of(activity1, activity2);

        when(activityService.getAllActivities()).thenReturn(activities);
        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        job.aggregateDailyStats();

        verify(activityService, times(1)).getAllActivities();
        verify(dailyStatsRepository, times(4)).save(any(DailyActivityStatsEntity.class));
    }

    @Test
    void shouldHandleEmptyActivityList_whenNoActivities() {
        when(activityService.getAllActivities()).thenReturn(Collections.emptyList());

        job.aggregateDailyStats();

        verify(activityService, times(1)).getAllActivities();
        verify(dailyStatsRepository, never()).save(any());
    }

    @Test
    void shouldCreateStatsInValidRange_whenAggregateStatsForActivityCalled() {
        Activity activity = createActivity(1L, "Test Activity");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

        assertThat(stats).isNotNull();
        assertThat(stats.getActivityId()).isEqualTo(1L);
        assertThat(stats.getStatDate()).isEqualTo(testDate);
        assertThat(stats.getViews()).isEqualTo(0);  // from real aggregation with mock
        assertThat(stats.getShares()).isEqualTo(0);
        assertThat(stats.getNewRegistrations()).isEqualTo(0);
        assertThat(stats.getConversions()).isEqualTo(0);
    }

    @Test
    void shouldSetCorrectActivityId_whenDifferentActivitiesProcessed() {
        Activity activity1 = createActivity(100L, "Activity 100");
        Activity activity2 = createActivity(200L, "Activity 200");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats1 = job.aggregateStatsForActivity(activity1, testDate);
        DailyActivityStats stats2 = job.aggregateStatsForActivity(activity2, testDate);

        assertThat(stats1.getActivityId()).isEqualTo(100L);
        assertThat(stats2.getActivityId()).isEqualTo(200L);
    }

    @Test
    void shouldSetCorrectDate_whenDifferentDatesProcessed() {
        Activity activity = createActivity(1L, "Test");
        LocalDate date1 = LocalDate.of(2024, 1, 1);
        LocalDate date2 = LocalDate.of(2024, 12, 31);

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats1 = job.aggregateStatsForActivity(activity, date1);
        DailyActivityStats stats2 = job.aggregateStatsForActivity(activity, date2);

        assertThat(stats1.getStatDate()).isEqualTo(date1);
        assertThat(stats2.getStatDate()).isEqualTo(date2);
    }

    @Test
    void shouldUpdateExistingEntity_whenStatsAlreadyExist() {
        Activity activity = createActivity(1L, "Test");
        DailyActivityStatsEntity existingEntity = new DailyActivityStatsEntity();
        existingEntity.setId(100L);
        existingEntity.setActivityId(1L);
        existingEntity.setStatDate(testDate);
        existingEntity.setViews(500);
        existingEntity.setShares(100);
        existingEntity.setNewRegistrations(30);
        existingEntity.setConversions(5);

        when(dailyStatsRepository.findByActivityIdAndStatDate(1L, testDate))
            .thenReturn(Optional.of(existingEntity));
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        job.aggregateStatsForActivity(activity, testDate);

        ArgumentCaptor<DailyActivityStatsEntity> captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class);
        verify(dailyStatsRepository, atLeastOnce()).save(captor.capture());

        DailyActivityStatsEntity savedEntity = captor.getValue();
        assertThat(savedEntity.getId()).isEqualTo(100L);
        assertThat(savedEntity.getViews()).isEqualTo(0);  // from real aggregation with mock
    }

    @Test
    void shouldCreateNewEntity_whenStatsDoNotExist() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(1L, testDate))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        job.aggregateStatsForActivity(activity, testDate);

        ArgumentCaptor<DailyActivityStatsEntity> captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class);
        verify(dailyStatsRepository, atLeastOnce()).save(captor.capture());

        DailyActivityStatsEntity savedEntity = captor.getValue();
        assertThat(savedEntity.getId()).isNull();
        assertThat(savedEntity.getActivityId()).isEqualTo(1L);
    }

    @Test
    void shouldHandleSingleActivity_whenOnlyOneActivityExists() {
        Activity activity = createActivity(1L, "Solo Activity");

        when(activityService.getAllActivities()).thenReturn(List.of(activity));
        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        job.aggregateDailyStats();

        verify(dailyStatsRepository, times(2)).save(any(DailyActivityStatsEntity.class));
    }

    @Test
    void shouldHandleManyActivities_whenLargeActivityList() {
        List<Activity> activities = new ArrayList<>();
        for (long i = 1; i <= 100; i++) {
            activities.add(createActivity(i, "Activity " + i));
        }

        when(activityService.getAllActivities()).thenReturn(activities);
        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        job.aggregateDailyStats();

        verify(activityService, times(1)).getAllActivities();
        verify(dailyStatsRepository, times(200)).save(any(DailyActivityStatsEntity.class));
    }

    @Test
    void shouldGenerateNonNegativeStats_whenRandomValuesGenerated() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        for (int i = 0; i < 50; i++) {
            DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

            // Now uses real aggregation, values are 0 from mocks
            assertThat(stats.getViews()).isGreaterThanOrEqualTo(0);
            assertThat(stats.getShares()).isGreaterThanOrEqualTo(0);
            assertThat(stats.getNewRegistrations()).isGreaterThanOrEqualTo(0);
            assertThat(stats.getConversions()).isGreaterThanOrEqualTo(0);
        }
    }

    @Test
    void shouldStoreStatsInConcurrentMap_whenAggregated() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

        assertThat(stats).isNotNull();
    }

    @Test
    void shouldCallUpsertDailyStats_whenAggregateStatsForActivity() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

        assertThat(stats.getActivityId()).isEqualTo(1L);
        verify(dailyStatsRepository, atLeastOnce()).save(any(DailyActivityStatsEntity.class));
    }

    @Test
    void shouldUseYesterdayDate_whenAggregateDailyStatsCalled() {
        when(activityService.getAllActivities()).thenReturn(Collections.emptyList());

        job.aggregateDailyStats();

        LocalDate yesterday = LocalDate.now().minusDays(1);
        verify(activityService, times(1)).getAllActivities();
    }

    @Test
    void shouldHandleActivityWithNullName_whenAggregated() {
        Activity activity = new Activity();
        activity.setId(1L);
        activity.setName(null);
        activity.setStartTime(ZonedDateTime.now());
        activity.setEndTime(ZonedDateTime.now().plusDays(1));

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

        assertThat(stats.getActivityId()).isEqualTo(1L);
        assertThat(stats.getStatDate()).isEqualTo(testDate);
    }

    @Test
    void shouldPreserveAllStatFields_whenSavingToRepository() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());

        job.aggregateStatsForActivity(activity, testDate);

        ArgumentCaptor<DailyActivityStatsEntity> captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class);
        verify(dailyStatsRepository, atLeastOnce()).save(captor.capture());

        DailyActivityStatsEntity saved = captor.getValue();
        assertThat(saved.getActivityId()).isNotNull();
        assertThat(saved.getStatDate()).isNotNull();
        assertThat(saved.getViews()).isNotNull();
        assertThat(saved.getShares()).isNotNull();
        assertThat(saved.getNewRegistrations()).isNotNull();
        assertThat(saved.getConversions()).isNotNull();
    }

    @Test
    void shouldHandleActivityWithZeroId_whenAggregated() {
        Activity activity = createActivity(0L, "Zero ID Activity");

        when(dailyStatsRepository.findByActivityIdAndStatDate(0L, testDate))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate);

        assertThat(stats.getActivityId()).isEqualTo(0L);
    }

    @Test
    void shouldGenerateStatsWithinExpectedRanges_whenMultipleCalls() {
        Activity activity = createActivity(1L, "Test");

        when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any()))
            .thenReturn(Optional.empty());
        when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        List<DailyActivityStats> allStats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            allStats.add(job.aggregateStatsForActivity(activity, testDate));
        }

        // Now uses real aggregation, all values are 0 from mocks
        assertThat(allStats)
            .allMatch(s -> s.getViews() >= 0)
            .allMatch(s -> s.getShares() >= 0)
            .allMatch(s -> s.getNewRegistrations() >= 0)
            .allMatch(s -> s.getConversions() >= 0);
    }

    private Activity createActivity(Long id, String name) {
        Activity activity = new Activity();
        activity.setId(id);
        activity.setName(name);
        activity.setStartTime(ZonedDateTime.now());
        activity.setEndTime(ZonedDateTime.now().plusDays(1));
        return activity;
    }
}
