/*
 * 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 java.time.LocalDateTime;
import java.util.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.RoomControllerApi;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.queue.dto.view.statistics.AssistantRatingViewDto;
import nl.tudelft.queue.model.*;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
import nl.tudelft.queue.model.embeddables.RequestEventInfo;
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.RequestStatus;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.events.*;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.model.labs.RegularLab;
import nl.tudelft.queue.model.labs.SlottedLab;
import nl.tudelft.queue.repository.*;
import test.TestDatabaseLoader;
import test.labracore.AssignmentApiMocker;
import test.labracore.PersonApiMocker;
import test.labracore.RoomApiMocker;
import test.labracore.SessionApiMocker;
import test.test.TestQueueApplication;

@Transactional
@SpringBootTest(classes = TestQueueApplication.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class EditionStatusServiceTest {

	@Autowired
	private EditionStatusService ess;

	@Autowired
	private PersonApiMocker pApiMocker;

	@Autowired
	private AssignmentApiMocker aApiMocker;

	@Autowired
	private RoomApiMocker rApiMocker;

	@Autowired
	private SessionApiMocker sApiMocker;

	@Autowired
	private FeedbackRepository fr;

	@Autowired
	private SlottedLabRepository slr;

	@Autowired
	private LabRepository lr;

	@Autowired
	private TimeSlotRepository tsr;

	@Autowired
	private LabRequestRepository rr;

	@Autowired
	private RequestEventRepository rer;

	@Autowired
	private TestDatabaseLoader db;

	@Autowired
	private AssignmentControllerApi aApi;

	@Autowired
	private PersonControllerApi pApi;

	@Autowired
	private RoomControllerApi rApi;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private FeedbackRepository feedbackRepository;

	@Autowired
	private RequestRepository requestRepository;
	private PersonSummaryDTO ta1;
	private PersonSummaryDTO ta2;

	private LabRequest r1;
	private LabRequest r2;
	private LabRequest r3;
	private Lab lab;
	private SlottedLab slottedLab;
	private TimeSlot timeSlot;
	private LabRequest request1;
	private LabRequest request2;
	private LabRequest request3;
	private LabRequest request4;
	private Feedback feedback1;
	private Feedback feedback2;
	private Feedback feedback3;

	private PersonSummaryDTO[] people = {
			new PersonSummaryDTO().id(10L).displayName("G"),
			new PersonSummaryDTO().id(11L).displayName("S"),
			new PersonSummaryDTO().id(12L).displayName("V"),
			new PersonSummaryDTO().id(13L).displayName("M"),
			new PersonSummaryDTO().id(14L).displayName("Ma")
	};

	private AssignmentDetailsDTO[] assignments = {
			new AssignmentDetailsDTO().id(32L).name("A1")
	};
	private RoomDetailsDTO[] rooms = {
			new RoomDetailsDTO().id(32L).name("R1")
	};
	private SessionDetailsDTO[] sessions = {
			new SessionDetailsDTO().id(34L).start(LocalDateTime.of(2023, 1, 1, 0, 0, 0))
					.endTime(LocalDateTime.of(2023, 1, 1, 0, 0, 2))
	};

	@BeforeEach
	void setUp() {

		pApiMocker = new PersonApiMocker(pApi);
		Arrays.asList(people).forEach(pApiMocker::save);
		pApiMocker.mock();

		aApiMocker = new AssignmentApiMocker(aApi);
		Arrays.asList(assignments).forEach(aApiMocker::save);
		aApiMocker.mock();

		rApiMocker = new RoomApiMocker(rApi);
		Arrays.asList(rooms).forEach(rApiMocker::save);
		rApiMocker.mock();

		sApiMocker = new SessionApiMocker(sApi);
		Arrays.asList(sessions).forEach(sApiMocker::save);
		sApiMocker.mock();

		ta1 = db.getStudents()[100];
		ta2 = db.getStudents()[200];

		r1 = new LabRequest();
		r2 = new LabRequest();
		r3 = new LabRequest();

		r1.setEventInfo(new RequestEventInfo());
		r2.setEventInfo(new RequestEventInfo());
		r3.setEventInfo(new RequestEventInfo());

		slottedLab = slr.save(SlottedLab.builder()
				.slottedLabConfig(SlottedLabConfig.builder()
						.selectionOpensAt(LocalDateTime.now().minusHours(1L)).build())
				.session(35L)
				.modules(Set.of(82L))
				.enqueueClosed(false)
				.deletedAt(null)
				.allowedRequests(Set.of(new AllowedRequest(33L, RequestType.QUESTION)))
				.communicationMethod(CommunicationMethod.STUDENT_VISIT_TA)
				.constraints(LabRequestConstraints.builder().build())
				.requests(new ArrayList<>())
				.build());

		timeSlot = new TimeSlot();
		timeSlot.setSlot(new Slot(LocalDateTime.of(2023, 1, 1, 0, 0, 6), LocalDateTime.of(2023, 1,
				1, 0, 2, 10)));
		timeSlot.setLab(slottedLab);
		timeSlot.setCapacity(10);
		timeSlot.setRequests(new HashSet<>());
		tsr.save(timeSlot);

		lab = lr.save(RegularLab.builder()
				.session(34L)
				.modules(Set.of(83L))
				.enqueueClosed(false)
				.deletedAt(null)
				.allowedRequests(Set.of(new AllowedRequest(32L, RequestType.QUESTION)))
				.communicationMethod(CommunicationMethod.STUDENT_VISIT_TA)
				.constraints(LabRequestConstraints.builder().build())
				.requests(new ArrayList<>())
				.build());

		request1 = rr.save(LabRequest.builder()
				.room(32L).assignment(32L).studentGroup(32L).requester(32L).comment("Hello")
				.session(lab)
				.requestType(RequestType.QUESTION)
				.createdAt(LocalDateTime.of(2023, 1, 1, 0, 0, 1))
				.build());
		request1.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request1),
				new RequestTakenEvent(request1, people[1].getId()),
				new RequestApprovedEvent(request1, people[1].getId(), "")));
		for (RequestEvent<?> event : request1.getEventInfo().getEvents()) {
			rer.applyAndSave(event);
		}

		request2 = rr.save(LabRequest.builder()
				.room(32L).assignment(32L).studentGroup(32L).requester(32L).comment("Hello")
				.requestType(RequestType.QUESTION)
				.session(lab)
				.createdAt(LocalDateTime.of(2023, 1, 1, 0, 0, 1))
				.build());
		request2.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request2),
				new RequestTakenEvent(request2, people[0].getId()),
				new RequestForwardedToAnyEvent(request2, people[2].getId(), "Hey"),
				new RequestTakenEvent(request2, people[3].getId()),
				new RequestForwardedToPersonEvent(request2, people[3].getId(), people[4].getId(), ""),
				new RequestApprovedEvent(request2, people[4].getId(), "")));
		for (RequestEvent<?> event : request2.getEventInfo().getEvents()) {
			rer.applyAndSave(event);
		}

		request3 = rr.save(LabRequest.builder()
				.room(32L).assignment(32L).studentGroup(33L).requester(32L).comment("Hello")
				.requestType(RequestType.QUESTION)
				.session(lab)
				.createdAt(LocalDateTime.of(2023, 1, 1, 0, 0, 8))
				.build());
		request3.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request3),
				new RequestTakenEvent(request3, people[1].getId()),
				new RequestApprovedEvent(request3, people[1].getId(), "")));
		for (RequestEvent<?> event : request3.getEventInfo().getEvents()) {
			rer.applyAndSave(event);
		}
		request4 = rr.save(LabRequest.builder()
				.room(32L).assignment(33L).studentGroup(34L).requester(33L).comment("Hello")
				.requestType(RequestType.QUESTION)
				.session(slottedLab)
				.timeSlot(timeSlot)
				.createdAt(LocalDateTime.of(2023, 1, 1, 0, 0, 8))
				.build());
		request4.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request4),
				new RequestTakenEvent(request4, people[1].getId()),
				new RequestApprovedEvent(request4, people[1].getId(), "")));
		for (RequestEvent<?> event : request4.getEventInfo().getEvents()) {
			rer.save(event);
		}

		feedback1 = fr.save(Feedback.builder()
				.id(new Feedback.Id(request1.getId(), people[1].getId()))
				.request(request3)
				.feedback("").rating(4).build());
		feedback2 = fr.save(Feedback.builder()
				.id(new Feedback.Id(request2.getId(), people[4].getId()))
				.request(request2)
				.feedback("").build());

		feedback3 = fr.save(Feedback.builder()
				.id(new Feedback.Id(request1.getId(), people[1].getId()))
				.request(request1)
				.feedback("").rating(2).build());

		// Quick & Dirty fix. Bidirectional association so code works as expected.
		request1.getFeedbacks().add(feedback3);
		request2.getFeedbacks().add(feedback2);
		request3.getFeedbacks().add(feedback1);
		requestRepository.saveAll(List.of(request1, request2, request3));

	}

	@Test
	void averageRatingsPerAssistantNoRequestsTest() {
		List<AssistantRatingViewDto> result = ess.ratingAndRequestsForAssistant(Collections.emptyList());

		assertThat(result).isEmpty();
	}

	@Test
	void countRequestsPerAssistantNoRequestsTest() {
		Map<Long, Long> result = ess.countRequestsPerAssistant(Collections.emptyList());

		assertThat(result).isEmpty();
	}

	@Test
	void countRequestsPerAssistantNoRequestsTakenTest() {
		Map<Long, Long> result = ess.countRequestsPerAssistant(List.of(r1, r2, r3));

		assertThat(result).isEmpty();
	}

	@Test
	void ratingAndRequestsForAssistanNoRequestsTakenTest() {
		List<AssistantRatingViewDto> result = ess.ratingAndRequestsForAssistant(List.of(r1, r2, r3));
		assertThat(result).isEmpty();
	}

	@Test
	void ratingAndRequestsForAssistanOneRatingPerPersonTest() {
		List<AssistantRatingViewDto> result = ess.ratingAndRequestsForAssistant(List.of(request1));
		List<AssistantRatingViewDto> list = new ArrayList<>();
		list.add(new AssistantRatingViewDto(people[1], 1L, 2.0));

		assertThat(result).containsExactlyInAnyOrderElementsOf(list);

	}

	@Test
	void ratingAndRequestsForAssistantMultipleRatingsPerPersonTest() {
		List<AssistantRatingViewDto> result = ess
				.ratingAndRequestsForAssistant(List.of(request1, request2, request3));
		List<AssistantRatingViewDto> list = new ArrayList<>();
		list.add(new AssistantRatingViewDto(people[1], 2L, 3.0));
		list.add(new AssistantRatingViewDto(people[4], 1L, 0.0));
		assertThat(result).containsExactlyInAnyOrderElementsOf(list);
	}

	@Test
	void countRequestsPerAssistantAllByOneTATest() {
		Map<Long, Long> result = ess.countRequestsPerAssistant(List.of(request1, request3));
		assertThat(result).containsExactlyEntriesOf(Map.of(people[1].getId(), 2L));
	}

	@Test
	void countRequestsPerAssistantSomeTakenByBothTAsTest() {
		r1.getEventInfo().setAssignedTo(ta1.getId());
		r3.getEventInfo().setAssignedTo(ta2.getId());

		Map<Long, Long> result = ess.countRequestsPerAssistant(List.of(request1, request2));

		assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of(
				people[1].getId(), 1L, people[4].getId(), 1L));

	}

	@Test
	void countRequestsPerAssistantAllTakenByBothTAsTest() {
		r1.getEventInfo().setAssignedTo(ta2.getId());
		r2.getEventInfo().setAssignedTo(ta1.getId());
		r3.getEventInfo().setAssignedTo(ta1.getId());

		Map<Long, Long> result = ess.countRequestsPerAssistant(List.of(request1, request2, request3));

		assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of(
				people[1].getId(), 2L, people[4].getId(), 1L));

	}

	@Test
	void countWhereTest() {
		Long result = ess.countWhere(List.of(feedback1, feedback2), f -> Objects.equals(4, f.getRating()));
		assertThat(result).isEqualTo(1L);

	}

	@Test
	void countWhereEmptyListTest() {
		Long result = ess.countWhere(new ArrayList<LabRequest>(), r -> r.getFeedbacks().contains(feedback1));

		assertThat(result).isEqualTo(0L);

	}

	@Test
	void getFilteredRequestsTest() {
		List<LabRequest> result = ess.getFilteredRequests(List.of(lab));

		assertThat(result).containsExactlyInAnyOrder(request1, request2, request3);

	}

	@Test
	void getFilteredRequestsBiggerMethodTest() {

		List<LabRequest> result = ess.getFilteredRequests(List.of(lab),
				Arrays.stream(assignments).map(AssignmentDetailsDTO::getId).toList(),
				Arrays.stream(rooms).map(RoomDetailsDTO::getId).toList(),
				List.of(RequestType.QUESTION));

		assertThat(result).containsExactlyInAnyOrder(request1, request2, request3);

	}

	@Test
	void getFilteredRequestsBiggerMethodEmptyTest() {

		List<LabRequest> result = ess.getFilteredRequests(List.of(lab),
				Arrays.stream(assignments).map(AssignmentDetailsDTO::getId).toList(),
				Arrays.stream(rooms).map(RoomDetailsDTO::getId).toList(),
				List.of(RequestType.SUBMISSION));

		assertThat(result).isEmpty();

	}

	@Test
	void countRequestsPerAssignment() {
		Map<String, Long> result = ess.countRequestsPerAssignment(List.of(32L),
				List.of(request1, request2, request3));

		assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of("A1 (#32)", 3L));

	}

	@Test
	void countRequestsPerRoom() {
		Map<String, Long> result = ess.countRequestsPerRoom(List.of(32L),
				List.of(request1, request2, request3));

		assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of("R1", 3L));

	}

	@Test
	void countDistinctAssistantsTest() {
		Long count = ess.countDistinctAssistants(List.of(request1, request3));

		assertThat(count).isEqualTo(1L);

	}

	@Test
	void countDistinctUsersTest() {
		Long count = ess.countDistinctUsers(List.of(request1, request2, request3));

		assertThat(count).isEqualTo(2L);

	}

	@Test
	void mostCountedNameTest() {
		String name = ess.mostCountedName(Map.of("Galya", 4L, "Marina", 3L, "Ada", 1L));

		assertThat(name).isEqualTo("Galya");

	}

	@Test
	void averageProcessingTimeNotHandledTest() {
		Long result = ess.averageProcessingTime(List.of(request1));

		assertThat(result).isEqualTo(0L);

	}

	@Test
	void averageProcessingTimeNoTimeAssignedTest() {
		request1.getEventInfo().setStatus(RequestStatus.APPROVED);
		rr.save(request1);
		Long result = ess.averageProcessingTime(List.of(request1));

		assertThat(result).isEqualTo(0L);

	}

	@Test
	void averageProcessingTimeNoTimeHandledTest() {
		request1.getEventInfo().setStatus(RequestStatus.APPROVED);
		request1.getEventInfo().setLastAssignedAt(LocalDateTime.now());
		rr.save(request1);
		Long result = ess.averageProcessingTime(List.of(request1));

		assertThat(result).isEqualTo(0L);

	}

	@Test
	void averageProcessingTimeTest() {
		request1.getEventInfo().setStatus(RequestStatus.APPROVED);
		request1.getEventInfo().setLastAssignedAt(LocalDateTime.now().minusSeconds(12L));
		request1.getEventInfo().setHandledAt(LocalDateTime.now());
		rr.save(request1);
		Long result = ess.averageProcessingTime(List.of(request1));

		assertThat(result).isEqualTo(12L);

	}

	@Test
	void averageWaitingTimeTest() {
		request1.getEventInfo().setStatus(RequestStatus.APPROVED);
		request1.getEventInfo().setLastAssignedAt(LocalDateTime.of(2023, 1, 1, 0, 0, 2));
		rr.save(request1);
		Long result = ess.averageWaitingTime(List.of(request1));
		assertThat(result).isEqualTo(1L);

	}

	@Test
	void averageWaitingTimeSlottedLabTest() {
		request4.getEventInfo().setStatus(RequestStatus.APPROVED);
		request4.getEventInfo().setLastAssignedAt(LocalDateTime.of(2023, 1, 1, 0, 0, 7));
		rr.save(request4);
		Long result = ess.averageWaitingTime(List.of(request4));
		assertThat(result).isEqualTo(1L);

	}

	@Test
	void createBucketsOverCourseTest() {
		TreeSet<Long> result = ess.createBucketsOverCourse(List.of(lab), 1);

		assertThat(result).containsExactlyInAnyOrder(0L, 2L);

	}

	@Test
	void countCreatedRequestsInBucketsTest() {
		TreeSet<Long> buckets = new TreeSet<>();
		buckets.add(1L);
		buckets.add(3L);
		List<Long> result = ess.countCreatedRequestsInBuckets(buckets, List.of(request1));
		assertThat(result).containsExactly(1L);

	}

	@Test
	void countCreatedRequestsInBucketsEmptyTest() {
		TreeSet<Long> buckets = new TreeSet<>();
		buckets.add(1L);
		List<Long> result = ess.countCreatedRequestsInBuckets(buckets, List.of(request1));
		assertThat(result).isEmpty();

	}

	@Test
	void countCreatedRequestsInBucketsMoreBucketsTest() {
		TreeSet<Long> buckets = new TreeSet<>();
		buckets.add(1L);
		buckets.add(2L);
		buckets.add(3L);
		List<Long> result = ess.countCreatedRequestsInBuckets(buckets, List.of(request1, request2));
		assertThat(result).containsExactly(2L, 0L);

	}

	@Test
	void countHandledRequestsInBucketsTest() {
		TreeSet<Long> buckets = new TreeSet<>();
		buckets.add(1L);
		buckets.add(2L);
		buckets.add(3L);
		request1.getEventInfo().setHandledAt(LocalDateTime.of(2023, 1, 1, 0, 0, 1));
		request2.getEventInfo().setHandledAt(LocalDateTime.of(2023, 1, 1, 0, 0, 2));
		rr.save(request1);
		rr.save(request2);
		List<Long> result = ess.countHandledRequestsInBuckets(buckets, List.of(request1, request2));
		assertThat(result).containsExactly(1L, 1L);

	}

	@Test
	void countOpenRequestsInBucketsTest() {
		TreeSet<Long> buckets = new TreeSet<>();
		buckets.add(1L);
		buckets.add(2L);
		buckets.add(3L);
		request1.getEventInfo().setHandledAt(LocalDateTime.of(2023, 1, 1, 0, 0, 2));
		request2.getEventInfo().setHandledAt(LocalDateTime.of(2023, 1, 1, 0, 0, 2));
		rr.save(request1);
		rr.save(request2);
		List<Long> result = ess.countOpenRequestsInBuckets(buckets, List.of(request1, request2));
		assertThat(result).containsExactly(0L, 0L);

	}

}
