package com.magistr.app.service;

import com.magistr.app.dto.TimeSlotDto;
import com.magistr.app.model.TimeSlot;
import com.magistr.app.model.TimeSlotScope;
import com.magistr.app.repository.ScheduleRuleSlotRepository;
import com.magistr.app.repository.TimeSlotRepository;
import com.magistr.app.repository.TimeSlotScopeRepository;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.time.LocalTime;
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 TimeSlotServiceTest {

    private static final long DEFAULT_SCOPE_ID = 10L;
    private static final long MANUAL_SCOPE_ID = 20L;
    private static final long SLOT_ID = 30L;

    @Mock
    private TimeSlotRepository timeSlotRepository;
    @Mock
    private TimeSlotScopeRepository timeSlotScopeRepository;
    @Mock
    private ScheduleRuleSlotRepository scheduleRuleSlotRepository;

    private TimeSlotService service;
    private TimeSlotScope defaultScope;
    private TimeSlotScope manualScope;

    @BeforeEach
    void setUp() {
        service = new TimeSlotService(
                timeSlotRepository,
                timeSlotScopeRepository,
                scheduleRuleSlotRepository
        );
        defaultScope = scope(DEFAULT_SCOPE_ID, "DEFAULT");
        manualScope = scope(MANUAL_SCOPE_ID, "MANUAL");
        when(timeSlotRepository.saveAndFlush(any(TimeSlot.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));
    }

    @Test
    void calculatesDurationOnBackendAndIgnoresRequestValue() {
        when(timeSlotScopeRepository.findByIdForUpdate(DEFAULT_SCOPE_ID))
                .thenReturn(Optional.of(defaultScope));

        TimeSlot saved = service.create(request(
                DEFAULT_SCOPE_ID,
                1,
                LocalTime.of(8, 0),
                LocalTime.of(9, 30),
                999
        ));

        assertThat(saved.getDurationMinutes()).isEqualTo(90);
        ArgumentCaptor<TimeSlot> captor = ArgumentCaptor.forClass(TimeSlot.class);
        verify(timeSlotRepository).saveAndFlush(captor.capture());
        assertThat(captor.getValue().getDurationMinutes()).isEqualTo(90);
    }

    @Test
    void rejectsOverlapWithinScope() {
        when(timeSlotScopeRepository.findByIdForUpdate(DEFAULT_SCOPE_ID))
                .thenReturn(Optional.of(defaultScope));
        when(timeSlotRepository.existsOverlapping(
                DEFAULT_SCOPE_ID,
                LocalTime.of(9, 0),
                LocalTime.of(10, 0)
        )).thenReturn(true);

        assertThatThrownBy(() -> service.create(request(
                DEFAULT_SCOPE_ID,
                2,
                LocalTime.of(9, 0),
                LocalTime.of(10, 0),
                null
        )))
                .isInstanceOf(ScheduleConflictException.class)
                .hasMessage("Номер пары или интервал времени уже занят в выбранной сетке");

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

    @Test
    void allowsAdjacentIntervals() {
        when(timeSlotScopeRepository.findByIdForUpdate(DEFAULT_SCOPE_ID))
                .thenReturn(Optional.of(defaultScope));
        when(timeSlotRepository.existsOverlapping(
                DEFAULT_SCOPE_ID,
                LocalTime.of(9, 30),
                LocalTime.of(11, 0)
        )).thenReturn(false);

        TimeSlot saved = service.create(request(
                DEFAULT_SCOPE_ID,
                2,
                LocalTime.of(9, 30),
                LocalTime.of(11, 0),
                null
        ));

        assertThat(saved.getStartTime()).isEqualTo(LocalTime.of(9, 30));
        assertThat(saved.getDurationMinutes()).isEqualTo(90);
    }

    @Test
    void rejectsMovingUsedBaseSlotToManualScope() {
        TimeSlot existing = slot(SLOT_ID, defaultScope);
        when(timeSlotRepository.findByIdForUpdate(SLOT_ID)).thenReturn(Optional.of(existing));
        when(timeSlotScopeRepository.findByIdForUpdate(DEFAULT_SCOPE_ID))
                .thenReturn(Optional.of(defaultScope));
        when(timeSlotScopeRepository.findByIdForUpdate(MANUAL_SCOPE_ID))
                .thenReturn(Optional.of(manualScope));
        when(scheduleRuleSlotRepository.existsByTimeSlotId(SLOT_ID)).thenReturn(true);

        assertThatThrownBy(() -> service.update(SLOT_ID, request(
                MANUAL_SCOPE_ID,
                1,
                LocalTime.of(8, 0),
                LocalTime.of(9, 30),
                null
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Слот базовой сетки, используемый в правилах расписания, нельзя перенести в другую сетку");

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

    @Test
    void calculatesDurationAgainOnUpdate() {
        TimeSlot existing = slot(SLOT_ID, defaultScope);
        when(timeSlotRepository.findByIdForUpdate(SLOT_ID)).thenReturn(Optional.of(existing));
        when(timeSlotScopeRepository.findByIdForUpdate(DEFAULT_SCOPE_ID))
                .thenReturn(Optional.of(defaultScope));

        TimeSlot saved = service.update(SLOT_ID, request(
                DEFAULT_SCOPE_ID,
                1,
                LocalTime.of(8, 15),
                LocalTime.of(9, 45),
                5
        ));

        assertThat(saved.getDurationMinutes()).isEqualTo(90);
        assertThat(saved.getStartTime()).isEqualTo(LocalTime.of(8, 15));
    }

    @Test
    void rejectsInvalidOrSubMinuteRangeBeforeLocking() {
        assertThatThrownBy(() -> service.create(request(
                DEFAULT_SCOPE_ID,
                1,
                LocalTime.of(10, 0),
                LocalTime.of(10, 0),
                null
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Время начала должно быть раньше времени окончания");

        assertThatThrownBy(() -> service.create(request(
                DEFAULT_SCOPE_ID,
                1,
                LocalTime.of(10, 0, 0),
                LocalTime.of(10, 0, 30),
                null
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Длительность временного слота должна быть не меньше одной минуты");

        verify(timeSlotScopeRepository, never()).findByIdForUpdate(any());
    }

    private TimeSlotDto request(Long scopeId,
                                Integer orderNumber,
                                LocalTime startTime,
                                LocalTime endTime,
                                Integer durationMinutes) {
        return new TimeSlotDto(
                null,
                orderNumber,
                scopeId,
                null,
                null,
                null,
                startTime,
                endTime,
                durationMinutes
        );
    }

    private TimeSlotScope scope(Long id, String applyMode) {
        TimeSlotScope scope = new TimeSlotScope();
        scope.setId(id);
        scope.setApplyMode(applyMode);
        return scope;
    }

    private TimeSlot slot(Long id, TimeSlotScope scope) {
        TimeSlot slot = new TimeSlot();
        slot.setId(id);
        slot.setTimeSlotScope(scope);
        slot.setOrderNumber(1);
        slot.setStartTime(LocalTime.of(8, 0));
        slot.setEndTime(LocalTime.of(9, 30));
        slot.setDurationMinutes(90);
        return slot;
    }
}
