package com.magistr.app.service;

import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.model.Role;
import com.magistr.app.model.ScheduleOverride;
import com.magistr.app.model.ScheduleParity;
import com.magistr.app.model.ScheduleRuleSlot;
import com.magistr.app.model.StudentGroup;
import com.magistr.app.model.TimeSlot;
import com.magistr.app.model.User;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.ScheduleOverrideRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.LocalTime;
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.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@DisplayName("Поиск расписания преподавателя с точечными изменениями")
class ScheduleQueryServiceTeacherOverrideTest {

    private static final LocalDate DATE = LocalDate.of(2026, 9, 1);
    private static final long SOURCE_TEACHER_ID = 11L;
    private static final long REPLACEMENT_TEACHER_ID = 12L;
    private static final long GROUP_ID = 31L;

    private final ScheduleGeneratorService generatorService = mock(ScheduleGeneratorService.class);
    private final GroupRepository groupRepository = mock(GroupRepository.class);
    private final ScheduleOverrideRepository overrideRepository = mock(ScheduleOverrideRepository.class);
    private final StudentGroupLifecycleService groupLifecycleService = mock(StudentGroupLifecycleService.class);
    private final ScheduleQueryService service = new ScheduleQueryService(
            generatorService,
            groupRepository,
            overrideRepository,
            groupLifecycleService
    );

    @Test
    @DisplayName("Замена A на B видна преподавателю B и скрыта у преподавателя A")
    void replacementIsVisibleOnlyToEffectiveTeacher() {
        RenderedLessonDto sourceLesson = lesson(101L, SOURCE_TEACHER_ID, 1);
        ScheduleOverride replacement = replacement(101L, REPLACEMENT_TEACHER_ID);
        when(generatorService.buildScheduleForTeacher(REPLACEMENT_TEACHER_ID, DATE, DATE))
                .thenReturn(List.of());
        when(generatorService.buildScheduleForTeacher(SOURCE_TEACHER_ID, DATE, DATE))
                .thenReturn(List.of(sourceLesson));
        when(overrideRepository.findByLessonDateBetweenWithDetails(DATE, DATE))
                .thenReturn(List.of(replacement));
        stubBaseDay(List.of(sourceLesson));

        List<RenderedLessonDto> replacementSchedule = searchTeacher(REPLACEMENT_TEACHER_ID);
        List<RenderedLessonDto> sourceSchedule = searchTeacher(SOURCE_TEACHER_ID);

        assertThat(replacementSchedule).singleElement().satisfies(lesson -> {
            assertThat(lesson.scheduleRuleSlotId()).isEqualTo(101L);
            assertThat(lesson.teacherId()).isEqualTo(REPLACEMENT_TEACHER_ID);
        });
        assertThat(sourceSchedule).isEmpty();
        verify(overrideRepository, times(2)).findByLessonDateBetweenWithDetails(DATE, DATE);
        verify(groupRepository).findAll();
        verifyBaseDayBatch();
    }

    @Test
    @DisplayName("Точечное изменение без базового occurrence не создаёт фантомную пару")
    void missingBaseOccurrenceDoesNotCreatePhantomLesson() {
        ScheduleOverride replacement = replacement(101L, REPLACEMENT_TEACHER_ID);
        when(generatorService.buildScheduleForTeacher(REPLACEMENT_TEACHER_ID, DATE, DATE))
                .thenReturn(List.of());
        when(overrideRepository.findByLessonDateBetweenWithDetails(DATE, DATE))
                .thenReturn(List.of(replacement));
        stubBaseDay(List.of(lesson(102L, SOURCE_TEACHER_ID, 2)));

        List<RenderedLessonDto> result = searchTeacher(REPLACEMENT_TEACHER_ID);

        assertThat(result).isEmpty();
        verify(overrideRepository).findByLessonDateBetweenWithDetails(DATE, DATE);
        verify(groupRepository).findAll();
        verifyBaseDayBatch();
    }

    @Test
    @DisplayName("Несколько замен одной даты генерируют базовый день только один раз")
    void multipleReplacementsOnSameDateGenerateBaseDayOnce() {
        RenderedLessonDto first = lesson(101L, SOURCE_TEACHER_ID, 1);
        RenderedLessonDto second = lesson(102L, SOURCE_TEACHER_ID, 2);
        when(generatorService.buildScheduleForTeacher(REPLACEMENT_TEACHER_ID, DATE, DATE))
                .thenReturn(List.of());
        when(overrideRepository.findByLessonDateBetweenWithDetails(DATE, DATE))
                .thenReturn(List.of(
                        replacement(101L, REPLACEMENT_TEACHER_ID),
                        replacement(102L, REPLACEMENT_TEACHER_ID)
                ));
        stubBaseDay(List.of(first, second));

        List<RenderedLessonDto> result = searchTeacher(REPLACEMENT_TEACHER_ID);

        assertThat(result)
                .extracting(RenderedLessonDto::scheduleRuleSlotId)
                .containsExactly(101L, 102L);
        verify(overrideRepository).findByLessonDateBetweenWithDetails(DATE, DATE);
        verify(groupRepository).findAll();
        verifyBaseDayBatch();
    }

    @Test
    @DisplayName("Уже загруженная базовая пара не достраивается повторно")
    void duplicateSourceOccurrenceIsRemovedAfterApplyingSnapshot() {
        RenderedLessonDto existingLesson = lesson(101L, REPLACEMENT_TEACHER_ID, 1);
        when(generatorService.buildScheduleForTeacher(REPLACEMENT_TEACHER_ID, DATE, DATE))
                .thenReturn(List.of(existingLesson));
        when(overrideRepository.findByLessonDateBetweenWithDetails(DATE, DATE))
                .thenReturn(List.of(replacement(101L, REPLACEMENT_TEACHER_ID)));
        stubBaseDay(List.of(existingLesson));

        List<RenderedLessonDto> result = searchTeacher(REPLACEMENT_TEACHER_ID);

        assertThat(result).singleElement().satisfies(lesson ->
                assertThat(lesson.scheduleRuleSlotId()).isEqualTo(101L));
        verify(overrideRepository).findByLessonDateBetweenWithDetails(DATE, DATE);
        verify(groupRepository, never()).findAll();
        verify(generatorService, never()).buildScheduleForGroups(any(), eq(DATE), eq(DATE));
    }

    @Test
    @DisplayName("Отмена и перенос применяются из единого снимка изменений")
    void cancelAndMoveContinueToApplyFromSingleSnapshot() {
        RenderedLessonDto cancelledLesson = lesson(101L, SOURCE_TEACHER_ID, 1);
        RenderedLessonDto movedLesson = lesson(102L, SOURCE_TEACHER_ID, 2);
        ScheduleOverride cancellation = override(101L, "CANCEL");
        ScheduleOverride move = override(102L, "MOVE");
        TimeSlot newTimeSlot = new TimeSlot();
        newTimeSlot.setId(3L);
        newTimeSlot.setOrderNumber(3);
        newTimeSlot.setStartTime(LocalTime.of(13, 0));
        newTimeSlot.setEndTime(LocalTime.of(14, 30));
        move.setNewTimeSlot(newTimeSlot);
        when(groupRepository.findById(GROUP_ID)).thenReturn(Optional.of(group()));
        when(groupLifecycleService.mayHaveScheduleInRange(any(), any(), any())).thenReturn(true);
        when(generatorService.buildScheduleForGroups(any(), eq(DATE), eq(DATE)))
                .thenReturn(List.of(cancelledLesson, movedLesson));
        when(overrideRepository.findByLessonDateBetweenWithDetails(DATE, DATE))
                .thenReturn(List.of(cancellation, move));

        List<RenderedLessonDto> result = service.search(
                GROUP_ID, null, null, null, null, null, null, null, DATE, DATE
        );

        assertThat(result).singleElement().satisfies(lesson -> {
            assertThat(lesson.scheduleRuleSlotId()).isEqualTo(102L);
            assertThat(lesson.timeSlotId()).isEqualTo(3L);
            assertThat(lesson.startTime()).isEqualTo(LocalTime.of(13, 0));
            assertThat(lesson.endTime()).isEqualTo(LocalTime.of(14, 30));
        });
        verify(overrideRepository).findByLessonDateBetweenWithDetails(DATE, DATE);
    }

    private List<RenderedLessonDto> searchTeacher(long teacherId) {
        return service.search(
                null, teacherId, null, null, null, null, null, null, DATE, DATE
        );
    }

    private void stubBaseDay(List<RenderedLessonDto> lessons) {
        when(groupRepository.findAll()).thenReturn(List.of(group()));
        when(groupLifecycleService.mayHaveScheduleInRange(any(), any(), any())).thenReturn(true);
        when(generatorService.buildScheduleForGroups(any(), eq(DATE), eq(DATE))).thenReturn(lessons);
    }

    private void verifyBaseDayBatch() {
        verify(generatorService).buildScheduleForGroups(
                argThat(groups -> groups.size() == 1 && groups.iterator().next().getId().equals(GROUP_ID)),
                eq(DATE),
                eq(DATE)
        );
    }

    private StudentGroup group() {
        StudentGroup group = new StudentGroup();
        group.setId(GROUP_ID);
        group.setName("Группа 31");
        return group;
    }

    private ScheduleOverride replacement(long baseRuleSlotId, long teacherId) {
        ScheduleOverride override = override(baseRuleSlotId, "REPLACE");
        User newTeacher = new User();
        newTeacher.setId(teacherId);
        newTeacher.setRole(Role.TEACHER);
        newTeacher.setFullName("Замещающий преподаватель");
        override.setNewTeacher(newTeacher);
        return override;
    }

    private ScheduleOverride override(long baseRuleSlotId, String action) {
        ScheduleRuleSlot baseRuleSlot = new ScheduleRuleSlot();
        baseRuleSlot.setId(baseRuleSlotId);
        ScheduleOverride override = new ScheduleOverride();
        override.setBaseRuleSlot(baseRuleSlot);
        override.setLessonDate(DATE);
        override.setAction(action);
        return override;
    }

    private RenderedLessonDto lesson(long baseRuleSlotId, long teacherId, int timeSlotOrder) {
        return new RenderedLessonDto(
                900L + baseRuleSlotId,
                baseRuleSlotId,
                DATE,
                2,
                "Вторник",
                1,
                ScheduleParity.BOTH,
                (long) timeSlotOrder,
                timeSlotOrder,
                LocalTime.of(7 + timeSlotOrder * 2, 0),
                LocalTime.of(8 + timeSlotOrder * 2, 30),
                500L + baseRuleSlotId,
                "Тестовая дисциплина " + baseRuleSlotId,
                teacherId,
                teacherId == SOURCE_TEACHER_ID ? "Исходный преподаватель" : "Замещающий преподаватель",
                21L,
                "А-101",
                601L,
                "Лекция",
                "Очно",
                null,
                null,
                List.of(),
                List.of(),
                List.of(GROUP_ID),
                List.of("Группа 31"),
                "Т",
                2,
                0,
                0,
                ScheduleParity.BOTH
        );
    }
}
