/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2021  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.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.List;
import java.util.Set;

import javax.transaction.Transactional;

import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO;
import nl.tudelft.queue.dto.create.labs.SlottedLabCreateDTO;
import nl.tudelft.queue.dto.create.requests.LabRequestCreateDTO;
import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO;
import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO;
import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO;
import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO;
import nl.tudelft.queue.dto.util.DistributeRequestsDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.SelectionRequest;
import nl.tudelft.queue.model.embeddables.*;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.events.RequestApprovedEvent;
import nl.tudelft.queue.model.events.RequestForwardedToAnyEvent;
import nl.tudelft.queue.model.events.RequestTakenEvent;
import nl.tudelft.queue.model.labs.*;
import nl.tudelft.queue.repository.*;
import nl.tudelft.queue.service.JitsiService;
import nl.tudelft.queue.service.LabService;
import nl.tudelft.queue.service.RequestService;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import test.TestDatabaseLoader;
import test.test.TestQueueApplication;

import com.fasterxml.jackson.databind.ObjectMapper;

@Transactional
@AutoConfigureMockMvc
@SpringBootTest(classes = TestQueueApplication.class)
class LabControllerTest {

	@Autowired
	private TestDatabaseLoader db;

	@Autowired
	private MockMvc mvc;

	@SpyBean
	private QueueSessionRepository qsr;

	@SpyBean
	private LabService ls;

	@SpyBean
	private RequestService rs;

	@SpyBean
	private JitsiService js;

	@Autowired
	private RequestRepository rr;

	@Autowired
	private RequestEventRepository rer;

	@Autowired
	private ExamLabRepository elr;

	@Autowired
	private SlottedLabRepository slr;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private LabRepository lr;

	@Autowired
	private StudentGroupControllerApi sgApi;

	@Captor
	private ArgumentCaptor<QueueSession<LabRequest>> queueSessionArgumentCaptor;

	private PersonSummaryDTO teacher1;
	private PersonSummaryDTO student5;

	private EditionDetailsDTO oopNow;
	private ModuleDetailsDTO labModule;
	private ModuleDetailsDTO lectureModule;
	private AssignmentDetailsDTO assignment1;
	private SessionDetailsDTO session1;
	private RoomDetailsDTO room1;
	private RoomDetailsDTO room2;
	private EditionCollectionDetailsDTO ec1;
	private StudentGroupDetailsDTO sg1;

	private Lab regLab1;
	private Lab expLab1;

	private Lab regHybridLab1;
	private SlottedLab slottedLab1;
	private ExamLab examLabNow;
	private Lab sharedLab;
	private CapacitySession pastLecture;
	private CapacitySession qSession2;

	@BeforeEach
	void setup() {
		db.mockAll();

		when(sApi.addSingleSession(any())).thenReturn(Mono.just(666L));
		when(sApi.addSharedSession(any())).thenReturn(Mono.just(667L));
		when(sApi.patchSession(any(), any())).thenReturn(Mono.just(668L));

		when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.empty());

		teacher1 = db.getTeachers()[1];
		student5 = db.getStudents()[5];

		oopNow = db.getOopNow();
		labModule = db.getOopNowLabsModule();
		lectureModule = db.getOopNowLecturesModule();
		assignment1 = db.getOopNowAssignments()[0];

		session1 = db.getOopNowLcLabs()[0];

		room1 = db.getRoomCz1();
		room2 = db.getRoomPc1();
		ec1 = db.getRlOopNowEc();

		sg1 = db.getOopNowLabGroups()[4];

		regLab1 = db.getOopNowRegularLab1();
		expLab1 = db.getOopNowRegularLab4();
		slottedLab1 = slr.getById(db.getOopNowSlottedLab1().getId());
		examLabNow = elr.getById(db.getOopExamNow().getId());
		sharedLab = db.getRlOopNowSharedLab();
		regHybridLab1 = db.getOopNowRegularHybridLab1();

		pastLecture = db.getOopPastLecture();
		qSession2 = db.getOopLectureRandom();
	}

	@Test
	@WithUserDetails("admin")
	void getCapacitySessionWorks() throws Exception {
		mvc.perform(get("/lab/" + pastLecture.getId()))
				.andExpect(status().is2xxSuccessful())
				.andExpect(view().name("lab/view/capacity"));
	}

	@Test
	@WithUserDetails("student5")
	void getCapacitySessionDisplaysFinishedRequest() throws Exception {
		rr.save(SelectionRequest.builder()
				.eventInfo(RequestEventInfo.builder()
						.status(RequestStatus.PENDING)
						.build())
				.studentGroup(sg1.getId())
				.requester(student5.getId())
				.session(pastLecture)
				.room(null)
				.build());

		mvc.perform(get("/lab/" + pastLecture.getId()))
				.andExpect(status().is2xxSuccessful())
				.andExpect(view().name("lab/view/capacity"))
				.andExpect(model().attribute("requests", Matchers.hasSize(1)));
	}

	@Test
	@WithUserDetails("student61")
	void getCapacitySessionIncludesFinishedRequest() throws Exception {
		mvc.perform(get("/lab/" + pastLecture.getId()))
				.andExpect(status().is2xxSuccessful())
				.andExpect(view().name("lab/view/capacity"))
				.andExpect(model().attributeExists("selectionResult"));
	}

	@Test
	@WithUserDetails("admin")
	void getSlottedLabWorks() throws Exception {
		mvc.perform(get("/lab/" + slottedLab1.getId()))
				.andExpect(status().is2xxSuccessful())
				.andExpect(view().name("lab/view/slotted"));
	}

	@Test
	@WithUserDetails("student54")
	void getRegularLabShowsCurrentRequest() throws Exception {
		mvc.perform(get("/lab/" + regLab1.getId()))
				.andExpect(status().is2xxSuccessful())
				.andExpect(model().attributeExists("current", "currentDto"));
	}

	@Test
	@WithUserDetails("admin")
	void editCapacitiesWorks() throws Exception {
		var ts0 = slottedLab1.getTimeSlots().get(0);
		var ts3 = slottedLab1.getTimeSlots().get(3);

		mvc.perform(post("/lab/{lId}/capacities", slottedLab1.getId()).with(csrf())
				.queryParam("capacities[" + ts0.getId() + "]", "99")
				.queryParam("capacities[" + ts3.getId() + "]", "78327"))
				.andExpect(redirectedUrl("/lab/" + slottedLab1.getId()));

		assertThat(slottedLab1.getTimeSlots().get(0).getCapacity()).isEqualTo(99);
		assertThat(slottedLab1.getTimeSlots().get(3).getCapacity()).isEqualTo(78327);
		assertThat(slottedLab1.getTimeSlots().stream().filter(ts -> !Set.of(ts0, ts3).contains(ts)))
				.allSatisfy(ts -> assertThat(ts.getCapacity())
						.isEqualTo(slottedLab1.getSlottedLabConfig().getCapacity()));
	}

	@Test
	@WithUserDetails("admin")
	void editCapacitiesVerifiesDTO() throws Exception {
		mvc.perform(post("/lab/{lId}/capacities", slottedLab1.getId()).with(csrf())
				.queryParam("capacities[" + examLabNow.getTimeSlots().get(0).getId() + "]", "99"))
				.andExpect(status().is4xxClientError());
	}

	@Test
	@WithUserDetails("admin")
	void editCapacitiesDoesNotChangeCapacityIfInvalid() throws Exception {
		mvc.perform(post("/lab/{lId}/capacities", slottedLab1.getId()).with(csrf())
				.queryParam("capacities[" + slottedLab1.getTimeSlots().get(0).getId() + "]", "99")
				.queryParam("capacities[" + examLabNow.getTimeSlots().get(0).getId() + "]", "99"))
				.andExpect(status().is4xxClientError());

		assertThat(slottedLab1.getTimeSlots().get(0).getCapacity())
				.isEqualTo(slottedLab1.getSlottedLabConfig().getCapacity());
	}

	@Test
	@WithUserDetails("student5")
	void getEnqueueViewWorks() throws Exception {
		mvc.perform(get("/lab/" + regLab1.getId() + "/enqueue"))
				.andExpect(status().isOk())
				.andExpect(view().name("lab/enqueue/lab"))
				.andExpect(model()
						.attributeExists("request", "edition", "rooms", "assignments", "types", "qSession"))
				.andExpect(model().attribute("rType", "lab"));
	}

	@Test
	@WithUserDetails("student5")
	void getEnqueueViewForClosedSelectionIsNotAllowed() throws Exception {
		mvc.perform(get("/lab/" + pastLecture.getId() + "/enqueue"))
				.andExpect(status().isOk())
				.andExpect(view().name("error/403"));
	}

	@Test
	@WithUserDetails("student5")
	void getEnqueueViewForSelectionWorks() throws Exception {
		mvc.perform(get("/lab/" + qSession2.getId() + "/enqueue"))
				.andExpect(status().isOk())
				.andExpect(view().name("lab/enqueue/capacity"))
				.andExpect(model().attributeExists("request", "edition", "qSession", "modules"))
				.andExpect(model().attribute("rType", "capacity"));
	}

	@Test
	@WithUserDetails("student5")
	void getEnqueueViewForFcfsSelectionWorks() throws Exception {
		mvc.perform(get("/lab/" + qSession2.getId() + "/enqueue"))
				.andExpect(status().isOk())
				.andExpect(view().name("lab/enqueue/capacity"))
				.andExpect(model().attributeExists("request", "edition", "qSession", "modules", "rooms"))
				.andExpect(model().attribute("rType", "capacity"));
	}

	@Test
	@WithUserDetails("student5")
	void getQuestionViewWorks() throws Exception {
		mvc.perform(get("/lab/" + regLab1.getId() + "/question/" + assignment1.getId()))
				.andExpect(status().isOk())
				.andExpect(view().name("lab/question"))
				.andExpect(model().attributeExists("edition", "qSession", "assignment"));
	}

	@Test
	@WithUserDetails("student1")
	void getEnqueueViewForExperimentalLabWorks() throws Exception {
		mvc.perform(
				get("/lab/" + regLab1.getId() + "/enqueue/question/" + assignment1.getId() + "?question=Q"))
				.andExpect(status().isOk())
				.andExpect(view().name("lab/enqueue/question"))
				.andExpect(
						model().attributeExists("request", "edition", "qSession", "assignment", "question"));
	}

	@Test
	@WithUserDetails("student5")
	void enqueueingCreatesRequest() throws Exception {
		mvc.perform(post("/lab/" + regLab1.getId() + "/enqueue/lab").with(csrf())
				.queryParam("requestType", "QUESTION")
				.queryParam("comment", "I am a comment")
				.queryParam("question", "This is the question I am asking??????")
				.queryParam("assignment", "" + assignment1.getId())
				.queryParam("room", "" + room1.getId()))
				.andExpect(redirectedUrl("/lab/" + regLab1.getId()));

		verify(rs).createRequest(eq(LabRequestCreateDTO.builder()
				.requestType(RequestType.QUESTION)
				.comment("I am a comment")
				.question("This is the question I am asking??????")
				.assignment(assignment1.getId())
				.room(room1.getId())
				.session(regLab1)
				.build()), eq(student5.getId()), anyBoolean());
	}

	@Test
	@WithUserDetails("student5")
	void enqueWithOnlineModeSelectedWorks() throws Exception {
		mvc.perform(post("/lab/" + regHybridLab1.getId() + "/enqueue/lab").with(csrf())
				.queryParam("requestType", "QUESTION")
				.queryParam("question", "This is the question I am asking??????")
				.queryParam("assignment", "" + assignment1.getId())
				.queryParam("onlineMode", "JITSI"))
				.andExpect(redirectedUrl("/lab/" + regHybridLab1.getId()));

		verify(js, times(1)).createJitsiRoomName(any());

	}

	@Test
	@WithUserDetails("student5")
	void enqueHybridLabWithRoomAndOnlineModeShouldFail() throws Exception {
		mvc.perform(post("/lab/" + regHybridLab1.getId() + "/enqueue/lab").with(csrf())
				.queryParam("requestType", "QUESTION")
				.queryParam("question", "This is the question I am asking??????")
				.queryParam("assignment", "" + assignment1.getId())
				.queryParam("room", "" + room1.getId())
				.queryParam("onlineMode", "JITSI"))
				.andExpect(status().is4xxClientError());

		verify(js, never()).createJitsiRoomName(any());

	}

	@Test
	@WithUserDetails("student5")
	void processingRequestForLabForwardsToRequests() throws Exception {

		LabRequest request = rr.save(LabRequest.builder()
				.requestType(RequestType.QUESTION)
				.comment("I am a comment")
				.question("This is the question I am asking?????")
				.assignment(assignment1.getId())
				.room(room1.getId())
				.session(regLab1)
				.requester(student5.getId())
				.studentGroup(sg1.getId())
				.build());
		rer.applyAndSave(new RequestTakenEvent(request, 0L));

		mvc.perform(get("/lab/" + regLab1.getId()).with(csrf()))
				.andExpect(redirectedUrl("/request/" + request.getId()));
	}

	@Test
	@WithUserDetails("student5")
	void finishedRequestForLabForwardsToRequests() throws Exception {
		LabRequest request = rr.save(LabRequest.builder()
				.requestType(RequestType.QUESTION)
				.comment("I am a comment")
				.question("This is the question I am asking?????")
				.assignment(assignment1.getId())
				.room(room1.getId())
				.session(regLab1)
				.requester(student5.getId())
				.studentGroup(sg1.getId())
				.build());
		rer.applyAndSave(new RequestTakenEvent(request, 0L));
		rer.applyAndSave(new RequestApprovedEvent(request, 0L, ""));

		mvc.perform(get("/lab/" + regLab1.getId()).with(csrf()))
				.andExpect(view().name("lab/view/regular"));
	}

	@Test
	@WithUserDetails("student5")
	void forwardedRequestForLabForwardsToRequests() throws Exception {
		LabRequest request = rr.save(LabRequest.builder()
				.requestType(RequestType.QUESTION)
				.comment("I am a comment")
				.question("This is the question I am asking?????")
				.assignment(assignment1.getId())
				.room(room1.getId())
				.session(regLab1)
				.requester(student5.getId())
				.studentGroup(sg1.getId())
				.build());
		rer.applyAndSave(new RequestTakenEvent(request, 0L));
		rer.applyAndSave(new RequestForwardedToAnyEvent(request, 0L, ""));

		mvc.perform(get("/lab/" + regLab1.getId()).with(csrf()))
				.andExpect(view().name("lab/view/regular"));
	}

	@Test
	@WithUserDetails("student5")
	void enqueueingRedirectsInExperimentalLab() throws Exception {
		mvc.perform(post("/lab/" + expLab1.getId() + "/enqueue/lab").with(csrf())
				.queryParam("requestType", "QUESTION")
				.queryParam("comment", "I am a comment")
				.queryParam("question", "This is the question I am asking??????")
				.queryParam("assignment", "" + assignment1.getId())
				.queryParam("room", "" + room1.getId()))
				.andExpect(redirectedUrl("/lab/" + expLab1.getId() + "/question/" + assignment1.getId()));

		verify(rs, never()).createRequest(any(), anyLong(), anyBoolean());
	}

	@Test
	@WithUserDetails("student5")
	void enqueueingCreatesRequestInCapacityLab() throws Exception {
		mvc.perform(post("/lab/" + qSession2.getId() + "/enqueue/capacity").with(csrf())
				.queryParam("module", "" + lectureModule.getId()))
				.andExpect(redirectedUrl("/lab/" + qSession2.getId()));

		verify(rs).createRequest(eq(SelectionRequestCreateDTO.builder()
				.module(lectureModule.getId())
				.session(qSession2)
				.build()), eq(student5.getId()), anyBoolean());
	}

	@Test
	@WithUserDetails("admin")
	void closeEnqueueChangesEnqueueClosedField() throws Exception {
		mvc.perform(post("/lab/" + regLab1.getId() + "/close-enqueue/true").with(csrf()))
				.andExpect(redirectedUrl("/lab/" + regLab1.getId()));

		assertThat(qsr.getById(regLab1.getId()).getEnqueueClosed()).isTrue();
	}

	@Test
	@WithUserDetails("student50")
	void revokingCallsRevokeRequestAndRedirects() throws Exception {
		mvc.perform(post("/lab/" + regLab1.getId() + "/revoke").with(csrf()))
				.andExpect(redirectedUrl("/lab/" + regLab1.getId()));
	}

	@Test
	@WithUserDetails("student80")
	void cannotRevokeProcessingRequest() throws Exception {
		mvc.perform(post("/lab/" + regLab1.getId() + "/revoke").with(csrf()))
				.andExpect(status().isOk())
				.andExpect(view().name("error/403"));
	}

	@Test
	@WithUserDetails("admin")
	void getLabCreateViewWorks() throws Exception {
		mvc.perform(get("/edition/" + oopNow.getId() + "/lab/create")
				.queryParam("type", "EXAM"))
				.andExpect(model().attributeExists("dto", "edition", "lType", "assignments", "rooms"))
				.andExpect(view().name("lab/create/exam"));
	}

	@Test
	@WithUserDetails("admin")
	void getSharedLabCreateViewWorks() throws Exception {
		mvc.perform(get("/shared-edition/" + ec1.getId() + "/lab/create")
				.queryParam("type", "SLOTTED"))
				.andExpect(model().attributeExists("dto", "ec", "lType", "assignments", "rooms"))
				.andExpect(view().name("lab/create/slotted"));
	}

	@Test
	@WithUserDetails("admin")
	void createCapacityLabCallsCreateLab() throws Exception {
		mvc.perform(
				capacityCreate(post("/edition/" + oopNow.getId() + "/lab/create/capacity")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(CapacitySessionCreateDTO.class), any(),
				eq(LabService.SessionType.REGULAR));
	}

	@Test
	@WithUserDetails("admin")
	void createRegularLabCallsCreateLab() throws Exception {
		mvc.perform(regularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR));
	}

	@Test
	@WithUserDetails("admin")
	void createSlottedLabCallsCreateLab() throws Exception {
		mvc.perform(slottedCreate(post("/edition/" + oopNow.getId() + "/lab/create/slotted")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(SlottedLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR));
	}

	@Test
	@WithUserDetails("admin")
	void createExamLabCallsCreateLab() throws Exception {
		mvc.perform(examCreate(post("/edition/" + oopNow.getId() + "/lab/create/exam")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(ExamLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR));
	}

	@Test
	@WithUserDetails("admin")
	void createSharedRegularLabCallsCreateLab() throws Exception {
		mvc.perform(
				regularCreate(post("/shared-edition/" + ec1.getId() + "/lab/create/regular")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.SHARED));
	}

	@Test
	@WithUserDetails("admin")
	void createSharedSlottedLabCallsCreateLab() throws Exception {
		mvc.perform(
				slottedCreate(post("/shared-edition/" + ec1.getId() + "/lab/create/slotted")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(SlottedLabCreateDTO.class), any(), eq(LabService.SessionType.SHARED));
	}

	@Test
	@WithUserDetails("admin")
	void createSharedExamLabCallsCreateLab() throws Exception {
		mvc.perform(examCreate(post("/shared-edition/" + ec1.getId() + "/lab/create/exam")).with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(ExamLabCreateDTO.class), any(), eq(LabService.SessionType.SHARED));
	}

	@Test
	@WithUserDetails("admin")
	void deleteLabWorks() throws Exception {
		mvc.perform(post("/lab/{id}/remove", regLab1.getId()).with(csrf()))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrlTemplate("/edition/{id}/labs", oopNow.getId()));
		doNothing().when(ls).deleteSession(any());
		verify(ls, times(1)).deleteSession(any(Lab.class));
	}

	@Test
	@WithUserDetails("admin")
	void sharedLabDeleteRedirectsToSharedLabPage() throws Exception {
		mvc.perform(post("/lab/{id}/remove", sharedLab.getId()).with(csrf()))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrlTemplate("/shared-edition/{shared-edition-id}", ec1.getId()));

		verify(ls, times(1)).deleteSession(any(Lab.class));
	}

	@Test
	@WithUserDetails("admin")
	void getLabCopyViewWorks() throws Exception {
		mvc.perform(get("/lab/" + regLab1.getId() + "/copy"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("dto"))
				.andExpect(view().name("lab/create/regular"));
	}

	@Test
	@WithUserDetails("admin")
	void getLabEditViewWorks() throws Exception {
		mvc.perform(get("/lab/" + regLab1.getId() + "/edit"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("dto", "lab", "edition"))
				.andExpect(view().name("lab/edit/regular"));
	}

	@Test
	@WithUserDetails("admin")
	void getExamLabEditViewWorks() throws Exception {
		mvc.perform(get("/lab/" + examLabNow.getId() + "/edit"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("dto", "lab", "edition"))
				.andExpect(view().name("lab/edit/exam"));
	}

	@Test
	@WithUserDetails("admin")
	void getSharedLabEditViewWorks() throws Exception {
		mvc.perform(get("/lab/" + sharedLab.getId() + "/edit"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("dto", "lab", "ec"))
				.andExpect(model().attributeDoesNotExist("edition"))
				.andExpect(view().name("lab/edit/regular"));
	}

	@Test
	@WithUserDetails("admin")
	void updateRegularLabWorks() throws Exception {
		mvc.perform(regularPatch(post("/lab/" + regLab1.getId() + "/edit/regular").with(csrf())))
				.andExpect(redirectedUrl("/lab/" + regLab1.getId()));

		verify(ls).updateSession(isA(RegularLabPatchDTO.class), isA(RegularLab.class));
	}

	@Test
	@WithUserDetails("admin")
	void updateSlottedLabWorks() throws Exception {
		mvc.perform(slottedPatch(post("/lab/" + slottedLab1.getId() + "/edit/slotted").with(csrf())))
				.andExpect(redirectedUrl("/lab/" + slottedLab1.getId()));

		verify(ls).updateSession(isA(SlottedLabPatchDTO.class), isA(SlottedLab.class));
	}

	@Test
	@WithUserDetails("admin")
	void updateExamLabWorks() throws Exception {
		mvc.perform(examPatch(post("/lab/" + examLabNow.getId() + "/edit/exam").with(csrf())))
				.andExpect(redirectedUrl("/lab/" + examLabNow.getId()));

		verify(ls).updateSession(isA(ExamLabPatchDTO.class), isA(ExamLab.class));
	}

	@Test
	@WithUserDetails("admin")
	void sessionWithOnlineModesOnlySucceeds() throws Exception {

		mvc.perform(regularPatch(post("/lab/" + regLab1.getId() + "/edit/regular").with(csrf()))
				.queryParam("onlineModes", "JITSI"))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("/lab/" + regLab1.getId()));

		verify(ls).updateSession(isA(RegularLabPatchDTO.class), isA(RegularLab.class));
	}

	@Test
	@WithUserDetails("admin")
	void createHybridLabWorksNormal() throws Exception {

		mvc.perform(regularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular"))
				.queryParam("onlineModes", "JITSI").with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR));

		verify(qsr, atLeastOnce()).save(queueSessionArgumentCaptor.capture());
		List<QueueSession<LabRequest>> qsList = queueSessionArgumentCaptor.getAllValues();
		var result = qsList.stream().filter(x -> x instanceof Lab).map(x -> (Lab) x)
				.filter(x -> x.getOnlineModes().size() != 0).findFirst();
		assertThat(result.isPresent()).isTrue();
		assertThat(result.get().getOnlineModes()).containsExactlyInAnyOrder(OnlineMode.JITSI);
	}

	@Test
	@WithUserDetails("admin")
	void createInvalidNoRoomNoOnlineModeLabShouldFail() throws Exception {
		mvc.perform(invalidRegularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular")))
				.andExpect(status().is4xxClientError());
	}

	@Test
	@WithUserDetails("admin")
	void createLabWithOnlyOnlineWorks() throws Exception {
		mvc.perform(invalidRegularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular"))
				.queryParam("onlineModes", "JITSI").with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR));

	}

	@Test
	@WithUserDetails("admin")
	void distributingRequestsWithoutFaultWorks() throws Exception {
		DistributeRequestsDTO dto = new DistributeRequestsDTO(2);
		dto.getEditionSelections().get(0).getSelectedAssignments().addAll(List.of(1L, 2L));
		dto.getEditionSelections().get(0).getSelectedAssistants().addAll(List.of(1L, 2L, 10L));
		mvc.perform(post("/lab/" + slottedLab1.getId() + "/distribute")
				.with(csrf())
				.content(new ObjectMapper().writeValueAsString(dto))
				.contentType(MediaType.APPLICATION_JSON))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("/lab/" + slottedLab1.getId()));

	}

	@Test
	@WithUserDetails("admin")
	void distributingRequestsWithoutDtoDoesNotCrash() throws Exception {
		mvc.perform(post("/lab/" + slottedLab1.getId() + "/distribute")
				.with(csrf()))
				.andExpect(status().is3xxRedirection());

		verify(rs, never()).distributeRequests(any(), any(), any(), any());
	}

	@Test
	@WithUserDetails("student50")
	void sendingDistributionRequestWorksWhenAuthorised() throws Exception {
		mvc.perform(post("/lab/" + regLab1.getId() + "/distribute")
				.with(csrf()))
				.andExpect(view().name("error/403"));
	}

	@Test
	@WithUserDetails("student150")
	void redirectToEnrollPageWhenNotEnrolled() throws Exception {
		mvc.perform(get("/lab/{id}", regLab1.getId()))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("/edition/" + session1.getEdition().getId() + "/enrol"));
	}

	@Test
	@WithUserDetails("student5")
	void getLabEditShouldStillThrow403ForStudent() throws Exception {
		mvc.perform(get("/lab/{id}/edit", regLab1.getId()))
				.andExpect(view().name("error/403"));
	}

	@Test
	void getPresentationIsAccessibleForEveryone() throws Exception {
		mvc.perform(get("/present/{id}", regLab1.getId()))
				.andExpect(status().isOk());
	}

	@Test
	@WithUserDetails("admin")
	void getEditPresentation() throws Exception {
		mvc.perform(get("/lab/{id}/presentation/edit", regLab1.getId()))
				.andExpect(status().isOk());
	}

	@Test
	@WithUserDetails("admin")
	void editPresentation() throws Exception {
		mvc.perform(get("/lab/{id}/presentation/edit", regLab1.getId()))
				.andExpect(status().isOk());
		mvc.perform(
				post("/lab/{id}/presentation/edit?showFeedbackReminder=false", regLab1.getId()).with(csrf()))
				.andExpect(status().is3xxRedirection());
		assertThat(lr.getById(regLab1.getId()).getPresentation().getDefaultSlides().getShowFeedbackReminder())
				.isFalse();
	}

	@ParameterizedTest
	@MethodSource(value = "protectedEndpoints")
	void requestWithoutUserDetailsGoesToLogin(MockHttpServletRequestBuilder request) throws Exception {
		mvc.perform(request.with(csrf()))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("http://localhost/login"));
	}

	private static List<MockHttpServletRequestBuilder> protectedEndpoints() {
		return List.of(
				get("/lab/1"),

				post("/lab/1/capacities"),

				get("/lab/1/enqueue"),
				post("/lab/1/enqueue"),
				post("/lab/1/revoke"),
				post("/lab/1/distribute"),

				get("/edition/1/lab/create"),
				get("/shared-edition/1/lab/create"),
				post("/edition/1/lab/create/capacity"),
				post("/edition/1/lab/create/regular"),
				post("/edition/1/lab/create/slotted"),
				post("/edition/1/lab/create/exam"),
				post("/shared-edition/1/lab/create/regular"),
				post("/shared-edition/1/lab/create/slotted"),
				post("/shared-edition/1/lab/create/exam"),

				get("/lab/1/remove"),
				post("/lab/1/remove"),
				get("/lab/1/copy"),

				get("/lab/1/question/1"),
				get("/lab/1/enqueue/question/1"),

				get("/lab/1/edit"),
				post("/lab/1/edit/capacity"),
				post("/lab/1/edit/regular"),
				post("/lab/1/edit/slotted"),
				post("/lab/1/edit/exam"));
	}

	private static MockHttpServletRequestBuilder examPatch(MockHttpServletRequestBuilder req) {
		return req.queryParam("examLabConfig.percentage", "99");
	}

	private static MockHttpServletRequestBuilder slottedPatch(MockHttpServletRequestBuilder req) {
		return req.queryParam("slottedLabConfig.selectionOpensAt", "2026-09-09T01:09");
	}

	private static MockHttpServletRequestBuilder regularPatch(MockHttpServletRequestBuilder req) {
		return req.queryParam("name", "New name");
	}

	private MockHttpServletRequestBuilder capacityCreate(MockHttpServletRequestBuilder req) {
		return req.queryParam("name", "Super Lab")
				.queryParam("slot.opensAt", "2020-09-09T12:09")
				.queryParam("slot.closesAt", "2020-09-10T12:09")
				.queryParam("modules", "" + labModule.getId())
				.queryParam("rooms", "" + room1.getId())
				.queryParam("capacitySessionConfig.enrolmentOpensAt", "2020-08-08T00:00")
				.queryParam("capacitySessionConfig.enrolmentClosesAt", "2020-08-08T01:00")
				.queryParam("capacitySessionConfig.selectionAt", "2020-08-08T02:00")
				.queryParam("capacitySessionConfig.procedure", "FCFS");
	}

	private MockHttpServletRequestBuilder examCreate(MockHttpServletRequestBuilder req) {
		return slottedCreate(req.queryParam("examLabConfig.percentage", "15"));
	}

	private MockHttpServletRequestBuilder slottedCreate(MockHttpServletRequestBuilder req) {
		return regularCreate(req.queryParam("slottedLabConfig.duration", "15")
				.queryParam("slottedLabConfig.capacity", "1")
				.queryParam("slottedLabConfig.selectionOpensAt", "2020-09-09T12:09"));
	}

	private MockHttpServletRequestBuilder regularCreate(MockHttpServletRequestBuilder req) {
		return req.queryParam("name", "Super Lab")
				.queryParam("slot.opensAt", "2020-09-09T12:09")
				.queryParam("slot.closesAt", "2020-09-10T12:09")
				.queryParam("communicationMethod", "STUDENT_VISIT_TA")
				.queryParam("modules", "" + labModule.getId())
				.queryParam("requestTypes['" + assignment1.getId() + "']", "QUESTION")
				.queryParam("rooms", "" + room1.getId())
				.queryParam("eolGracePeriod", "15");
	}

	private MockHttpServletRequestBuilder invalidRegularCreate(MockHttpServletRequestBuilder req) {
		return req.queryParam("name", "Super Lab")
				.queryParam("slot.opensAt", "2020-09-09T12:09")
				.queryParam("slot.closesAt", "2020-09-10T12:09")
				.queryParam("communicationMethod", "STUDENT_VISIT_TA")
				.queryParam("modules", "" + labModule.getId())
				.queryParam("requestTypes['" + assignment1.getId() + "']", "QUESTION")
				.queryParam("eolGracePeriod", "15");
	}
}
