package com.magistr.app.service;

import com.magistr.app.dto.AcademicYearDto;
import com.magistr.app.dto.SemesterDto;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.model.Semester;
import com.magistr.app.model.SemesterType;
import com.magistr.app.repository.AcademicYearRepository;
import com.magistr.app.repository.SemesterRepository;
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 org.springframework.dao.DataIntegrityViolationException;

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 AcademicPeriodServiceTest {

    private static final long YEAR_ID = 10L;
    private static final long SEMESTER_ID = 20L;
    private static final LocalDate YEAR_START = LocalDate.of(2026, 9, 1);
    private static final LocalDate YEAR_END = LocalDate.of(2027, 6, 30);

    @Mock
    private AcademicYearRepository academicYearRepository;
    @Mock
    private SemesterRepository semesterRepository;
    @Mock
    private ScheduleGeneratorService scheduleGeneratorService;

    private AcademicPeriodService service;
    private AcademicYear year;

    @BeforeEach
    void setUp() {
        service = new AcademicPeriodService(
                academicYearRepository,
                semesterRepository,
                scheduleGeneratorService
        );
        year = year();
        when(academicYearRepository.saveAndFlush(any(AcademicYear.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
        when(semesterRepository.saveAndFlush(any(Semester.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
    }

    @Test
    void createsNonOverlappingYearWithTrimmedTitle() {
        AcademicYear saved = service.createYear(yearRequest(
                " 2026-2027 ",
                YEAR_START,
                YEAR_END
        ));

        assertThat(saved.getTitle()).isEqualTo("2026-2027");
        assertThat(saved.getStartDate()).isEqualTo(YEAR_START);
        verify(scheduleGeneratorService).clearCache();
    }

    @Test
    void rejectsDuplicateTitleOrOverlappingYear() {
        when(academicYearRepository.existsByTitle("2026-2027")).thenReturn(true);

        assertThatThrownBy(() -> service.createYear(yearRequest(
                "2026-2027",
                YEAR_START,
                YEAR_END
        )))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Название или период учебного года конфликтует с существующим учебным годом");

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

    @Test
    void rejectsYearUpdateThatWouldExcludeExistingSemester() {
        Semester autumn = semester(
                SEMESTER_ID,
                SemesterType.autumn,
                YEAR_START,
                LocalDate.of(2027, 1, 31)
        );
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(year));
        when(semesterRepository.findByAcademicYearIdOrderByStartDateAsc(YEAR_ID))
                .thenReturn(List.of(autumn));

        assertThatThrownBy(() -> service.updateYear(YEAR_ID, yearRequest(
                "2026-2027",
                YEAR_START.plusDays(1),
                YEAR_END
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Новые границы учебного года не включают все его семестры");

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

    @Test
    void rejectsSemesterOutsideAcademicYear() {
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(year));

        assertThatThrownBy(() -> service.createSemester(YEAR_ID, semesterRequest(
                SemesterType.autumn,
                YEAR_START.minusDays(1),
                LocalDate.of(2027, 1, 31)
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Семестр должен полностью находиться в границах учебного года");

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

    @Test
    void rejectsDuplicateTypeOrOverlappingSemester() {
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(year));
        when(semesterRepository.existsOverlapping(
                YEAR_ID,
                YEAR_START,
                LocalDate.of(2027, 2, 1)
        )).thenReturn(true);

        assertThatThrownBy(() -> service.createSemester(YEAR_ID, semesterRequest(
                SemesterType.autumn,
                YEAR_START,
                LocalDate.of(2027, 2, 1)
        )))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Тип или период семестра конфликтует с существующим семестром учебного года");

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

    @Test
    void acceptsAdjacentSemesters() {
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(year));

        Semester saved = service.createSemester(YEAR_ID, semesterRequest(
                SemesterType.spring,
                LocalDate.of(2027, 2, 1),
                YEAR_END
        ));

        assertThat(saved.getStartDate()).isEqualTo(LocalDate.of(2027, 2, 1));
        assertThat(saved.getAcademicYear()).isSameAs(year);
        verify(scheduleGeneratorService).clearCache();
    }

    @Test
    void validatesDuplicateSemesterTypeDuringUpdate() {
        Semester existing = semester(
                SEMESTER_ID,
                SemesterType.spring,
                LocalDate.of(2027, 2, 1),
                YEAR_END
        );
        when(semesterRepository.findById(SEMESTER_ID)).thenReturn(Optional.of(existing));
        when(academicYearRepository.findByIdForUpdate(YEAR_ID)).thenReturn(Optional.of(year));
        when(semesterRepository.findByIdForUpdate(SEMESTER_ID)).thenReturn(Optional.of(existing));
        when(semesterRepository.existsByAcademicYearIdAndSemesterTypeAndIdNot(
                YEAR_ID,
                SemesterType.autumn,
                SEMESTER_ID
        )).thenReturn(true);

        assertThatThrownBy(() -> service.updateSemester(SEMESTER_ID, semesterRequest(
                SemesterType.autumn,
                LocalDate.of(2027, 2, 1),
                YEAR_END
        )))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Тип или период семестра конфликтует с существующим семестром учебного года");

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

    @Test
    void mapsDatabaseRaceToSafeConflict() {
        when(academicYearRepository.saveAndFlush(any(AcademicYear.class)))
                .thenThrow(new DataIntegrityViolationException("ex_academic_years_no_overlap"));

        assertThatThrownBy(() -> service.createYear(yearRequest(
                "2026-2027",
                YEAR_START,
                YEAR_END
        )))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Название или период учебного года конфликтует с существующим учебным годом")
                .hasNoCause();
    }

    private AcademicYearDto yearRequest(String title, LocalDate startDate, LocalDate endDate) {
        return new AcademicYearDto(null, title, startDate, endDate, List.of());
    }

    private SemesterDto semesterRequest(SemesterType semesterType,
                                        LocalDate startDate,
                                        LocalDate endDate) {
        return new SemesterDto(
                null,
                YEAR_ID,
                "2026-2027",
                semesterType,
                startDate,
                endDate
        );
    }

    private AcademicYear year() {
        AcademicYear academicYear = new AcademicYear();
        academicYear.setId(YEAR_ID);
        academicYear.setTitle("2026-2027");
        academicYear.setStartDate(YEAR_START);
        academicYear.setEndDate(YEAR_END);
        return academicYear;
    }

    private Semester semester(Long id,
                              SemesterType semesterType,
                              LocalDate startDate,
                              LocalDate endDate) {
        Semester semester = new Semester();
        semester.setId(id);
        semester.setAcademicYear(year);
        semester.setSemesterType(semesterType);
        semester.setStartDate(startDate);
        semester.setEndDate(endDate);
        return semester;
    }
}
