/*
 * 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.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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.PersonControllerApi;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.queue.dto.patch.FeedbackPatchDTO;
import nl.tudelft.queue.model.Feedback;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.RequestEvent;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
import nl.tudelft.queue.model.enums.CommunicationMethod;
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.repository.FeedbackRepository;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.repository.LabRequestRepository;
import nl.tudelft.queue.repository.RequestEventRepository;
import test.labracore.PersonApiMocker;
import test.test.TestQueueApplication;

@Transactional
@SpringBootTest(classes = TestQueueApplication.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FeedbackServiceTest {
	@Autowired
	private FeedbackRepository fr;

	@Autowired
	private LabRepository lr;

	@Autowired
	private LabRequestRepository rr;

	@Autowired
	private RequestEventRepository rer;

	@Autowired
	private FeedbackService fs;

	@Autowired
	private PersonControllerApi pApi;

	@Autowired
	private PersonApiMocker pApiMocker;

	private Lab lab;

	private LabRequest request1;
	private LabRequest request2;
	private LabRequest request3;

	private Feedback feedback1;
	private Feedback feedback2;
	private Feedback feedback3;
	private Feedback feedback4;
	private Feedback feedback5;

	private PersonSummaryDTO[] people = {
			new PersonSummaryDTO().id(0L),
			new PersonSummaryDTO().id(1L).displayName("bla"),
			new PersonSummaryDTO().id(2L),
			new PersonSummaryDTO().id(3L),
			new PersonSummaryDTO().id(4L).displayName("bla"),
			new PersonSummaryDTO().id(5L),
			new PersonSummaryDTO().id(6L),
			new PersonSummaryDTO().id(7L),
			new PersonSummaryDTO().id(8L)
	};

	@BeforeEach
	void setUp() {
		pApiMocker = new PersonApiMocker(pApi);

		Arrays.asList(people).forEach(pApiMocker::save);
		pApiMocker.mock();

		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)
				.build());
		request1.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request1),
				new RequestTakenEvent(request1, people[0].getId()),
				new RequestForwardedToAnyEvent(request1, people[0].getId(), "Hey"),
				new RequestTakenEvent(request1, people[1].getId()),
				new RequestApprovedEvent(request1, people[1].getId(), "")));

		request2 = rr.save(LabRequest.builder()
				.room(32L).assignment(32L).studentGroup(32L).requester(32L).comment("Hello")
				.requestType(RequestType.QUESTION)
				.session(lab)
				.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(), "")));

		request3 = rr.save(LabRequest.builder()
				.room(32L).assignment(32L).studentGroup(32L).requester(32L).comment("Hello")
				.requestType(RequestType.QUESTION)
				.session(lab)
				.build());

		request3.getEventInfo().getEvents().addAll(List.of(
				new RequestCreatedEvent(request3),
				new RequestTakenEvent(request3, people[0].getId()),
				new StudentNotFoundEvent(request3, people[0].getId()),
				new RequestTakenEvent(request3, people[1].getId()),
				new RequestForwardedToAnyEvent(request3, people[1].getId(), "Hey"),
				new RequestTakenEvent(request3, people[2].getId()),
				new RequestApprovedEvent(request3, people[2].getId(), "Approved"),
				new RequestTakenEvent(request3, people[3].getId()),
				new StudentNotFoundEvent(request3, people[3].getId())));

		for (RequestEvent<?> event : Stream.of(request1, request2, request3)
				.flatMap(rq -> rq.getEventInfo().getEvents().stream()).toList()) {
			rer.applyAndSave(event);
		}

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

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

		feedback4 = fr.save(Feedback.builder()
				.id(new Feedback.Id(request2.getId(), people[2].getId()))
				.request(request2)
				.feedback("Meh").rating(3).build());

		feedback5 = fr.save(Feedback.builder()
				.id(new Feedback.Id(request2.getId(), people[3].getId()))
				.request(request2)
				.feedback("Meh").build());

		// Quick & Dirty fix. Bidirectional association so code works as expected.
		request1.getFeedbacks().addAll(List.of(feedback1, feedback3));
		request2.getFeedbacks().addAll(List.of(feedback2, feedback4, feedback5));

	}

	@Test
	void assistantsInvolvedInRequestShouldIncludeAllAssistants() {
		assertThat(fs.assistantsInvolvedInRequest(request1.getId()))
				.containsExactlyInAnyOrder(people[1], people[0]);
	}

	@Test
	void assistantsInvolvedInRequestShouldIncludeAllInChain() {
		assertThat(fs.assistantsInvolvedInRequest(request2.getId()))
				.containsExactlyInAnyOrder(people[4], people[3], people[2], people[0]);
	}

	@Test
	void requestsDoNotHaveAssistantsInvolvedThatCouldNotFindStudent() {
		assertThat(fs.assistantsInvolvedInRequest(request3.getId()))
				.containsExactlyElementsOf(List.of(people[2], people[1]));

	}

	@Test
	void countRatingsWorks() {
		var p0FeedBacks = fr.findByAssistant(people[0].getId());
		var p1FeedBacks = fr.findByAssistant(people[1].getId());
		var p2FeedBacks = fr.findByAssistant(people[2].getId());
		var p3FeedBacks = fr.findByAssistant(people[3].getId());
		var p4FeedBacks = fr.findByAssistant(people[4].getId());

		assertThat(fs.countRatings(p0FeedBacks))
				.isEqualTo(List.of(0, 0, 0, 2, 0));
		assertThat(fs.countRatings(p1FeedBacks))
				.isEqualTo(List.of(0, 1, 0, 0, 0));
		assertThat(fs.countRatings(p2FeedBacks))
				.isEqualTo(List.of(0, 0, 1, 0, 0));
		assertThat(fs.countRatings(p3FeedBacks))
				.isEqualTo(List.of(0, 0, 0, 0, 0));
		assertThat(fs.countRatings(p4FeedBacks))
				.isEqualTo(List.of(0, 0, 0, 0, 0));
	}

	@Test
	void countRatingsDoesNotConsiderRatingsFeedbacksWithNoRating() {
		Feedback f1 = Feedback.builder().feedback("Good TA").build();
		Feedback f2 = Feedback.builder().feedback("Bad TA").rating(1).build();
		assertThat(fs.countRatings(List.of(f1, f2))).isEqualTo(List.of(1, 0, 0, 0, 0));

	}

	@Test
	void updateFeedbackCreatesNewFeedbackWhenNeeded() {
		assertThat(fr.findByAssistant(people[4].getId())).isEmpty();

		fs.updateFeedback(request2, people[4].getId(), FeedbackPatchDTO.builder()
				.feedback("Hello")
				.rating(5)
				.build());

		assertThat(fr.findByAssistant(people[4].getId())).isNotEmpty();
	}

	@Test
	void updateFeedbackChangesExistingFeedback() {
		fs.updateFeedback(request2, people[3].getId(), FeedbackPatchDTO.builder()
				.feedback("New feedback")
				.rating(3)
				.build());

		assertThat(fr.findById(feedback5.getId()).get().getRating())
				.isEqualTo(3);
		assertThat(fr.findById(feedback5.getId()).get().getFeedback())
				.isEqualTo("New feedback");
	}

	@Test
	void updateFeedbackDoesNotAllowEmptyFeedbackCreation() {
		assertThatThrownBy(
				() -> fs.updateFeedback(request2, people[4].getId(), FeedbackPatchDTO.builder().build()));
	}

	@Test
	void testAvgRatingOnFeedbackReturnsZeroWhenNoRequestsAreProvided() {
		assertThat(fs.getAvgStarRating(people[0].getId(), new ArrayList<>())).isEqualTo(0.0d);
	}

	@Test
	void testAvgRatingOnFeedbackDoesNotIncludeAssistantsWhoWereNotPresentInTheRequest() {
		assertThat(fs.getAvgStarRating(433223L, List.of(request1, request2, request3))).isEqualTo(0.0);
	}

	@Test
	void testAvgRatingOnFeedbackReturnsTheCorrectResult() {
		feedback1.setRating(1);
		assertThat(fs.getAvgStarRating(people[0].getId(), List.of(request1, request2))).isEqualTo(2.5d);
	}

	/**
	 * Simulates the people cache manager by getting the list of people with the given ids from a local array.
	 *
	 * @param  ids The ids of the people to lookup.
	 * @return     The list of {@link PersonSummaryDTO} objects representing the people to lookup.
	 */
	private List<PersonSummaryDTO> getPeople(Stream<Long> ids) {
		return ids
				.map(id -> people[id.intValue()])
				.collect(Collectors.toList());
	}

	/**
	 * Simulates the people cache manager by getting the list of people with the given ids from a local array.
	 *
	 * @param  ids The ids of the people to lookup.
	 * @return     The list of {@link PersonSummaryDTO} objects representing the people to lookup.
	 */
	private List<PersonSummaryDTO> getPeople(List<Long> ids) {
		return getPeople(ids.stream());
	}

	@Test
	void filterTextualFeedback() {
		assertThat(fs.filterTextualFeedback(new ArrayList<>())).isEqualTo(new ArrayList<>());
		assertThat(fs.filterTextualFeedback(List.of(feedback1, feedback2, feedback3, feedback4, feedback5)))
				.containsExactly(feedback4, feedback5);
	}
}
