/*
 * 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 org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

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.MockBean;
import org.springframework.security.access.AccessDeniedException;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.cache.RoomCacheManager;
import nl.tudelft.queue.model.Request;
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.SelectionProcedure;
import nl.tudelft.queue.model.events.RequestNotSelectedEvent;
import nl.tudelft.queue.model.events.RequestSelectedEvent;
import nl.tudelft.queue.model.labs.CapacitySession;
import nl.tudelft.queue.repository.CapacitySessionRepository;
import nl.tudelft.queue.repository.RequestEventRepository;
import nl.tudelft.queue.repository.RequestRepository;
import nl.tudelft.queue.repository.SelectionRequestRepository;
import test.labracore.SessionApiMocker;
import test.labracore.StudentGroupApiMocker;
import test.test.TestQueueApplication;

@Transactional
@SpringBootTest(classes = TestQueueApplication.class)
public class CapacitySessionServiceTest {
	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))
			.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 OLD = LocalDateTime.now().minusHours(2L);
	private static final LocalDateTime NOW = LocalDateTime.now().minusMinutes(3L);

	private CapacitySession session;
	private SelectionRequest[] requests;

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

	@Autowired
	private CapacitySessionService css;

	@Autowired
	private CapacitySessionRepository csr;

	@Autowired
	private RequestRepository rr;

	@Autowired
	private RequestEventRepository rer;

	@Autowired
	private SessionApiMocker sApiMocker;

	@Autowired
	private StudentGroupApiMocker sgApiMocker;

	@Autowired
	private SelectionRequestRepository srr;

	@Autowired
	private RequestService rs;

	@MockBean
	private RoomCacheManager rCache;

	@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));

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

		sApiMocker.mock();
		sgApiMocker.mock();

	}

	private void mockIndividualStudentGroup(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 setupRequests(CapacitySession session, int numberOfRequests) {
		requests = new SelectionRequest[numberOfRequests];
		for (int i = 0; i < numberOfRequests; i++) {
			requests[i] = rr.save(SelectionRequest.builder()
					.session(session)
					.room(null)
					.requester((long) i)
					.studentGroup((long) i)
					.build());
			session.getRequests().add(requests[i]);
			mockIndividualStudentGroup(1L, List.of((long) i), (long) i);
		}
	}

	private void setupOldRequests(CapacitySession session, int numberOfRequests, Set<Integer> selection) {
		for (int i = 0; i < numberOfRequests; i++) {
			SelectionRequest request = rr.save(SelectionRequest.builder()
					.session(session)
					.room(null)
					.requester((long) i)
					.studentGroup((long) i)
					.build());
			session.getRequests().add(request);
			mockIndividualStudentGroup(1L, List.of((long) i), (long) i);

			if (selection.contains(i)) {
				request.getEventInfo().getEvents().add(rer.applyAndSave(new RequestSelectedEvent(request)));
			} else {
				request.getEventInfo().getEvents()
						.add(rer.applyAndSave(new RequestNotSelectedEvent(request)));
			}
		}
	}

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

	private void setupRandom() {
		session = setupSession(lcSessionNow.getId(), NOW, SelectionProcedure.RANDOM);
	}

	private void setupWeightedByCount() {
		session = setupSession(lcSessionNow.getId(), NOW, SelectionProcedure.WEIGHTED_BY_COUNT);
	}

	private void setupWeightedByLeastRecent() {
		session = setupSession(lcSessionNow.getId(), NOW, SelectionProcedure.WEIGHTED_BY_LEAST_RECENT);
	}

	private int selectedCount() {
		return selectedRequests().size();
	}

	private List<SelectionRequest> selectedRequests() {
		session = csr.getById(session.getId());
		return session.getRequests()
				.stream().filter(r -> r.getEventInfo().getStatus().isSelected())
				.collect(Collectors.toList());
	}

	private List<SelectionRequest> notSelectedRequests() {
		session = csr.getById(session.getId());
		return session.getRequests()
				.stream().filter(r -> !r.getEventInfo().getStatus().isSelected())
				.collect(Collectors.toList());
	}

	private void assertNoUnselectedRequests() {
		assertThat(
				session.getRequests().stream().filter(r -> !r.getEventInfo().getStatus().hasSelectedState()))
				.isEmpty();
	}

	private void assertSelectedContains(int id) {
		assertThat(selectedRequests()).contains(requests[id]);
	}

	private void assertSelectedContainsNumberOf(Set<Integer> ids, int number) {
		Set<SelectionRequest> shouldSelect = ids.stream().map(id -> requests[id]).collect(Collectors.toSet());
		assertThat(selectedRequests().stream().filter(shouldSelect::contains))
				.size().isEqualTo(number);
	}

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

	@Test
	void individualSelectionLabShouldPickExactly3() {
		setupRandom();
		setupRequests(session, 10);

		css.selectStudentsInCapacityLab();

		assertThat(selectedCount()).isEqualTo(3);
		assertNoUnselectedRequests();
	}

	@Test
	void randomPicksExactly3() {
		setupRandom();
		setupRequests(session, 10);

		css.selectStudentsInCapacityLab();

		assertThat(selectedCount()).isEqualTo(3);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierCountAlwaysPicksUnpicked() {
		setupWeightedByCount();
		setupRequests(session, 10);

		CapacitySession oldSession1 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession1, 10, Set.of(0, 1, 2, 3, 4, 5, 6, 7));

		CapacitySession oldSession2 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession2, 10, Set.of(3, 4, 5, 6, 7, 8));

		// Counts should be: [1, 1, 1, 2, 2, 2, 2, 2, 1, 0], so always select id=9, select 2 out of [0, 1, 2, 8].

		css.selectStudentsInCapacityLab();

		assertSelectedContains(9);
		assertSelectedContainsNumberOf(Set.of(0, 1, 2, 8), 2);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierCountWithEqual0Counts() {
		setupWeightedByCount();
		setupRequests(session, 10);

		// All counts are 0
		css.selectStudentsInCapacityLab();

		assertThat(selectedCount()).isEqualTo(3);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierCountWithEqual1Counts() {
		setupWeightedByCount();
		setupRequests(session, 10);

		CapacitySession oldSession1 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession1, 10, Set.of(0, 1, 2, 3, 4));

		CapacitySession oldSession2 = setupSession(lcSessionOld2.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession2, 10, Set.of(5, 6, 7, 8, 9));

		// Counts are all equal, but one group was later. It is seemingly impossible to reliably test that this does not
		// do least recently selected now, since it is random.
		css.selectStudentsInCapacityLab();

		assertThat(selectedCount()).isEqualTo(3);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierCountIgnoresFCFSLabs() {
		setupWeightedByCount();
		setupRequests(session, 10);

		CapacitySession oldSession1 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession1, 10, Set.of(0, 1, 2, 3, 4));

		CapacitySession oldSession2 = setupSession(lcSessionOld2.getId(), OLD, SelectionProcedure.FCFS);
		setupOldRequests(oldSession2, 10, Set.of(5, 6, 7, 8, 9));

		CapacitySession oldSession3 = setupSession(lcSessionOld2.getId(), OLD, SelectionProcedure.FCFS);
		setupOldRequests(oldSession3, 10, Set.of(5, 6, 7, 8, 9));

		// Counts are all equal, but one group was later. It is seemingly impossible to reliably test that this does not
		// do least recently selected now, since it is random.
		css.selectStudentsInCapacityLab();

		assertSelectedContainsNumberOf(Set.of(5, 6, 7, 8, 9), 3);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierLeastRecentAlwaysPicksUnpicked() {
		setupWeightedByLeastRecent();
		setupRequests(session, 10);

		CapacitySession oldSession1 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession1, 10, Set.of(0, 1, 2, 3, 4, 5, 6, 7, 8));

		// Counts are equal, except for 9, which has never been selected.
		css.selectStudentsInCapacityLab();

		assertSelectedContains(9);
		assertThat(selectedCount()).isEqualTo(3);
		assertNoUnselectedRequests();
	}

	@Test
	void byEarlierLeastRecentAlwaysPicksFromLeastRecentlySelected() {
		setupWeightedByLeastRecent();
		setupRequests(session, 10);

		CapacitySession oldSession1 = setupSession(lcSessionOld1.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession1, 10, Set.of(0, 1, 2, 3, 4));

		CapacitySession oldSession2 = setupSession(lcSessionOld2.getId(), OLD, SelectionProcedure.RANDOM);
		setupOldRequests(oldSession2, 10, Set.of(5, 6, 7, 8, 9));

		// Counts are equal for all, but [0, 1, 2, 3, 4] were all less recently selected.
		css.selectStudentsInCapacityLab();

		assertSelectedContainsNumberOf(Set.of(0, 1, 2, 3, 4), 3);
		assertNoUnselectedRequests();
	}

	@Test
	void selectedRequestsGetARoomAssigned() {
		setupRandom();
		setupRequests(session, 10);

		css.selectStudentsInCapacityLab();

		assertThat(selectedRequests()).allSatisfy(r -> {
			assertThat(r.getRoom()).isEqualTo(room1.getId());
		});
		assertThat(notSelectedRequests()).allSatisfy(r -> {
			assertThat(r.getRoom()).isNull();
		});
	}

	@Test
	void rejectedRequestNeverSelected() {
		setupRandom();
		setupRequests(session, 3);

		SelectionRequest request = findSelectionRequestByRequester(1L).get();
		rs.rejectSelectionRequest(request, 4L, "assitant", "student");

		css.selectStudentsInCapacityLab();

		assertThat(selectedCount()).isEqualTo(2);
	}

	@Test
	void rejectedRequestAfterSelection() {
		setupRandom();
		setupRequests(session, 3);

		SelectionRequest request = findSelectionRequestByRequester(1L).get();

		css.selectStudentsInCapacityLab();

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

		// Request was already selected thus shouldn't change.
		assertThat(selectedCount()).isEqualTo(3);
	}

	@Test
	void resetRequestsToFcfs() {
		setupRandom();
		setupRequests(session, 10);

		css.selectStudentsInCapacityLab();
		session.getCapacitySessionConfig().setProcedure(SelectionProcedure.FCFS);

		css.resetRequests(session);
		var requests = session.getRequests();

		assertThat(selectedRequests()).containsAll(session.getRequests().stream()
				.sorted(Comparator.comparing(Request::getCreatedAt)).limit(3).collect(Collectors.toList()));
		assertThat(selectedRequests()).hasSize(3);
	}
}
