/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2024  Delft University of Technology
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package nl.tudelft.queue.service;

import static nl.tudelft.queue.model.enums.RequestStatus.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.AdditionalMatchers.or;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithUserDetails;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.RoleControllerApi;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.cache.PersonCacheManager;
import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.SelectionRequest;
import nl.tudelft.queue.model.embeddables.CapacitySessionConfig;
import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.enums.SelectionProcedure;
import nl.tudelft.queue.model.labs.CapacitySession;
import nl.tudelft.queue.model.labs.RegularLab;
import nl.tudelft.queue.model.labs.SlottedLab;
import nl.tudelft.queue.repository.CapacitySessionRepository;
import nl.tudelft.queue.repository.SelectionRequestRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import test.TestDatabaseLoader;
import test.labracore.*;
import test.test.TestQueueApplication;

@Transactional
@SpringBootTest(classes = TestQueueApplication.class)
public class RequestServiceTest {
	private final RoomDetailsDTO room1 = new RoomDetailsDTO()
			.id(8932L)
			.capacity(3);

	private final SessionDetailsDTO lcSessionNow = new SessionDetailsDTO()
			.id(78323L)
			.name("Session now")
			.start(LocalDateTime.now().minusHours(1).minusHours(4))
			.endTime(LocalDateTime.now().minusHours(1))
			.edition(new EditionSummaryDTO().id(124902375L))
			.rooms(Set.of(room1));

	private final SessionDetailsDTO lcSessionOld1 = new SessionDetailsDTO()
			.id(78324L)
			.name("Session old 1")
			.start(LocalDateTime.now().minusDays(2).minusHours(4))
			.endTime(LocalDateTime.now().minusDays(2))
			.rooms(Set.of(room1));

	private final SessionDetailsDTO lcSessionOld2 = new SessionDetailsDTO()
			.id(78325L)
			.name("Session old 2")
			.start(LocalDateTime.now().minusDays(1).minusHours(4))
			.endTime(LocalDateTime.now().minusDays(1))
			.rooms(Set.of(room1));

	private static final LocalDateTime LATER = LocalDateTime.now().plusHours(3L);

	private CapacitySession session;

	private EditionDetailsDTO oopNow;

	private EditionDetailsDTO rlNow;

	private RegularLab oopNowRegularLab1;

	private SlottedLab oopNowSlottedLab1;

	private RoleDetailsDTO[] oopNowTAs;

	private LabRequest[] rlOopNowSharedLabRequests;

	CapacitySessionConfig.CapacitySessionConfigBuilder sessionConfigBuilderSelectNow;
	CapacitySession.CapacitySessionBuilder<?, ? extends CapacitySession.CapacitySessionBuilder<?, ?>> sessionBuilder;

	@Autowired
	private TestDatabaseLoader db;
	@Autowired
	private CapacitySessionRepository csr;

	@Autowired
	private SelectionRequestRepository srr;

	@SpyBean
	private RequestService rs;

	@Autowired
	private SessionApiMocker sApiMocker;

	@Autowired
	private RoomApiMocker rApiMocker;

	@Autowired
	private StudentGroupApiMocker sgApiMocker;

	@Autowired
	private PersonApiMocker pApiMocker;

	@Autowired
	private RoleApiMocker rlApiMocker;

	@Autowired
	private ModuleApiMocker mApiMocker;

	@Autowired
	private AssignmentApiMocker asApiMocker;

	@SpyBean
	private AssignmentControllerApi asApi;

	@Autowired
	private StudentGroupControllerApi sgApi;

	@SpyBean
	private PersonCacheManager pCache;

	@Autowired
	private RoleControllerApi rlApi;

	@BeforeEach
	void setUp() {
		sessionBuilder = CapacitySession.builder()
				.modules(Set.of(1L))
				.session(lcSessionNow.getId())
				.constraints(LabRequestConstraints.builder().build())
				.requests(new ArrayList<>());
		sessionConfigBuilderSelectNow = CapacitySessionConfig.builder()
				.enrolmentOpensAt(LocalDateTime.now().minusDays(2))
				.enrolmentClosesAt(LocalDateTime.now().minusDays(1))
				.selectionAt(LocalDateTime.now().minusMinutes(3));

		oopNowRegularLab1 = db.getOopNowRegularLab1();

		oopNowSlottedLab1 = db.getOopNowSlottedLab1();

		oopNowTAs = db.getOopNowTAs();

		rlOopNowSharedLabRequests = db.getRlOopNowSharedLabRequests();

		oopNow = db.getOopNow();

		rlNow = db.getRlNow();

		sApiMocker.save(lcSessionNow);
		sApiMocker.save(lcSessionOld1);
		sApiMocker.save(lcSessionOld2);

		rApiMocker.save(room1);

		sApiMocker.mock();
		sgApiMocker.mock();
		rApiMocker.mock();
		rlApiMocker.mock();
		pApiMocker.mock();
		mApiMocker.mock();
		asApiMocker.mock();
	}

	private void mockStudentGroup(Long moduleId, List<Long> requesterIds, Long studentGroupId) {
		sgApiMocker.save(new StudentGroupDetailsDTO()
				.id(studentGroupId)
				.members(requesterIds.stream()
						.map(id -> new RolePersonLayer1DTO().type(RolePersonLayer1DTO.TypeEnum.STUDENT)
								.person(new PersonSummaryDTO().id(id)))
						.collect(Collectors.toList()))
				.module(new ModuleSummaryDTO().id(moduleId))
				.name("Group " + requesterIds.stream().findFirst().get())
				.capacity(1));
	}

	private void createSelectionRequest(CapacitySession session, Long personId, Long module, Long room) {
		var createDto = SelectionRequestCreateDTO.builder()
				.session(session)
				.module(module)
				.room(room)
				.build();
		mockStudentGroup(module, List.of(personId), module * 1000 + personId);
		rs.createRequest(createDto, personId, false);
	}

	private CapacitySession setupCapacitySession(Long session, LocalDateTime selectionAt,
			SelectionProcedure procedure) {
		return csr.save(sessionBuilder.session(session)
				.capacitySessionConfig(sessionConfigBuilderSelectNow
						.procedure(procedure)
						.selectionAt(selectionAt)
						.build())
				.requests(new ArrayList<>())
				.build());
	}

	private Optional<SelectionRequest> findSelectionRequestByRequester(Long requester) {
		return srr.findAll().stream().filter(r -> Objects.equals(r.getRequester(), requester)).findFirst();
	}

	private void assertThatRequestIsStatus(Long requester, RequestStatus status) {
		assertThat(findSelectionRequestByRequester(requester)).isNotEmpty()
				.hasValueSatisfying(r -> assertThat(r.getEventInfo().getStatus()).isEqualTo(status));
	}

	@Test
	void creatingSelectionRequestInFcfsShouldImmediatelySelect() {
		session = setupCapacitySession(lcSessionNow.getId(), LATER, SelectionProcedure.FCFS);

		createSelectionRequest(session, 1L, 982L, room1.getId());

		assertThat(session.getRequests()).size().isEqualTo(1);
		assertThatRequestIsStatus(1L, SELECTED);
	}

	@Test
	void creatingRequestOnCapacityDoesNotSelectIt() {
		session = setupCapacitySession(lcSessionNow.getId(), LATER, SelectionProcedure.FCFS);

		createSelectionRequest(session, 1L, 982L, room1.getId());
		createSelectionRequest(session, 2L, 982L, room1.getId());
		createSelectionRequest(session, 3L, 982L, room1.getId());
		createSelectionRequest(session, 4L, 982L, room1.getId());

		assertThat(session.getRequests()).size().isEqualTo(4);
		assertThatRequestIsStatus(1L, SELECTED);
		assertThatRequestIsStatus(2L, SELECTED);
		assertThatRequestIsStatus(3L, SELECTED);
		assertThatRequestIsStatus(4L, PENDING);
	}

	@Test
	void revokingRequestOnFullCapacityAllowsOtherRequestToBeSelected() {
		session = setupCapacitySession(lcSessionNow.getId(), LATER, SelectionProcedure.FCFS);

		createSelectionRequest(session, 1L, 982L, room1.getId());
		createSelectionRequest(session, 2L, 982L, room1.getId());
		createSelectionRequest(session, 3L, 982L, room1.getId());
		createSelectionRequest(session, 4L, 982L, room1.getId());
		createSelectionRequest(session, 5L, 982L, room1.getId());

		findSelectionRequestByRequester(2L).ifPresent(rq -> rs.revokeRequest(rq, 2L));

		assertThat(session.getRequests()).size().isEqualTo(5);
		assertThatRequestIsStatus(1L, SELECTED);
		assertThatRequestIsStatus(2L, REVOKED);
		assertThatRequestIsStatus(3L, SELECTED);
		assertThatRequestIsStatus(4L, SELECTED);
		assertThatRequestIsStatus(5L, PENDING);
	}

	@Test
	void rejectingSelectedRequestThrowsError() {
		session = setupCapacitySession(lcSessionNow.getId(), LATER, SelectionProcedure.FCFS);
		createSelectionRequest(session, 1L, 982L, room1.getId());
		SelectionRequest selectionRequest = findSelectionRequestByRequester(1L).get();

		assertThrows(AccessDeniedException.class, () -> rs.rejectSelectionRequest(selectionRequest,
				1L,
				"assistant", "student"));

		assertThatRequestIsStatus(1L, SELECTED);
	}

	@Test
	void rejectingSelectionRequestWorksWhenNotSelected() {
		session = setupCapacitySession(lcSessionNow.getId(), LATER, SelectionProcedure.RANDOM);
		createSelectionRequest(session, 1L, 982L, null);
		SelectionRequest selectionRequest = findSelectionRequestByRequester(1L).get();

		rs.rejectSelectionRequest(selectionRequest, 1L, "assistant", "student");

		assertThatRequestIsStatus(1L, REJECTED);
	}

	@Test
	void getOrCreateStudentGroupGroupExists() {
		sgApiMocker.save(new StudentGroupDetailsDTO()
				.members(List.of(new RolePersonLayer1DTO().person(new PersonSummaryDTO().id(2L))))
				.module(new ModuleSummaryDTO().id(3L)));

		assertThat(rs.getOrCreateIndividualStudentGroup(1L, 2L, 3L)).isNotNull();

		verify(sgApi, never()).getAllGroupsInModule(anyLong());
		verify(sgApi, never()).addMemberToGroup(anyLong(), anyLong());
		verify(sgApi, never()).getStudentGroupsById(anyList());
		verify(sgApi, never()).addGroup(any());
	}

	@Test
	void getOrCreateStudentGroupEmptyGroup() {
		when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.just(
				new StudentGroupSummaryDTO().memberUsernames(List.of("nonempty")),
				new StudentGroupSummaryDTO().id(5L).memberUsernames(Collections.emptyList())));
		when(sgApi.addMemberToGroup(anyLong(), anyLong())).thenReturn(Mono.empty());
		when(sgApi.getStudentGroupsById(anyList())).thenReturn(Flux.just(new StudentGroupDetailsDTO()));

		assertThat(rs.getOrCreateIndividualStudentGroup(1L, 2L, 3L)).isNotNull();

		verify(sgApi).getAllGroupsInModule(3L);
		verify(sgApi).addMemberToGroup(5L, 2L);
		verify(sgApi).getStudentGroupsById(List.of(5L));
		verify(sgApi, never()).addGroup(any());
	}

	@Test
	void getOrCreateStudentGroupCreateGroup() {
		when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.just());
		when(sgApi.addGroup(any())).thenReturn(Mono.just(6L));
		when(sgApi.getStudentGroupsById(anyList())).thenReturn(Flux.just(new StudentGroupDetailsDTO()));
		mApiMocker.save(new ModuleDetailsDTO().id(3L).edition(new EditionSummaryDTO()));
		pApiMocker.save(new PersonSummaryDTO().id(2L));

		assertThat(rs.getOrCreateIndividualStudentGroup(lcSessionNow.getId(), 2L, 3L)).isNotNull();

		verify(sgApi).getAllGroupsInModule(3L);
		verify(sgApi, never()).addMemberToGroup(anyLong(), anyLong());
		verify(sgApi).getStudentGroupsById(List.of(6L));
		verify(sgApi).addGroup(any());
	}

	@Test
	void sharedEditionFilterDoesNotCallApiWhenEmptyRequests() {
		assertThat(rs.filterRequestsSharedEditionCheck(new ArrayList<>())).isEmpty();

		verify(asApi, never()).getAssignmentsWithModules(any());

	}

	@Test
	void distributingEmptyRequestsNothingHappens() {
		rs.distributeRequests(List.of(), List.of(),
				Person.builder().id(1L).displayName("Test Person").build(), oopNowSlottedLab1);

		rs.distributeRequests(List.of(), List.of(1L, 2L),
				Person.builder().id(1L).displayName("Test Person").build(), oopNowSlottedLab1);

		verify(rs, never()).forwardRequestToPerson(any(), any(), any(), any());

	}

	@Test
	void onlyPendingRequestsWithCorrectAssignmentsGetDistributed() {
		pApiMocker.save(new PersonSummaryDTO().id(6001L));
		pApiMocker.save(new PersonSummaryDTO().id(6002L));

		doNothing().when(rs).forwardRequestToPerson(any(), any(), any(), any());

		oopNowSlottedLab1.getRequests().addAll(List.of(LabRequest.builder().assignment(1L).build(),
				LabRequest.builder().assignment(1L).build(), LabRequest.builder().assignment(2L).build(),
				LabRequest.builder().assignment(3L).build(), LabRequest.builder().assignment(3L).build()));
		oopNowSlottedLab1.getRequests().get(1).getEventInfo().setStatus(APPROVED);
		oopNowSlottedLab1.getRequests().get(2).getEventInfo().setStatus(PROCESSING);

		rs.distributeRequests(List.of(9801L), List.of(6001L, 6002L), Person.builder().build(),
				oopNowSlottedLab1);

		verify(rs, times(1)).forwardRequestToPerson(eq(oopNowSlottedLab1.getRequests().get(0)), any(), any(),
				any());

	}

	@Test
	void distributingRequestsWithMissingPeopleBehavesAsExpected() {
		pApiMocker.save(new PersonSummaryDTO().id(5001L));

		doNothing().when(rs).forwardRequestToPerson(any(), any(), any(), any());

		oopNowSlottedLab1.getRequests().addAll(List.of(LabRequest.builder().assignment(9801L).build(),
				LabRequest.builder().assignment(9801L).build(),
				LabRequest.builder().assignment(9802L).build(),
				LabRequest.builder().assignment(9803L).build(),
				LabRequest.builder().assignment(9803L).build()));
		oopNowSlottedLab1.getRequests().get(1).getEventInfo().setStatus(APPROVED);
		oopNowSlottedLab1.getRequests().get(2).getEventInfo().setStatus(PENDING);

		rs.distributeRequests(List.of(9801L), List.of(5002L), Person.builder().build(), oopNowSlottedLab1);

		verify(rs, never()).forwardRequestToPerson(
				or(eq(oopNowSlottedLab1.getRequests().get(0)), eq(oopNowSlottedLab1.getRequests().get(1))),
				any(), any(), any());

		rs.distributeRequests(List.of(9801L, 9802L), List.of(5001L, 5002L), Person.builder().build(),
				oopNowSlottedLab1);

		verify(rs, times(2)).forwardRequestToPerson(any(), any(), any(), any());

	}

	@Test
	@WithUserDetails("student200")
	void oopTaGetsOopRequestsOnlyInSharedSession() {
		assertThat(rs.filterRequestsSharedEditionCheck(Arrays.stream(rlOopNowSharedLabRequests).toList()))
				.containsExactly(rlOopNowSharedLabRequests);
	}

	@Test
	void labRequestWithoutEditionFilterYieldsOriginalListThroughEditionFetch() {
		assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests), Set.of()))
				.containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));

		assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests),
				Set.of(oopNow.getId(), rlNow.getId())))
				.containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));
	}

	@Test
	void labRequestWithSingleEditionFilterOnlyYieldsRequestsForThatEdition() {
		assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests),
				Set.of(oopNow.getId()))).containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));
		assertThat(
				rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests), Set.of(rlNow.getId())))
				.isEmpty();
	}

}
