package com.magistr.app.service;

import com.magistr.app.dto.AcademicCalendarGridDayDto;
import com.magistr.app.model.AcademicCalendar;
import com.magistr.app.model.AcademicCalendarActivityType;
import com.magistr.app.model.AcademicCalendarDay;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.repository.AcademicCalendarActivityTypeRepository;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import com.magistr.app.repository.AcademicCalendarRepository;
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.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;

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.Mockito.inOrder;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AcademicCalendarGridServiceTest {

    private static final long CALENDAR_ID = 10L;
    private static final long ACTIVITY_TYPE_ID = 20L;
    private static final LocalDate YEAR_START = LocalDate.of(2025, 9, 1);

    @Mock
    private AcademicCalendarRepository calendarRepository;
    @Mock
    private AcademicCalendarDayRepository calendarDayRepository;
    @Mock
    private AcademicCalendarActivityTypeRepository activityTypeRepository;
    @Mock
    private ScheduleGeneratorService scheduleGeneratorService;

    private AcademicCalendarGridService service;
    private AcademicCalendarActivityType activityType;
    private AcademicYear academicYear;

    @BeforeEach
    void setUp() {
        academicYear = new AcademicYear();
        academicYear.setId(1L);
        academicYear.setTitle("2025-2026");
        academicYear.setStartDate(YEAR_START);
        academicYear.setEndDate(LocalDate.of(2026, 6, 30));

        AcademicCalendar calendar = new AcademicCalendar();
        calendar.setId(CALENDAR_ID);
        calendar.setTitle("Тестовый календарный график");
        calendar.setAcademicYear(academicYear);
        calendar.setCourseCount(4);

        activityType = new AcademicCalendarActivityType();
        activityType.setId(ACTIVITY_TYPE_ID);
        activityType.setCode("Т");
        activityType.setName("Теоретическое обучение");
        activityType.setAllowSchedule(true);

        service = new AcademicCalendarGridService(
                calendarRepository,
                calendarDayRepository,
                activityTypeRepository,
                scheduleGeneratorService
        );

        when(calendarRepository.findByIdWithDetails(CALENDAR_ID)).thenReturn(Optional.of(calendar));
        lenient().when(activityTypeRepository.findById(ACTIVITY_TYPE_ID)).thenReturn(Optional.of(activityType));
    }

    @Test
    void rejectsDuplicateCourseAndDateBeforeMutation() {
        AcademicCalendarGridDayDto first = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto duplicate = validRow(YEAR_START, 1, 1);

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, List.of(first, duplicate)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Для курса и даты можно указать только одну активность");

        verifyNoMutation();
    }

    @Test
    void rejectsNullSecondRowBeforeMutation() {
        List<AcademicCalendarGridDayDto> rows = Arrays.asList(
                validRow(YEAR_START, 1, 1),
                null
        );

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, rows))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Строка календарного графика не может быть пустой");

        verifyNoMutation();
    }

    @Test
    void rejectsDayOfWeekThatDoesNotMatchDateBeforeMutation() {
        AcademicCalendarGridDayDto validFirst = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto invalidSecond = validRow(YEAR_START.plusDays(1), 1, 7);

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, List.of(validFirst, invalidSecond)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("День недели не соответствует дате");

        verifyNoMutation();
    }

    @Test
    void rejectsWeekThatDoesNotMatchDateBeforeMutation() {
        AcademicCalendarGridDayDto validFirst = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto invalidSecond = validRow(YEAR_START.plusDays(7), 1, 1);

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, List.of(validFirst, invalidSecond)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Номер недели не соответствует дате учебного года");

        verifyNoMutation();
    }

    @Test
    void rejectsDateOutsideAcademicYearBeforeMutation() {
        AcademicCalendarGridDayDto validFirst = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto outsideYear = validRow(LocalDate.of(2026, 7, 1), 44, 3);

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, List.of(validFirst, outsideYear)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Дата выходит за пределы учебного года");

        verifyNoMutation();
    }

    @Test
    void rejectsUnknownActivityInLastRowBeforeMutation() {
        AcademicCalendarGridDayDto validFirst = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto unknownActivity = row(
                YEAR_START.plusDays(1), 1, 2, 999L
        );

        assertThatThrownBy(() -> service.replaceGrid(CALENDAR_ID, List.of(validFirst, unknownActivity)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Код активности не найден");

        verifyNoMutation();
    }

    @Test
    void reportsMissingCalendarWithoutMutation() {
        when(calendarRepository.findByIdWithDetails(CALENDAR_ID)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.replaceGrid(
                CALENDAR_ID,
                List.of(validRow(YEAR_START, 1, 1))
        ))
                .isInstanceOf(NoSuchElementException.class)
                .hasMessage("Календарный график не найден");

        verifyNoMutation();
    }

    @Test
    void replacesOnlyAfterWholePayloadIsValid() {
        AcademicCalendarGridDayDto first = validRow(YEAR_START, 1, 1);
        AcademicCalendarGridDayDto second = validRow(YEAR_START.plusDays(1), 1, 2);

        service.replaceGrid(CALENDAR_ID, List.of(first, second));

        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<AcademicCalendarDay>> replacementCaptor = ArgumentCaptor.forClass(List.class);
        InOrder mutationOrder = inOrder(calendarDayRepository, scheduleGeneratorService);
        mutationOrder.verify(calendarDayRepository).deleteByCalendarId(CALENDAR_ID);
        mutationOrder.verify(calendarDayRepository).flush();
        mutationOrder.verify(calendarDayRepository).saveAll(replacementCaptor.capture());
        mutationOrder.verify(calendarDayRepository).flush();
        mutationOrder.verify(scheduleGeneratorService).clearCache();

        assertThat(replacementCaptor.getValue())
                .extracting(AcademicCalendarDay::getDate)
                .containsExactly(YEAR_START, YEAR_START.plusDays(1));
        assertThat(replacementCaptor.getValue())
                .allSatisfy(day -> {
                    assertThat(day.getAcademicCalendar().getId()).isEqualTo(CALENDAR_ID);
                    assertThat(day.getActivityType()).isSameAs(activityType);
                });
    }

    @Test
    void alignsFirstWeekToMondayWhenAcademicYearStartsOnTuesday() {
        LocalDate yearStart = LocalDate.of(2026, 9, 1);
        academicYear.setStartDate(yearStart);
        academicYear.setEndDate(LocalDate.of(2027, 6, 30));

        service.replaceGrid(CALENDAR_ID, List.of(
                validRow(yearStart, 1, 2),
                validRow(LocalDate.of(2026, 9, 6), 1, 7),
                validRow(LocalDate.of(2026, 9, 7), 2, 1),
                validRow(LocalDate.of(2026, 9, 14), 3, 1)
        ));

        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<AcademicCalendarDay>> replacementCaptor = ArgumentCaptor.forClass(List.class);
        verify(calendarDayRepository).saveAll(replacementCaptor.capture());
        assertThat(replacementCaptor.getValue())
                .extracting(AcademicCalendarDay::getWeekNumber)
                .containsExactly(1, 1, 2, 3);
    }

    @Test
    void alignsFirstWeekToMondayWhenAcademicYearStartsOnWednesday() {
        LocalDate yearStart = LocalDate.of(2027, 9, 1);
        academicYear.setStartDate(yearStart);
        academicYear.setEndDate(LocalDate.of(2028, 6, 30));

        service.replaceGrid(CALENDAR_ID, List.of(
                validRow(yearStart, 1, 3),
                validRow(LocalDate.of(2027, 9, 5), 1, 7),
                validRow(LocalDate.of(2027, 9, 6), 2, 1),
                validRow(LocalDate.of(2027, 9, 13), 3, 1)
        ));

        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<AcademicCalendarDay>> replacementCaptor = ArgumentCaptor.forClass(List.class);
        verify(calendarDayRepository).saveAll(replacementCaptor.capture());
        assertThat(replacementCaptor.getValue())
                .extracting(AcademicCalendarDay::getWeekNumber)
                .containsExactly(1, 1, 2, 3);
    }

    private AcademicCalendarGridDayDto validRow(LocalDate date, int weekNumber, int dayOfWeek) {
        return row(date, weekNumber, dayOfWeek, ACTIVITY_TYPE_ID);
    }

    private AcademicCalendarGridDayDto row(LocalDate date,
                                           int weekNumber,
                                           int dayOfWeek,
                                           long activityTypeId) {
        return new AcademicCalendarGridDayDto(
                null,
                CALENDAR_ID,
                1,
                date,
                weekNumber,
                dayOfWeek,
                activityTypeId,
                null,
                null,
                null
        );
    }

    private void verifyNoMutation() {
        verify(calendarDayRepository, never()).deleteByCalendarId(any());
        verify(calendarDayRepository, never()).flush();
        verify(calendarDayRepository, never()).save(any());
        verify(calendarDayRepository, never()).saveAll(any());
        verify(scheduleGeneratorService, never()).clearCache();
    }
}
