/*
 * 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.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.data.domain.Page;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;

import jakarta.transaction.Transactional;
import lombok.SneakyThrows;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO;
import nl.tudelft.queue.model.Feedback;
import nl.tudelft.queue.repository.FeedbackRepository;
import nl.tudelft.queue.repository.RequestRepository;
import nl.tudelft.queue.service.PermissionService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import test.TestDatabaseLoader;
import test.test.TestQueueApplication;

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

	@Autowired
	private MockMvc mvc;

	@Autowired
	private TestDatabaseLoader db;

	@Autowired
	private StudentGroupControllerApi sgApi;

	@Autowired
	private RequestRepository rr;

	@SpyBean
	private PermissionService permissionService;

	@SpyBean
	private FeedbackRepository feedbackRepository;

	private Feedback f1;

	private Feedback f2;

	private Feedback f3;

	private Feedback fullyDeletedFeedback;

	private PersonSummaryDTO student100;

	private PersonSummaryDTO teacher1;

	private PersonSummaryDTO teacher2;

	@BeforeEach
	void setUp() {

		LocalDateTime now = LocalDateTime.now();

		db.mockAll();

		var oopNowRegLab1Requests = db.getOopNowRegularLab1Requests();

		teacher1 = db.getTeachers()[1];

		teacher2 = db.getTeachers()[2];

		student100 = db.getStudents()[100];

		StudentGroupDetailsDTO sg1 = db.getOopNowLabGroups()[4];
		when(sgApi.addGroup(any())).thenReturn(Mono.just(sg1.getId()));
		when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.empty());

		f1 = Feedback.builder()
				.id(new Feedback.Id(oopNowRegLab1Requests[0].getId(), student100.getId()))
				.request(oopNowRegLab1Requests[0])
				.rating(5)
				.feedback("GREAT TA")
				.build();

		f2 = Feedback.builder()
				.id(new Feedback.Id(oopNowRegLab1Requests[1].getId(), student100.getId()))
				.request(oopNowRegLab1Requests[1])
				.rating(1)
				.build();

		f3 = Feedback.builder()
				.id(new Feedback.Id(oopNowRegLab1Requests[2].getId(), student100.getId()))
				.request(oopNowRegLab1Requests[2])
				.feedback("Mediocre")
				.build();

		fullyDeletedFeedback = Feedback.builder()
				.id(new Feedback.Id(oopNowRegLab1Requests[3].getId(), student100.getId()))
				.request(oopNowRegLab1Requests[3])
				.feedback("Completely Deleted feedback")
				.rating(3)
				.createdAt(now.minusSeconds(10L))
				.lastUpdatedAt(now.minusSeconds(5L))
				.build();

		// This mock has been untrustworthy in the past, so we should reset it every test.
		reset(feedbackRepository);

		feedbackRepository.saveAll(List.of(f1, f2, f3, fullyDeletedFeedback));
		feedbackRepository.delete(fullyDeletedFeedback);
	}

	@Test
	void feedbackNeedsAuthentication() throws Exception {
		mvc.perform(get("/feedback"))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("http://localhost/login"));
	}

	@Test
	@WithUserDetails("student100")
	void feedbackAllowsIfCanViewFeedback() throws Exception {
		mvc.perform(get("/feedback?page=0&size=1"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("assistant", "feedback"))
				.andExpect(view().name("home/feedback"));

		verify(feedbackRepository).findByAssistantAnonymised(student100.getId());
		verify(permissionService, atLeastOnce()).canViewOwnFeedback();
	}

	@Test
	@WithUserDetails("student1")
	void specificFeedbackVerifiesCanViewFeedback() throws Exception {
		mvc.perform(get("/feedback/{id}?page=1&size=1", student100.getId()))
				.andExpect(status().isForbidden());

		verify(permissionService).canViewFeedback(student100.getId());
		verify(feedbackRepository, never()).findByAssistant(anyLong());
	}

	@Test
	@WithUserDetails("teacher1")
	void specificFeedbackAllowsIfCanViewFeedback() throws Exception {
		mvc.perform(get("/feedback/{id}?page=1&size=1", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("assistant", matchPersonId(student100.getId())))
				.andExpect(model().attributeExists("feedback", "assistantRating"))
				.andExpect(view().name("home/feedback"));

		verify(feedbackRepository, atLeastOnce()).findByAssistant(eq(student100.getId()));
		verify(permissionService).canViewFeedback(student100.getId());
	}

	@Test
	@WithUserDetails("teacher0")
	void teacherCanSeeAssociatedWrittenFeedbackInManagerview() throws Exception {
		var queryResult = mvc.perform(get("/feedback/{id}/manager?page=0&size=100", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("stars", List.of(1, 0, 0, 0, 1)))
				.andReturn();

		assertThat(queryResult.getResponse().getContentAsString()).contains(f1.getFeedback(),
				f3.getFeedback());

	}

	@Test
	@WithUserDetails("teacher0")
	void teacherCanOnlySeeCountsOfFeedbackRatingsThatHaveDeletedRequestsInManagerView() throws Exception {

		rr.delete(f1.getRequest());
		rr.delete(f3.getRequest());
		var queryResult = mvc.perform(get("/feedback/{id}/manager?page=0&size=100", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("stars", List.of(1, 0, 0, 0, 1)))
				.andReturn();

		String res = queryResult.getResponse().getContentAsString();

		assertThat(res).doesNotContain(f1.getFeedback(),
				f3.getFeedback());
		assertThat(res).contains("This person has no written feedback.");

	}

	@Test
	@WithUserDetails("teacher1")
	void teachersCanSeeStarsAssociatedWithOtherCoursesButNotWrittenFeedback() throws Exception {
		var queryResult = mvc.perform(get("/feedback/{id}/manager?page=0&size=100", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("stars", List.of(1, 0, 0, 0, 1)))
				.andReturn();

		assertThat(queryResult.getResponse().getContentAsString()).doesNotContain(f1.getFeedback(),
				f3.getFeedback(), fullyDeletedFeedback.getFeedback());
	}

	@Test
	@WithUserDetails("teacher0")
	void teacherCanSeeCountsOfFeedbackRatingsAndTextThatHaveDeletedRequestsInNormalView() throws Exception {

		rr.delete(f1.getRequest());
		var queryResult = mvc.perform(get("/feedback/{id}", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("stars", List.of(1, 0, 0, 0, 1)))
				.andReturn();

		String res = queryResult.getResponse().getContentAsString();

		assertThat(res).contains(f1.getFeedback(),
				f3.getFeedback());

		assertThat(res).doesNotContain(fullyDeletedFeedback.getFeedback());

	}

	@Test
	@WithUserDetails("admin")
	void adminCanSeeEverythingIncludingFullyDeletedFeedback() throws Exception {
		rr.delete(f1.getRequest());
		var queryResult = mvc.perform(get("/feedback/{id}", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("stars", List.of(1, 0, 1, 0, 1)))
				.andReturn();

		String res = queryResult.getResponse().getContentAsString();

		assertThat(res).contains(f1.getFeedback(),
				f3.getFeedback(), fullyDeletedFeedback.getFeedback());

	}

	@Test
	@WithUserDetails("teacher1")
	void managerViewWorks() throws Exception {

		mvc.perform(get("/feedback/{id}/manager", student100.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("assistant", matchPersonId(student100.getId())))
				.andExpect(model().attributeExists("feedback"))
				.andExpect(view().name("home/feedback"))
				.andExpect(model().attribute("feedback", instanceOf(Page.class)))
				.andExpect(model().attribute("feedback", hasProperty("totalElements", equalTo(0L))));

		verify(feedbackRepository, atLeastOnce()).findByAssistant(eq(student100.getId()));
		verify(permissionService).canViewFeedback(student100.getId());
	}

	@Test
	@WithUserDetails("admin")
	void adminCanViewTeacherFeedbackSuccessfully() throws Exception {
		mvc.perform(get("/feedback/{id}/manager", teacher1.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("assistant", matchPersonId(teacher1.getId())))
				.andExpect(model().attributeExists("feedback"))
				.andExpect(view().name("home/feedback"));

		mvc.perform(get("/feedback/{id}/manager", teacher2.getId()))
				.andExpect(status().isOk())
				.andExpect(model().attribute("assistant", matchPersonId(teacher2.getId())))
				.andExpect(model().attributeExists("feedback"))
				.andExpect(view().name("home/feedback"));

	}

	@Test
	@WithUserDetails("teacher1")
	void teachersCannotViewFeedbackOfOtherTeachers() throws Exception {
		mvc.perform(get("/feedback/{id}/manager", teacher2.getId()))
				.andExpect(status().isForbidden())
				.andExpect(view().name("error/403"));

	}

	@Test
	@WithUserDetails("teacher1")
	void teachersCanViewTheirOwnFeedback() throws Exception {
		mvc.perform(get("/feedback/{id}", teacher1.getId()))
				.andExpect(status().isOk())
				.andExpect(view().name("home/feedback"))
				.andExpect(model().attributeExists("feedback"));
	}

	@Test
	void specificFeedbackNeedsAuthentication() throws Exception {
		mvc.perform(get("/feedback/1"))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("http://localhost/login"));
	}

	@Test
	@WithUserDetails("student100")
	void miscFeedbackModelAttrsAreNotInModelWhenYouViewOwnFeedback() throws Exception {
		mvc.perform(get("/feedback"))
				.andExpect(status().isOk())
				.andExpect(model().attributeDoesNotExist("assistantRating"));
	}

	private Matcher<Object> matchPersonId(Long personId) {
		return new BaseMatcher<>() {
			@Override
			@SneakyThrows
			public boolean matches(Object actual) {
				return Objects.equals(actual.getClass().getMethod("getId").invoke(actual), personId);
			}

			@Override
			public void describeTo(Description description) {
				description.appendText("Expected person id to be equal to " + personId);
			}
		};
	}
}
