package com.magistr.app.service;

import com.magistr.app.dto.AcademicCalendarDto;
import com.magistr.app.dto.CreateGroupRequest;
import com.magistr.app.dto.GroupCalendarAssignmentDto;
import com.magistr.app.model.AcademicCalendar;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.model.EducationForm;
import com.magistr.app.model.Speciality;
import com.magistr.app.model.SpecialtyProfile;
import com.magistr.app.model.StudentGroup;
import com.magistr.app.model.StudentGroupCalendarAssignment;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import com.magistr.app.repository.AcademicCalendarRepository;
import com.magistr.app.repository.AcademicCalendarSubjectRepository;
import com.magistr.app.repository.AcademicYearRepository;
import com.magistr.app.repository.EducationFormRepository;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.SpecialtiesRepository;
import com.magistr.app.repository.SpecialtyProfileRepository;
import com.magistr.app.repository.StudentGroupCalendarAssignmentRepository;
import com.magistr.app.repository.SubgroupRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.util.List;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

    private static final long YEAR_ID = 10L;
    private static final long SPECIALITY_ID = 20L;
    private static final long PROFILE_ID = 30L;
    private static final long FORM_ID = 40L;
    private static final long OTHER_FORM_ID = 41L;
    private static final long CALENDAR_ID = 50L;
    private static final long GROUP_ID = 60L;

    @Mock
    private AcademicCalendarRepository calendarRepository;
    @Mock
    private AcademicCalendarDayRepository calendarDayRepository;
    @Mock
    private AcademicCalendarSubjectRepository calendarSubjectRepository;
    @Mock
    private AcademicYearRepository academicYearRepository;
    @Mock
    private SpecialtiesRepository specialtiesRepository;
    @Mock
    private SpecialtyProfileRepository profileRepository;
    @Mock
    private EducationFormRepository educationFormRepository;
    @Mock
    private GroupRepository groupRepository;
    @Mock
    private StudentGroupCalendarAssignmentRepository assignmentRepository;
    @Mock
    private SubgroupRepository subgroupRepository;
    @Mock
    private ScheduleGeneratorService scheduleGeneratorService;

    private AcademicStructureService service;
    private AcademicYear academicYear;
    private Speciality speciality;
    private SpecialtyProfile profile;
    private EducationForm studyForm;
    private EducationForm otherStudyForm;
    private AcademicCalendar calendar;
    private StudentGroup group;

    @BeforeEach
    void setUp() {
        service = new AcademicStructureService(
                calendarRepository,
                calendarDayRepository,
                calendarSubjectRepository,
                academicYearRepository,
                specialtiesRepository,
                profileRepository,
                educationFormRepository,
                groupRepository,
                assignmentRepository,
                subgroupRepository,
                scheduleGeneratorService
        );
        academicYear = academicYear();
        speciality = speciality();
        profile = profile(speciality);
        studyForm = studyForm(FORM_ID, "Бакалавриат");
        otherStudyForm = studyForm(OTHER_FORM_ID, "Магистратура");
        calendar = calendar();
        group = group(2025);

        when(calendarRepository.findByIdWithDetailsForUpdate(CALENDAR_ID))
                .thenReturn(Optional.of(calendar));
        when(academicYearRepository.findById(YEAR_ID)).thenReturn(Optional.of(academicYear));
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(academicYear));
        when(specialtiesRepository.findById(SPECIALITY_ID)).thenReturn(Optional.of(speciality));
        when(profileRepository.findById(PROFILE_ID)).thenReturn(Optional.of(profile));
        when(educationFormRepository.findById(FORM_ID)).thenReturn(Optional.of(studyForm));
        when(educationFormRepository.findById(OTHER_FORM_ID)).thenReturn(Optional.of(otherStudyForm));
        when(groupRepository.findByIdForUpdate(GROUP_ID)).thenReturn(Optional.of(group));
        when(subgroupRepository.sumActiveStudentCapacityByGroupId(GROUP_ID)).thenReturn(0L);
        when(calendarRepository.saveAndFlush(any(AcademicCalendar.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
        when(groupRepository.saveAndFlush(any(StudentGroup.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
        when(assignmentRepository.saveAndFlush(any(StudentGroupCalendarAssignment.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
    }

    @Test
    void rejectsCalendarDimensionChangeAndKeepsEntityUnchanged() {
        when(assignmentRepository.findByCalendarIdWithDetails(CALENDAR_ID))
                .thenReturn(List.of(assignment(group, calendar)));

        assertThatThrownBy(() -> service.updateCalendar(
                CALENDAR_ID,
                calendarRequest(4, OTHER_FORM_ID)
        ))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Нельзя изменить график: сохранённые назначения групп станут несовместимыми");

        assertThat(calendar.getStudyForm()).isSameAs(studyForm);
        verify(calendarRepository, never()).saveAndFlush(any());
    }

    @Test
    void rejectsCourseCountBelowExistingGrid() {
        when(calendarDayRepository.existsByAcademicCalendarIdAndCourseNumberGreaterThan(
                CALENDAR_ID, 3)).thenReturn(true);

        assertThatThrownBy(() -> service.updateCalendar(
                CALENDAR_ID,
                calendarRequest(3, FORM_ID)
        ))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Нельзя уменьшить количество курсов: в сетке есть данные старших курсов");

        assertThat(calendar.getCourseCount()).isEqualTo(4);
    }

    @Test
    void rejectsCourseCountBelowExistingSubjects() {
        when(calendarSubjectRepository.existsByAcademicCalendarIdAndSemesterNumberGreaterThan(
                CALENDAR_ID, 6)).thenReturn(true);

        assertThatThrownBy(() -> service.updateCalendar(
                CALENDAR_ID,
                calendarRequest(3, FORM_ID)
        ))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Нельзя уменьшить количество курсов: есть дисциплины старших семестров");
    }

    @Test
    void buildsCalendarTitleFromDimensionsInsteadOfRequestField() {
        AcademicCalendar saved = service.updateCalendar(
                CALENDAR_ID,
                calendarRequest(4, FORM_ID)
        );

        assertThat(saved.getTitle())
                .isEqualTo("09.03.01 — Без профиля — Бакалавриат — 2025-2026");
        verify(calendarRepository).saveAndFlush(calendar);
    }

    @Test
    void rejectsGroupDimensionChangeAndKeepsAssignment() {
        when(assignmentRepository.findByGroupIdWithDetails(GROUP_ID))
                .thenReturn(List.of(assignment(group, calendar)));

        assertThatThrownBy(() -> service.updateGroup(
                GROUP_ID,
                groupRequest(2025, OTHER_FORM_ID)
        ))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Нельзя изменить группу: сохранённые назначения календарных графиков станут несовместимыми");

        assertThat(group.getEducationForm()).isSameAs(studyForm);
        verify(groupRepository, never()).saveAndFlush(any());
        verify(assignmentRepository, never()).delete(any());
    }

    @Test
    void acceptsCompatibleGroupUpdateAndClearsCache() {
        when(assignmentRepository.findByGroupIdWithDetails(GROUP_ID))
                .thenReturn(List.of(assignment(group, calendar)));

        StudentGroup saved = service.updateGroup(GROUP_ID, groupRequest(2025, FORM_ID));

        assertThat(saved.getName()).isEqualTo("ИВТ-26");
        assertThat(saved.getEducationForm()).isSameAs(studyForm);
        verify(scheduleGeneratorService).clearCache();
    }

    @Test
    void createsGroupThroughSharedValidationAndClearsCache() {
        StudentGroup saved = service.createGroup(groupRequest(2025, FORM_ID));

        assertThat(saved.getName()).isEqualTo("ИВТ-26");
        assertThat(saved.getGroupSize()).isEqualTo(25L);
        assertThat(saved.getEducationForm()).isSameAs(studyForm);
        verify(groupRepository).saveAndFlush(saved);
        verify(scheduleGeneratorService).clearCache();
    }

    @Test
    void rejectsNonPositiveGroupSizeOnCreateAndUpdate() {
        CreateGroupRequest createRequest = groupRequest(2025, FORM_ID);
        createRequest.setGroupSize(0L);
        CreateGroupRequest updateRequest = groupRequest(2025, FORM_ID);
        updateRequest.setGroupSize(-1L);

        assertThatThrownBy(() -> service.createGroup(createRequest))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Численность группы должна быть больше нуля");
        assertThatThrownBy(() -> service.updateGroup(GROUP_ID, updateRequest))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Численность группы должна быть больше нуля");

        verify(groupRepository, never()).saveAndFlush(any());
    }

    @Test
    void rejectsGroupSizeBelowActiveSubgroups() {
        when(subgroupRepository.sumActiveStudentCapacityByGroupId(GROUP_ID)).thenReturn(24L);
        CreateGroupRequest request = groupRequest(2025, FORM_ID);
        request.setGroupSize(23L);

        assertThatThrownBy(() -> service.updateGroup(GROUP_ID, request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Численность группы не может быть меньше суммы численностей активных подгрупп (24 чел.)");

        verify(groupRepository, never()).saveAndFlush(any());
    }

    @Test
    void rejectsNonPositiveStudyStartYear() {
        CreateGroupRequest request = groupRequest(0, FORM_ID);

        assertThatThrownBy(() -> service.createGroup(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Год начала обучения должен быть больше нуля");
    }

    @Test
    void rejectsAssignmentWhenGroupCourseIsOutsideCalendar() {
        group.setYearStartStudy(2020);

        assertThatThrownBy(() -> service.saveAssignment(
                GROUP_ID,
                assignmentRequest()
        ))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Календарный график не соответствует учебному году, параметрам или курсу группы");

        verify(assignmentRepository, never()).saveAndFlush(any());
    }

    @Test
    void savesCompatibleAssignmentWithoutDeletingExistingData() {
        when(assignmentRepository.findByGroupIdAndAcademicYearIdForUpdate(GROUP_ID, YEAR_ID))
                .thenReturn(Optional.empty());

        StudentGroupCalendarAssignment saved = service.saveAssignment(
                GROUP_ID,
                assignmentRequest()
        );

        assertThat(saved.getStudentGroup()).isSameAs(group);
        assertThat(saved.getAcademicCalendar()).isSameAs(calendar);
        verify(assignmentRepository, never()).delete(any());
        verify(scheduleGeneratorService).clearCache();
    }

    private AcademicCalendarDto calendarRequest(int courseCount, long studyFormId) {
        return new AcademicCalendarDto(
                CALENDAR_ID,
                null,
                YEAR_ID,
                null,
                null,
                null,
                SPECIALITY_ID,
                null,
                null,
                PROFILE_ID,
                null,
                studyFormId,
                null,
                courseCount
        );
    }

    private CreateGroupRequest groupRequest(int yearStartStudy, long studyFormId) {
        CreateGroupRequest request = new CreateGroupRequest();
        request.setName("ИВТ-26");
        request.setGroupSize(25L);
        request.setEducationFormId(studyFormId);
        request.setDepartmentId(1L);
        request.setYearStartStudy(yearStartStudy);
        request.setSpecialtyId(SPECIALITY_ID);
        request.setSpecialtyProfileId(PROFILE_ID);
        return request;
    }

    private GroupCalendarAssignmentDto assignmentRequest() {
        return new GroupCalendarAssignmentDto(
                null,
                GROUP_ID,
                null,
                YEAR_ID,
                null,
                CALENDAR_ID,
                null,
                List.of()
        );
    }

    private AcademicYear academicYear() {
        AcademicYear year = new AcademicYear();
        year.setId(YEAR_ID);
        year.setTitle("2025-2026");
        year.setStartDate(LocalDate.of(2025, 9, 1));
        year.setEndDate(LocalDate.of(2026, 6, 30));
        return year;
    }

    private Speciality speciality() {
        Speciality value = new Speciality();
        value.setId(SPECIALITY_ID);
        value.setSpecialityCode("09.03.01");
        value.setSpecialityName("Информатика");
        return value;
    }

    private SpecialtyProfile profile(Speciality owner) {
        SpecialtyProfile value = new SpecialtyProfile();
        value.setId(PROFILE_ID);
        value.setName("Без профиля");
        value.setSpeciality(owner);
        return value;
    }

    private EducationForm studyForm(long id, String name) {
        EducationForm value = new EducationForm();
        value.setId(id);
        value.setName(name);
        return value;
    }

    private AcademicCalendar calendar() {
        AcademicCalendar value = new AcademicCalendar();
        value.setId(CALENDAR_ID);
        value.setTitle("Исходный график");
        value.setAcademicYear(academicYear);
        value.setSpeciality(speciality);
        value.setSpecialtyProfile(profile);
        value.setStudyForm(studyForm);
        value.setCourseCount(4);
        return value;
    }

    private StudentGroup group(int yearStartStudy) {
        StudentGroup value = new StudentGroup();
        value.setId(GROUP_ID);
        value.setName("ИВТ-25");
        value.setGroupSize(25L);
        value.setEducationForm(studyForm);
        value.setDepartmentId(1L);
        value.setYearStartStudy(yearStartStudy);
        value.setSpeciality(speciality);
        value.setSpecialtyProfile(profile);
        return value;
    }

    private StudentGroupCalendarAssignment assignment(StudentGroup assignedGroup,
                                                      AcademicCalendar assignedCalendar) {
        StudentGroupCalendarAssignment value = new StudentGroupCalendarAssignment();
        value.setId(70L);
        value.setStudentGroup(assignedGroup);
        value.setAcademicYear(academicYear);
        value.setAcademicCalendar(assignedCalendar);
        return value;
    }
}
