/*
 * 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 java.time.LocalDateTime.now;
import static nl.tudelft.queue.misc.QueueSessionStatus.*;
import static nl.tudelft.queue.service.LabService.SessionType.REGULAR;
import static nl.tudelft.queue.service.LabService.SessionType.SHARED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.modelmapper.ModelMapper;
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.core.io.Resource;
import org.springframework.ui.Model;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO;
import nl.tudelft.queue.dto.create.embeddables.CapacitySessionConfigCreateDTO;
import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO;
import nl.tudelft.queue.dto.create.embeddables.SlottedLabConfigCreateDTO;
import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
import nl.tudelft.queue.dto.create.labs.LabCreateDTO;
import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO;
import nl.tudelft.queue.dto.create.labs.SlottedLabCreateDTO;
import nl.tudelft.queue.dto.patch.LabPatchDTO;
import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.embeddables.Slot;
import nl.tudelft.queue.model.embeddables.SlottedLabConfig;
import nl.tudelft.queue.model.enums.CommunicationMethod;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.enums.SelectionProcedure;
import nl.tudelft.queue.model.labs.*;
import nl.tudelft.queue.repository.ExamLabRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;
import reactor.core.publisher.Mono;
import test.TestDatabaseLoader;
import test.labracore.EditionApiMocker;
import test.labracore.EditionCollectionApiMocker;
import test.labracore.PersonApiMocker;
import test.labracore.SessionApiMocker;
import test.test.TestQueueApplication;

@Transactional
@SpringBootTest(classes = TestQueueApplication.class)
public class LabServiceTest {
	@Autowired
	private ModelMapper mapper;

	@Autowired
	private LabService ls;

	@Autowired
	private LabStatusService lss;

	@SpyBean
	private TimeSlotService tss;

	@Autowired
	private QueueSessionRepository lr;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private EditionApiMocker eApiMocker;

	@Autowired
	private EditionCollectionApiMocker ecApiMocker;

	@Autowired
	private SessionApiMocker sApiMocker;

	@Autowired
	private ExamLabRepository examLabRepository;

	@Autowired
	private TestDatabaseLoader db;

	@Autowired
	private PersonApiMocker personApiMocker;

	@Autowired
	private SessionService sessionService;

	@SpyBean
	private QueueSessionRepository qsr;

	private SessionDetailsDTO session1;
	private SessionDetailsDTO session2;
	private SessionDetailsDTO session3;

	private SessionDetailsDTO session4;
	private EditionDetailsDTO edition1;
	private EditionCollectionDetailsDTO ec1;
	private ModuleDetailsDTO module1;
	private RoomDetailsDTO room1;
	private AssignmentDetailsDTO assignment1;

	private RegularLab lab1;
	private SlottedLab lab2;

	private RegularLab lab4;

	private LabCreateDTO<RegularLab> createDTO;
	private LabPatchDTO<RegularLab> labPatchDTO;

	private LabPatchDTO<RegularLab> addOnlineModesPatchDTO;

	private Model model;

	private RoleDetailsDTO student1;

	@BeforeEach
	void setUp() {
		model = mock(Model.class);

		eApiMocker.mock();
		ecApiMocker.mock();
		sApiMocker.mock();
		personApiMocker.mock();

		session1 = db.getOopNowLcSessionYesterday();
		session2 = db.getRlOopSession();
		session3 = db.getOopNowLcSessionNow();

		edition1 = db.getOopNow();
		ec1 = db.getRlOopNowEc();
		module1 = db.getOopNowLabsModule();
		assignment1 = db.getOopNowAssignments()[0];
		room1 = db.getRoomPc1();

		lab1 = RegularLab.builder()
				.communicationMethod(CommunicationMethod.STUDENT_VISIT_TA)
				.session(session1.getId())
				.id(420L)
				.allowedRequests(Set.of(new AllowedRequest(assignment1.getId(), RequestType.QUESTION)))
				.build();
		lab2 = SlottedLab.builder()
				.slottedLabConfig(SlottedLabConfig.builder()
						.selectionOpensAt(LocalDateTime.now().minusDays(1L))
						.build())
				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
				.session(session2.getId())
				.allowedRequests(Set.of(new AllowedRequest(assignment1.getId(), RequestType.QUESTION)))
				.build();

		createDTO = RegularLabCreateDTO.builder()
				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
				.modules(Set.of(module1.getId()))
				.name("Lab 1")
				.eolGracePeriod(15)
				.requestTypes(Map.of(
						assignment1.getId(), Set.of(RequestType.QUESTION)))
				.rooms(Set.of(room1.getId()))
				.slot(Slot.builder()
						.opensAt(LocalDateTime.now())
						.closesAt(LocalDateTime.now().plusHours(1L))
						.build())
				.constraints(LabRequestConstraintsCreateDTO.builder()
						.clusterConstraint(ClusterConstraintCreateDTO.builder().clusters(Set.of(3L)).build())
						.build())
				.build();

		labPatchDTO = RegularLabPatchDTO.builder()
				.communicationMethod(CommunicationMethod.STUDENT_VISIT_TA)
				.slot(Slot.builder()
						.opensAt(LocalDateTime.now())
						.closesAt(LocalDateTime.now().plusHours(1))
						.build())
				.build();
		addOnlineModesPatchDTO = RegularLabPatchDTO.builder()
				.onlineModes(Set.of(OnlineMode.JITSI)).build();

		student1 = db.getOopNowStudents()[0];

	}

	@Test
	void setOrganizationInModelSetsEdition() {
		ls.setOrganizationInModel(session1, model);

		verify(model).addAttribute(eq("edition"), any());
	}

	@Test
	void setOrganizationInModelDoesNotSetEditionCollection() {
		ls.setOrganizationInModel(session1, model);

		verify(model).addAttribute("ec", null);
	}

	@Test
	void setOrganizationInModelSession2SetsEditionCollection() {
		ls.setOrganizationInModel(session2, model);

		verify(model).addAttribute(eq("ec"), any());
	}

	@Test
	void setOrganizationInModelSession2DoesNotSetEdition() {
		ls.setOrganizationInModel(session2, model);

		verify(model).addAttribute("edition", null);
	}

	@Test
	void containsRoomFindsExistingRoom() {
		assertThat(ls.containsRoom(session1, room1.getId())).isTrue();
	}

	@Test
	void containsRoomDoesNotFindNonExistentRoom() {
		assertThat(ls.containsRoom(session1, 7831237L)).isFalse();
	}

	@Test
	void containsAssignmentFindsExistingAssignment() {
		assertThat(ls.containsAssignment(lab1, assignment1.getId())).isTrue();
	}

	@Test
	void containsAssignmentDoesNotFindNonExistentAssignment() {
		assertThat(ls.containsAssignment(lab1, 89264L)).isFalse();
	}

	@Test
	void containsAllowedRequestFindsExisting() {
		assertThat(ls.containsAllowedRequest(lab1, assignment1.getId(), RequestType.QUESTION)).isTrue();
	}

	@Test
	void containsAllowedRequestNonExistingType() {
		assertThat(ls.containsAllowedRequest(lab1, assignment1.getId(), RequestType.SUBMISSION)).isFalse();
	}

	@Test
	void containsAllowedRequestNonExistingSubmission() {
		assertThat(ls.containsAllowedRequest(lab1, 78323L, RequestType.QUESTION)).isFalse();
	}

	@Test
	void isOpenToEnqueueingShouldBeFalseForOverDateLab() {
		assertThat(lss.getStatus(lab1)).isEqualTo(FINISHED_SESSION);
	}

	@Test
	void isOpenToEnqueueingShouldBeTrueForCurrentLab() {
		lab1.setSession(session3.getId());

		assertThat(lss.getStatus(lab1)).isEqualTo(OPEN);
	}

	@Test
	void isOpenToEnqueueingShouldCheckForEnqueueClosed() {
		lab1.setEnqueueClosed(true);

		assertThat(lss.getStatus(lab1)).isEqualTo(GENERIC_CLOSED);
	}

	@Test
	void isOpenToEnqueueingShouldCheckForSlotSelectionDateBefore() {
		lab2.getSlottedLabConfig().setSelectionOpensAt(LocalDateTime.now().minusDays(1L));

		assertThat(lss.getStatus(lab2)).isEqualTo(OPEN);
	}

	@Test
	void isOpenToEnqueueingShouldCheckForSlotSelectionDateAfter() {
		lab2.getSlottedLabConfig().setSelectionOpensAt(LocalDateTime.now().plusMinutes(30L));

		assertThat(lss.getStatus(lab2)).isEqualTo(SLOT_SELECTION_CLOSED);
	}

	@Test
	void isOpenToEnqueueingForSlottedLabShouldCheckEnqueueClosed() {
		lab2.setEnqueueClosed(true);
		lab2.getSlottedLabConfig().setSelectionOpensAt(LocalDateTime.now().minusDays(1L));

		assertThat(lss.getStatus(lab2)).isEqualTo(GENERIC_CLOSED);
	}

	@Test
	void createLabSavesANewLab() {
		when(sApi.addSingleSession(any())).thenReturn(Mono.just(5372L));
		when(sApi.addSharedSession(any())).thenReturn(Mono.just(73298L));

		Lab created = ls.createSessions(createDTO, 7832L, REGULAR).get(0);

		assertThat(lr.findById(created.getId())).isPresent().hasValue(created);
	}

	@Test
	void createLabSavesALabUnderRegularSessionId() {
		when(sApi.addSingleSession(any())).thenReturn(Mono.just(5372L));
		when(sApi.addSharedSession(any())).thenReturn(Mono.just(73298L));

		Lab created = ls.createSessions(createDTO, 7832L, REGULAR).get(0);

		assertThat(created.getSession()).isEqualTo(5372L);
	}

	@Test
	void createLabSavesALabUnderSharedSessionId() {
		when(sApi.addSingleSession(any())).thenReturn(Mono.just(5372L));
		when(sApi.addSharedSession(any())).thenReturn(Mono.just(73298L));

		Lab created = ls.createSessions(createDTO, 7832L, SHARED).get(0);

		assertThat(created.getSession()).isEqualTo(73298L);
	}

	@Test
	void createLabSavesConstraints() {
		when(sApi.addSingleSession(any())).thenReturn(Mono.just(5372L));
		when(sApi.addSharedSession(any())).thenReturn(Mono.just(73298L));

		Lab created = ls.createSessions(createDTO, 7832L, REGULAR).get(0);

		assertThat(created.getConstraints().getClusterConstraint()).isNotNull();
		assertThat(created.getConstraints().getModuleDivisionConstraint()).isNull();
	}

	@Test
	void createSlottedLabWithRepeatCreatesNLabs() {
		AtomicLong sessionId = new AtomicLong(473843L);

		when(sApi.addSingleSession(any())).thenReturn(Mono.fromCallable(sessionId::getAndIncrement));
		when(sApi.addSharedSession(any())).thenReturn(Mono.fromCallable(sessionId::getAndIncrement));

		var now = LocalDateTime.now();

		var slottedCreateDTO = mapper.map(createDTO, SlottedLabCreateDTO.class);
		slottedCreateDTO.setSlottedLabConfig(SlottedLabConfigCreateDTO.builder()
				.selectionOpensAt(now.plusWeeks(0))
				.build());
		slottedCreateDTO.setRepeatForXWeeks(5);

		List<SlottedLab> created = ls.createSessions(slottedCreateDTO, 2324L, REGULAR);

		assertThat(created).size().isEqualTo(6);
		assertThat(created.get(0).getSlottedLabConfig().getSelectionOpensAt())
				.isEqualTo(now.plusWeeks(0));
		assertThat(created.get(4).getSlottedLabConfig().getSelectionOpensAt())
				.isEqualTo(now.plusWeeks(4));
	}

	@Test
	void createCapacitySessionWithRepeatCreatesNLabs() {
		AtomicLong sessionId = new AtomicLong(473843L);

		when(sApi.addSingleSession(any())).thenReturn(Mono.fromCallable(sessionId::getAndIncrement));
		when(sApi.addSharedSession(any())).thenReturn(Mono.fromCallable(sessionId::getAndIncrement));

		var now = LocalDateTime.now();

		var capacityCreateDTO = mapper.map(createDTO, CapacitySessionCreateDTO.class);
		capacityCreateDTO.setCapacitySessionConfig(CapacitySessionConfigCreateDTO.builder()
				.enrolmentOpensAt(now.minusDays(5))
				.enrolmentClosesAt(now.minusDays(1))
				.selectionAt(now)
				.procedure(SelectionProcedure.WEIGHTED_BY_LEAST_RECENT)
				.build());
		capacityCreateDTO.setRepeatForXWeeks(5);

		List<CapacitySession> created = ls.createSessions(capacityCreateDTO, 2324L, REGULAR);

		assertThat(created).size().isEqualTo(6);
		assertThat(created.get(4).getCapacitySessionConfig()).satisfies(config -> {
			assertThat(config.getEnrolmentOpensAt()).isEqualTo(now.minusDays(5).plusWeeks(4));
			assertThat(config.getEnrolmentClosesAt()).isEqualTo(now.minusDays(1).plusWeeks(4));
			assertThat(config.getSelectionAt()).isEqualTo(now.plusWeeks(4));
		});
	}

	@Test
	void updateLabSendsAPatchRequest() {
		when(sApi.patchSession(any(), any())).thenReturn(Mono.empty());

		ls.updateSession(labPatchDTO, lab1);

		verify(sApi).patchSession(any(), any());
	}

	@Test
	void updateLabChangesInMemoryLabObject() {
		when(sApi.patchSession(any(), any())).thenReturn(Mono.empty());

		ls.updateSession(labPatchDTO, lab1);

		assertThat(lab1.getCommunicationMethod()).isEqualTo(CommunicationMethod.STUDENT_VISIT_TA);
	}

	@Test
	void updateLabToOnlyhaveOnlineModesWorks() {
		when(sApi.patchSession(any(), any())).thenReturn(Mono.empty());

		ls.updateSession(addOnlineModesPatchDTO, lab1);

		assertThat(lab1.getOnlineModes()).containsExactly(OnlineMode.JITSI);
	}

	@Test
	void updateLabIncludesAssignmentsIfNonEmpty() {
		when(sApi.patchSession(any(), any())).thenReturn(Mono.empty());

		labPatchDTO.setRequestTypes(Map.of(7832L, Set.of(RequestType.QUESTION)));

		ls.updateSession(labPatchDTO, lab1);

		verify(sApi).patchSession(eq(new SessionPatchDTO()
				.name(labPatchDTO.getName())
				.start(labPatchDTO.getSlot().getOpensAt())
				.end(labPatchDTO.getSlot().getClosesAt())
				.assignments(Set.of(new AssignmentIdDTO().id(7832L)))
				.rooms(null)), any());
	}

	@Test
	void updateLabIncludesRoomsIfNonEmpty() {
		when(sApi.patchSession(any(), any())).thenReturn(Mono.empty());

		labPatchDTO.setRooms(Set.of(879342L));

		ls.updateSession(labPatchDTO, lab1);

		verify(sApi).patchSession(eq(new SessionPatchDTO()
				.name(labPatchDTO.getName())
				.start(labPatchDTO.getSlot().getOpensAt())
				.end(labPatchDTO.getSlot().getClosesAt())
				.assignments(null)
				.rooms(Set.of(new RoomIdDTO().id(879342L)))), any());
	}

	@Test
	void deleteLabWorks() {
		long numLabs = qsr.count();
		long sessionId = db.getOopNowRegularLab1().getSession();
		sessionService.deleteQueueSessionByCoreId(sessionId);
		verify(qsr, times(1)).deleteAllBySession(sessionId);
		assertThat(qsr.count()).isEqualTo(numLabs - 1);
	}

	@Test
	void getOnlineModesInLabSession() {
		var testLab = db.getOopNowRegularHybridLab1();
		var result = ls.getOnlineModesInLabSession(testLab.getSession());
		assertThat(result).containsExactlyInAnyOrder(OnlineMode.JITSI);
	}

	@Test
	void illegalFileName() throws IOException {
		setupSessionWithIllegalName();

		Resource res = ls.sessionToCsv(lab4);

		String expectedFileName = "session-bla-";
		assertThat(res.getFile().getPath()).contains(expectedFileName);
	}

	private void setupSessionWithIllegalName() {
		session4 = new SessionDetailsDTO()
				.id(42L)
				.name("../../../bla")
				.start(LocalDateTime.now().plusHours(1))
				.endTime(LocalDateTime.now().plusDays(6))
				.description("Session with illegal name");
		sApiMocker.save(session4);

		lab4 = RegularLab.builder()
				.communicationMethod(CommunicationMethod.STUDENT_VISIT_TA)
				.session(session4.getId())
				.allowedRequests(Set.of(new AllowedRequest(assignment1.getId(), RequestType.QUESTION)))
				.build();
	}

}
