/*
 * 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.model.enums.QueueSessionType.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyIterable;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.RoleControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.csv.EmptyCsvException;
import nl.tudelft.queue.csv.InvalidCsvException;
import nl.tudelft.queue.csv.InvalidCsvValuesException;
import nl.tudelft.queue.csv.UserCsvHelper;
import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
import nl.tudelft.queue.misc.QueueSessionStatus;
import nl.tudelft.queue.model.embeddables.Slot;
import nl.tudelft.queue.model.labs.ExamLab;
import nl.tudelft.queue.model.labs.RegularLab;
import nl.tudelft.queue.model.labs.SlottedLab;
import nl.tudelft.queue.repository.QueueSessionRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import test.test.NoDBTestQueueApplication;

@Transactional
@SpringBootTest(classes = NoDBTestQueueApplication.class)
public class EditionServiceTest {

	@MockBean
	private QueueSessionRepository lr;

	@Autowired
	private EditionService es;

	@Autowired
	private PersonControllerApi personControllerApi;

	@Autowired
	private RoleControllerApi roleControllerApi;

	@Autowired
	private EditionControllerApi editionControllerApi;

	private PersonSummaryDTO person1;
	private PersonSummaryDTO person2;
	private PersonSummaryDTO person3;
	private PersonSummaryDTO person4;
	private PersonSummaryDTO person5;

	private List<PersonSummaryDTO> people;

	private QueueSessionSummaryDTO regularLab;
	private QueueSessionSummaryDTO examLab;
	private QueueSessionSummaryDTO slottedLab;

	private List<QueueSessionSummaryDTO> labs;

	private Slot labSlot;

	private EditionDetailsDTO editionDetailsDTO;

	@BeforeEach
	void setUp() {
		person1 = new PersonSummaryDTO()
				.displayName("Christopher Columbus")
				.number(345)
				.username("cruv");
		person2 = new PersonSummaryDTO()
				.displayName("Rubenov Maliciious")
				.number(6482)
				.username("rmaliciious");
		person3 = new PersonSummaryDTO()
				.displayName("Sarin Justice")
				.number(618)
				.username("hippo5");
		person4 = new PersonSummaryDTO()
				.displayName("Karen McKarenVille")
				.number(42)
				.username("karen42");
		person5 = new PersonSummaryDTO()
				.displayName("Ottomano Coronu")
				.number(55)
				.username("maninthecorona");

		people = List.of(person1, person2, person3, person4, person5);

		LocalDateTime labTime = now();
		labSlot = new Slot(labTime.minusMinutes(5), labTime.plusMinutes(10));

		regularLab = new QueueSessionSummaryDTO(0L, REGULAR, null, "lab", "ReadableName", labSlot, 0, "",
				false, false,
				QueueSessionStatus.OPEN, false);
		examLab = new QueueSessionSummaryDTO(1L, EXAM, null, "lab", "ReadableName", labSlot, 0, "",
				false, false,
				QueueSessionStatus.OPEN, false);
		slottedLab = new QueueSessionSummaryDTO(2L, SLOTTED, null, "lab", "ReadableName", labSlot, 0, "",
				false,
				false,
				QueueSessionStatus.OPEN, false);

		labs = List.of(regularLab, examLab, slottedLab);

		RegularLab regularDetailLab = new RegularLab();
		regularDetailLab.setId(0L);
		regularDetailLab.setModules(Set.of(0L, 2L));
		ExamLab examDetailLab = new ExamLab();
		examDetailLab.setId(1L);
		examDetailLab.setModules(Set.of(0L, 1L));
		SlottedLab slottedDetailLab = new SlottedLab();
		slottedDetailLab.setId(2L);
		slottedDetailLab.setModules(Collections.emptySet());

		when(lr.findAllById(anyIterable()))
				.thenReturn(List.of(regularDetailLab, examDetailLab, slottedDetailLab));
		editionDetailsDTO = new EditionDetailsDTO().id(1L).name("testEdition");
	}

	@Test
	void studentsMatchingFilterChecksDisplayName() {
		assertThat(es.studentsMatchingFilter(people, "en"))
				.containsExactly(person2, person4);
	}

	@Test
	void studentsMatchingFilterChecksStudentNumber() {
		assertThat(es.studentsMatchingFilter(people, "5"))
				.containsExactly(person1, person3, person5);
	}

	@Test
	void studentsMatchingFilterChecksUsername() {
		assertThat(es.studentsMatchingFilter(people, "p"))
				.containsExactly(person1, person3);
	}

	@Test
	void sortLabsSortsOpenBeforeClosed() {
		QueueSessionSummaryDTO openLab = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().minusMinutes(5), now().plusMinutes(10)),
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO closedLab = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().minusMinutes(10), now().minusMinutes(5)),
				0, "", false, false, QueueSessionStatus.OPEN, false);

		assertThat(es.sortLabs(List.of(closedLab, openLab))).containsExactly(openLab, closedLab);
		assertThat(es.sortLabs(List.of(openLab, closedLab))).containsExactly(openLab, closedLab);
	}

	@Test
	void sortLabsSortsPastLabsAfterFuture() {
		QueueSessionSummaryDTO pastLab = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().minusMinutes(15), now().minusMinutes(10)),
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO futureLab = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().plusMinutes(20), now().plusMinutes(100)),
				0, "", false, false, QueueSessionStatus.OPEN, false);

		assertThat(es.sortLabs(List.of(futureLab, pastLab))).containsExactly(futureLab, pastLab);
		assertThat(es.sortLabs(List.of(pastLab, futureLab))).containsExactly(futureLab, pastLab);
	}

	@Test
	void sortLabsSortsOnMinutes() {
		QueueSessionSummaryDTO lab1 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().plusMinutes(5), now().plusMinutes(50)),
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO lab2 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().plusMinutes(10), now().plusMinutes(15)),
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO lab3 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"lab", "ReadableName",
				new Slot(now().plusMinutes(70), now().plusMinutes(90)),
				0, "", false, false, QueueSessionStatus.OPEN, false);

		assertThat(es.sortLabs(List.of(lab2, lab3, lab1))).containsExactly(lab1, lab2, lab3);
	}

	@Test
	void sortLabsSortsOnName() {
		QueueSessionSummaryDTO lab1 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"Alab", "ReadableName",
				labSlot,
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO lab2 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"Flab", "ReadableName",
				labSlot,
				0, "", false, false, QueueSessionStatus.OPEN, false);
		QueueSessionSummaryDTO lab3 = new QueueSessionSummaryDTO(0L,
				REGULAR,
				null,
				"Zlab", "ReadableName",
				labSlot,
				0, "", false, false, QueueSessionStatus.OPEN, false);
		assertThat(es.sortLabs(List.of(lab3, lab2, lab1))).containsExactly(lab1, lab2, lab3);
	}

	@Test
	void filterLabsFiltersOnOneType() {
		assertThat(es.filterLabs(labs, List.of(REGULAR), Collections.emptyList()))
				.containsExactly(regularLab);
	}

	@Test
	void filterLabsFiltersWithMultipleTypes() {
		assertThat(es.filterLabs(labs, List.of(EXAM, SLOTTED), Collections.emptyList()))
				.containsExactlyInAnyOrder(examLab,
						slottedLab);
	}

	@Test
	void filterLabsFiltersOnOneModule() {
		assertThat(es.filterLabs(labs, Collections.emptyList(), List.of(0L)))
				.containsExactlyInAnyOrder(regularLab, examLab);
	}

	@Test
	void filterLabsFiltersOnMultipleModules() {
		assertThat(es.filterLabs(labs, Collections.emptyList(), List.of(1L, 2L)))
				.containsExactlyInAnyOrder(regularLab, examLab);
	}

	@Test
	void filterLabsDoesNotFilterWithEmptyList() {
		assertThat(es.filterLabs(labs, Collections.emptyList(), Collections.emptyList())).isEqualTo(labs);
	}

	@Test
	void addParticipantsCSVFile() throws EmptyCsvException, InvalidCsvException, InvalidCsvValuesException {
		Mono<PersonDetailsDTO> personDetailsDTOMono = Mono.just(new PersonDetailsDTO().id(100L));
		Mono<RoleId> idMono = Mono.just(new RoleId().personId(1L));
		Flux<RolePersonDetailsDTO> personDetailsDTOFlux = Flux.just(new RolePersonDetailsDTO().person(person1)
				.id(new RoleId().personId(100L).editionId(1L)).type(RolePersonDetailsDTO.TypeEnum.TEACHER));

		doReturn(personDetailsDTOMono).when(personControllerApi).getPersonByUsername(any());
		doReturn(idMono).when(roleControllerApi).addRole(any());
		doReturn(personDetailsDTOFlux).when(editionControllerApi).getEditionParticipants(any());

		MockMultipartFile file = new MockMultipartFile(
				"file",
				"test.csv",
				MediaType.TEXT_PLAIN_VALUE,
				"\"netid\", \"role\"\n\"netid1\",\"TA\"".getBytes());

		es.addCourseParticipants(file, editionDetailsDTO);

		verify(personControllerApi).getPersonByUsername("netid1");

		ArgumentCaptor<RoleCreateDTO> captor = ArgumentCaptor.forClass(RoleCreateDTO.class);
		verify(roleControllerApi).addRole(captor.capture());
		RoleCreateDTO captured = captor.getValue();

		assertThat(captured.getType()).isEqualTo(RoleCreateDTO.TypeEnum.TA);
		assertThat(captured.getEdition().getId()).isEqualTo(editionDetailsDTO.getId());
	}

	@Test
	void dontLetTeacherDemoteThemselvesWithCSVFile() {
		Mono<PersonDetailsDTO> personDetailsDTOMono = Mono.just(new PersonDetailsDTO().id(100L));
		//		Mono<Id> idMono = Mono.just(new RoleId().personId(1L));
		Flux<RolePersonDetailsDTO> personDetailsDTOFlux = Flux.just(new RolePersonDetailsDTO()
				.person(person1.id(100L))
				.id(new RoleId().personId(100L).editionId(1L)).type(RolePersonDetailsDTO.TypeEnum.TEACHER));

		doReturn(personDetailsDTOMono).when(personControllerApi).getPersonByUsername(any());
		//		doReturn(idMono).when(roleControllerApi).addRole(any());
		doReturn(personDetailsDTOFlux).when(editionControllerApi).getEditionParticipants(any());

		MockMultipartFile file = new MockMultipartFile(
				"file",
				"test.csv",
				MediaType.TEXT_PLAIN_VALUE,
				"\"netid\", \"role\"\n\"netid1\",\"TA\"".getBytes());

		assertThatThrownBy(() -> {
			es.addCourseParticipants(file, new EditionDetailsDTO().id(1L));
		}).isInstanceOf(InvalidCsvValuesException.class).hasMessage(
				"Import happened but teachers were not demoted as that would leave no teachers");
		verify(roleControllerApi, never()).addRole(any());

	}

	@Test
	void dontLetTeacherDemoteAndUpdateOthersWithCSVFile() throws InvalidCsvException {
		Mono<PersonDetailsDTO> personDetailsDTOMono1 = Mono
				.just(new PersonDetailsDTO().id(11L).username("netid1"));
		Mono<PersonDetailsDTO> personDetailsDTOMono2 = Mono
				.just(new PersonDetailsDTO().id(22L).username("netid2"));
		Mono<RoleId> idMono = Mono.just(new RoleId().personId(1L));

		Flux<RolePersonDetailsDTO> personDetailsDTOFlux = Flux.just(
				new RolePersonDetailsDTO()
						.person(person1.id(11L))
						.id(new RoleId().personId(11L).editionId(1L))
						.type(RolePersonDetailsDTO.TypeEnum.TEACHER),
				new RolePersonDetailsDTO().person(person2.id(22L))
						.id(new RoleId().personId(22L).editionId(1L))
						.type(RolePersonDetailsDTO.TypeEnum.TA));

		doReturn(personDetailsDTOMono1).when(personControllerApi).getPersonByUsername("netid1");
		doReturn(personDetailsDTOMono2).when(personControllerApi).getPersonByUsername("netid2");
		doReturn(idMono).when(roleControllerApi).addRole(any());

		doReturn(personDetailsDTOFlux).when(editionControllerApi).getEditionParticipants(any());

		MockMultipartFile file = new MockMultipartFile(
				"file",
				"test.csv",
				MediaType.TEXT_PLAIN_VALUE,
				"\"netid\", \"role\"\n\"netid1\",\"TA\"\n\"netid2\",\"HEAD_TA\"".getBytes());

		assertThatThrownBy(() -> {
			es.addCourseParticipants(file, new EditionDetailsDTO().id(1L));
		})
				.isInstanceOf(InvalidCsvValuesException.class).hasMessage(
						"Import happened but teachers were not demoted as that would leave no teachers");

		verify(roleControllerApi, never()).addRole(new UserCsvHelper("netid1", "TA")
				.type(RoleCreateDTO.TypeEnum.TA)
				.person(new PersonIdDTO().id(11L))
				.edition(new EditionIdDTO().id(1L)));

		verify(roleControllerApi).addRole(new UserCsvHelper("netid2", "HEAD_TA")
				.type(RoleCreateDTO.TypeEnum.HEAD_TA)
				.person(new PersonIdDTO().id(22L))
				.edition(new EditionIdDTO().id(1L)));

	}

	@Test
	void demoteTeachersIfThereAreTeachersLeftWithCSVFile() throws InvalidCsvException {
		Mono<PersonDetailsDTO> personDetailsDTOMono1 = Mono
				.just(new PersonDetailsDTO().id(101L).username("netid1"));
		Mono<PersonDetailsDTO> personDetailsDTOMono2 = Mono
				.just(new PersonDetailsDTO().id(102L).username("netid2"));
		Mono<RoleId> idMono = Mono.just(new RoleId().personId(1L));
		Flux<RolePersonDetailsDTO> personDetailsDTOFlux = Flux.just(new RolePersonDetailsDTO()
				.person(person1.id(101L))
				.id(new RoleId().personId(101L).editionId(1L)).type(RolePersonDetailsDTO.TypeEnum.TEACHER),
				new RolePersonDetailsDTO()
						.person(person2.id(102L))
						.id(new RoleId().personId(102L).editionId(1L)).type(RolePersonDetailsDTO.TypeEnum.TA),
				new RolePersonDetailsDTO()
						.person(person3.id(103L))
						.id(new RoleId().personId(103L).editionId(1L))
						.type(RolePersonDetailsDTO.TypeEnum.TEACHER));

		doReturn(personDetailsDTOMono1).when(personControllerApi).getPersonByUsername("netid1");
		doReturn(personDetailsDTOMono2).when(personControllerApi).getPersonByUsername("netid2");
		doReturn(idMono).when(roleControllerApi).addRole(any());
		doReturn(personDetailsDTOFlux).when(editionControllerApi).getEditionParticipants(any());

		MockMultipartFile file = new MockMultipartFile(
				"file",
				"test.csv",
				MediaType.TEXT_PLAIN_VALUE,
				"\"netid\", \"role\"\n\"netid1\",\"TA\"\n\"netid2\",\"HEAD_TA\"".getBytes());

		assertThatCode(() -> {
			es.addCourseParticipants(file, editionDetailsDTO);
		}).doesNotThrowAnyException();

		verify(roleControllerApi, atLeastOnce()).addRole(eq(new UserCsvHelper("netid1", "TA")
				.type(RoleCreateDTO.TypeEnum.TA)
				.person(new PersonIdDTO().id(101L))
				.edition(new EditionIdDTO().id(1L))));

		verify(roleControllerApi, atLeastOnce()).addRole(eq(new UserCsvHelper("netid2", "HEAD_TA")
				.type(RoleCreateDTO.TypeEnum.HEAD_TA)
				.person(new PersonIdDTO().id(102L))
				.edition(new EditionIdDTO().id(1L))));
	}

	@Test
	void demoteConsideringNewTeachersWithCSVFile() throws InvalidCsvException {
		Mono<PersonDetailsDTO> personDetailsDTOMono1 = Mono
				.just(new PersonDetailsDTO().id(101L).username("netid1"));
		Mono<PersonDetailsDTO> personDetailsDTOMono2 = Mono
				.just(new PersonDetailsDTO().id(102L).username("netid2"));
		Mono<RoleId> idMono = Mono.just(new RoleId().personId(1L));
		Flux<RolePersonDetailsDTO> personDetailsDTOFlux = Flux.just(new RolePersonDetailsDTO()
				.person(person1.id(101L))
				.id(new RoleId().personId(101L).editionId(1L)).type(RolePersonDetailsDTO.TypeEnum.STUDENT),
				new RolePersonDetailsDTO()
						.person(person2.id(102L))
						.id(new RoleId().personId(102L).editionId(1L))
						.type(RolePersonDetailsDTO.TypeEnum.TEACHER));

		doReturn(personDetailsDTOMono1).when(personControllerApi).getPersonByUsername("netid1");
		doReturn(personDetailsDTOMono2).when(personControllerApi).getPersonByUsername("netid2");
		doReturn(idMono).when(roleControllerApi).addRole(any());
		doReturn(personDetailsDTOFlux).when(editionControllerApi).getEditionParticipants(any());

		MockMultipartFile file = new MockMultipartFile(
				"file",
				"test.csv",
				MediaType.TEXT_PLAIN_VALUE,
				"\"netid\", \"role\"\n\"netid1\",\"TEACHER\"\n\"netid2\",\"TA\"".getBytes());

		assertThatCode(() -> {
			es.addCourseParticipants(file, editionDetailsDTO);
		}).doesNotThrowAnyException();

		verify(roleControllerApi, atLeastOnce()).addRole(eq(new UserCsvHelper("netid1", "TA")
				.type(RoleCreateDTO.TypeEnum.TEACHER)
				.person(new PersonIdDTO().id(101L))
				.edition(new EditionIdDTO().id(1L))));

		verify(roleControllerApi, atLeastOnce()).addRole(eq(new UserCsvHelper("netid2", "TEACHER")
				.type(RoleCreateDTO.TypeEnum.TA)
				.person(new PersonIdDTO().id(102L))
				.edition(new EditionIdDTO().id(1L))));
	}
}
