package com.magistr.app.service;

import com.magistr.app.model.*;
import com.magistr.app.repository.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.HashSet;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

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

@DisplayName("Число запросов генератора расписания")
class ScheduleGenerationQueryCountTest {

    private static final LocalDate START_DATE = LocalDate.of(2026, 9, 1);
    private static final LocalDate END_DATE = LocalDate.of(2026, 12, 29);
    private static final int GROUP_COUNT = 30;
    private static final int RULE_COUNT = 20;

    @Test
    @DisplayName("120 дат, десятки групп и правил обрабатываются семью batch-запросами")
    void queryCountDoesNotGrowAsDatesTimesGroupsTimesRules() {
        SemesterRepository semesterRepository = mock(SemesterRepository.class);
        StudentGroupCalendarAssignmentRepository assignmentRepository =
                mock(StudentGroupCalendarAssignmentRepository.class);
        AcademicCalendarDayRepository calendarDayRepository = mock(AcademicCalendarDayRepository.class);
        ScheduleRuleRepository ruleRepository = mock(ScheduleRuleRepository.class);
        TimeSlotDateAssignmentRepository dateAssignmentRepository = mock(TimeSlotDateAssignmentRepository.class);
        TimeSlotScopeRepository scopeRepository = mock(TimeSlotScopeRepository.class);
        TimeSlotRepository timeSlotRepository = mock(TimeSlotRepository.class);
        GroupRepository groupRepository = mock(GroupRepository.class);
        UserRepository userRepository = mock(UserRepository.class);

        AcademicYear academicYear = academicYear();
        Semester semester = semester(academicYear);
        AcademicCalendar calendar = calendar(academicYear);
        List<StudentGroup> groups = groups();
        List<StudentGroupCalendarAssignment> assignments = groups.stream()
                .map(group -> assignment(group, academicYear, calendar))
                .toList();
        List<AcademicCalendarDay> calendarDays = START_DATE.datesUntil(END_DATE.plusDays(1))
                .map(date -> calendarDay(calendar, date))
                .toList();
        List<ScheduleRule> rules = IntStream.rangeClosed(1, RULE_COUNT)
                .mapToObj(index -> rule(index, semester, groups))
                .toList();
        TimeSlotScope defaultScope = timeSlotScope(40L, "DEFAULT", null);
        TimeSlotScope weekdayScope = timeSlotScope(41L, "WEEKDAY", 2);
        TimeSlot baseSlot = timeSlot(50L, defaultScope, LocalTime.of(8, 0), LocalTime.of(9, 30));
        TimeSlot effectiveSlot = timeSlot(51L, weekdayScope, LocalTime.of(8, 15), LocalTime.of(9, 45));
        addRenderedLecture(rules.get(0), baseSlot);

        when(semesterRepository.findOverlappingForSchedule(START_DATE, END_DATE))
                .thenReturn(List.of(semester));
        when(assignmentRepository.findForScheduleBatch(anyCollection(), anyCollection()))
                .thenReturn(assignments);
        when(calendarDayRepository.findForScheduleBatch(anyCollection(), eq(START_DATE), eq(END_DATE)))
                .thenReturn(calendarDays);
        when(ruleRepository.findByGroupIdsAndSemesterIds(anyCollection(), anyCollection()))
                .thenReturn(rules);
        when(dateAssignmentRepository.findForScheduleRange(START_DATE, END_DATE)).thenReturn(List.of());
        when(scopeRepository.findByApplyModeOrderByDisplayOrderAsc("WEEKDAY"))
                .thenReturn(List.of(weekdayScope));
        when(timeSlotRepository.findAllForSchedule()).thenReturn(List.of(baseSlot, effectiveSlot));

        AcademicDateService academicDateService = new AcademicDateService(
                semesterRepository,
                calendarDayRepository,
                assignmentRepository,
                false
        );
        ScheduleGeneratorService generator = new ScheduleGeneratorService(
                ruleRepository,
                groupRepository,
                userRepository,
                academicDateService,
                timeSlotRepository,
                scopeRepository,
                dateAssignmentRepository
        );

        var result = generator.buildScheduleForGroups(groups, START_DATE, END_DATE);

        assertThat(START_DATE.datesUntil(END_DATE.plusDays(1)).count()).isEqualTo(120);
        assertThat(groups).hasSize(GROUP_COUNT);
        assertThat(rules).hasSize(RULE_COUNT);
        assertThat(result).hasSize(GROUP_COUNT).allSatisfy(lesson -> {
            assertThat(lesson.date()).isEqualTo(START_DATE);
            assertThat(lesson.timeSlotId()).isEqualTo(effectiveSlot.getId());
            assertThat(lesson.startTime()).isEqualTo(effectiveSlot.getStartTime());
            assertThat(lesson.remainingLessonTypeAcademicHoursAfterLesson()).isZero();
            assertThat(lesson.groupIds()).hasSize(1);
        });

        verify(semesterRepository).findOverlappingForSchedule(START_DATE, END_DATE);
        verify(assignmentRepository).findForScheduleBatch(anyCollection(), anyCollection());
        verify(calendarDayRepository).findForScheduleBatch(anyCollection(), eq(START_DATE), eq(END_DATE));
        verify(ruleRepository).findByGroupIdsAndSemesterIds(anyCollection(), anyCollection());
        verify(dateAssignmentRepository).findForScheduleRange(START_DATE, END_DATE);
        verify(scopeRepository).findByApplyModeOrderByDisplayOrderAsc("WEEKDAY");
        verify(timeSlotRepository).findAllForSchedule();

        verify(semesterRepository, never())
                .findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(any(), any());
        verify(assignmentRepository, never()).findForSchedule(any(), any());
        verify(calendarDayRepository, never())
                .findByCalendarIdAndCourseNumberAndDate(any(), any(), any());
        verify(ruleRepository, never()).findByGroupIdAndSemesterId(any(), any());
        verify(dateAssignmentRepository, never()).findByDate(any());
        verify(scopeRepository, never()).findByApplyModeAndDayOfWeek(any(), any());
        verify(timeSlotRepository, never()).findByTimeSlotScopeIdAndOrderNumber(any(), any());

        int repositoryCalls = Stream.of(
                        semesterRepository,
                        assignmentRepository,
                        calendarDayRepository,
                        ruleRepository,
                        dateAssignmentRepository,
                        scopeRepository,
                        timeSlotRepository,
                        groupRepository,
                        userRepository
                )
                .mapToInt(repository -> mockingDetails(repository).getInvocations().size())
                .sum();
        assertThat(repositoryCalls).isEqualTo(7);
    }

    @Test
    @DisplayName("Генератор отклоняет 121 включительную дату")
    void rejects121InclusiveDates() {
        ScheduleGeneratorService generator = new ScheduleGeneratorService(
                mock(ScheduleRuleRepository.class),
                mock(GroupRepository.class),
                mock(UserRepository.class),
                mock(AcademicDateService.class),
                mock(TimeSlotRepository.class),
                mock(TimeSlotScopeRepository.class),
                mock(TimeSlotDateAssignmentRepository.class)
        );
        LocalDate startDate = LocalDate.of(2026, 1, 1);

        assertThatThrownBy(() -> generator.buildScheduleForGroups(
                List.of(),
                startDate,
                startDate.plusDays(120)
        ))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Диапазон расписания не может превышать 120 дней");
    }

    private AcademicYear academicYear() {
        AcademicYear academicYear = new AcademicYear();
        academicYear.setId(1L);
        academicYear.setTitle("2026/2027");
        academicYear.setStartDate(START_DATE);
        academicYear.setEndDate(LocalDate.of(2027, 8, 31));
        return academicYear;
    }

    private Semester semester(AcademicYear academicYear) {
        Semester semester = new Semester();
        semester.setId(10L);
        semester.setAcademicYear(academicYear);
        semester.setSemesterType(SemesterType.autumn);
        semester.setStartDate(START_DATE);
        semester.setEndDate(END_DATE);
        return semester;
    }

    private AcademicCalendar calendar(AcademicYear academicYear) {
        AcademicCalendar calendar = new AcademicCalendar();
        calendar.setId(20L);
        calendar.setTitle("Основной учебный график");
        calendar.setAcademicYear(academicYear);
        calendar.setCourseCount(4);
        return calendar;
    }

    private List<StudentGroup> groups() {
        return IntStream.rangeClosed(1, GROUP_COUNT)
                .mapToObj(index -> {
                    StudentGroup group = new StudentGroup();
                    group.setId((long) index);
                    group.setName("Группа " + index);
                    group.setYearStartStudy(2026);
                    return group;
                })
                .toList();
    }

    private StudentGroupCalendarAssignment assignment(StudentGroup group,
                                                      AcademicYear academicYear,
                                                      AcademicCalendar calendar) {
        StudentGroupCalendarAssignment assignment = new StudentGroupCalendarAssignment();
        assignment.setId(100L + group.getId());
        assignment.setStudentGroup(group);
        assignment.setAcademicYear(academicYear);
        assignment.setAcademicCalendar(calendar);
        return assignment;
    }

    private AcademicCalendarDay calendarDay(AcademicCalendar calendar, LocalDate date) {
        AcademicCalendarActivityType theory = new AcademicCalendarActivityType();
        theory.setId(30L);
        theory.setCode("Т");
        theory.setName("Теоретическое обучение");
        theory.setAllowSchedule(true);

        AcademicCalendarDay day = new AcademicCalendarDay();
        day.setAcademicCalendar(calendar);
        day.setCourseNumber(1);
        day.setDate(date);
        day.setActivityType(theory);
        return day;
    }

    private ScheduleRule rule(int index, Semester semester, List<StudentGroup> groups) {
        Subject subject = new Subject((long) index, "Дисциплина " + index, "D" + index, 1L);
        ScheduleRule rule = new ScheduleRule();
        rule.setId((long) index);
        rule.setSubject(subject);
        rule.setSemester(semester);
        rule.setGroups(new HashSet<>(groups));
        return rule;
    }

    private TimeSlotScope timeSlotScope(Long id, String applyMode, Integer dayOfWeek) {
        TimeSlotScope scope = new TimeSlotScope();
        scope.setId(id);
        scope.setCode(applyMode + "-" + id);
        scope.setName("Сетка " + id);
        scope.setApplyMode(applyMode);
        scope.setDayOfWeek(dayOfWeek);
        scope.setDisplayOrder(id.intValue());
        return scope;
    }

    private TimeSlot timeSlot(Long id, TimeSlotScope scope, LocalTime startTime, LocalTime endTime) {
        TimeSlot slot = new TimeSlot();
        slot.setId(id);
        slot.setOrderNumber(1);
        slot.setTimeSlotScope(scope);
        slot.setStartTime(startTime);
        slot.setEndTime(endTime);
        slot.setDurationMinutes(90);
        return slot;
    }

    private void addRenderedLecture(ScheduleRule rule, TimeSlot timeSlot) {
        User teacher = new User();
        teacher.setId(60L);
        teacher.setUsername("teacher");
        teacher.setFullName("Тестовый преподаватель");
        teacher.setRole(Role.TEACHER);

        Classroom classroom = new Classroom();
        classroom.setId(70L);
        classroom.setName("А-101");
        classroom.setIsAvailable(true);

        LessonType lessonType = new LessonType();
        lessonType.setId(80L);
        lessonType.setLessonType("Лекция");

        ScheduleRuleSlot slot = new ScheduleRuleSlot();
        slot.setId(90L);
        slot.setScheduleRule(rule);
        slot.setDayOfWeek(2);
        slot.setParity(ScheduleParity.BOTH);
        slot.setTimeSlot(timeSlot);
        slot.setTeacher(teacher);
        slot.setClassroom(classroom);
        slot.setLessonType(lessonType);
        slot.setLessonFormat("Очно");

        rule.setLectureAcademicHours(2);
        rule.setSlots(new HashSet<>(List.of(slot)));
    }
}
