/*
 * 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.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import lombok.SneakyThrows;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.api.dto.EditionSummaryDTO;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.queue.repository.FeedbackRepository;
import nl.tudelft.queue.service.PermissionService;

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.modelmapper.ModelMapper;
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.data.domain.PageRequest;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;

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

@Transactional
@AutoConfigureMockMvc
@SpringBootTest(classes = TestQueueApplication.class)
class HomeControllerTest {
	@Autowired
	private MockMvc mvc;

	@Autowired
	private TestDatabaseLoader db;

	@SpyBean
	private PermissionService permissionService;

	@Autowired
	private PersonControllerApi personApi;

	@Autowired
	private EditionControllerApi editionApi;

	@SpyBean
	private FeedbackRepository feedbackRepository;

	private PersonSummaryDTO admin;
	private PersonSummaryDTO student50;
	private PersonSummaryDTO student100;

	private PersonSummaryDTO teacher1;

	private PersonSummaryDTO teacher2;

	private final ModelMapper mapper = new ModelMapper();

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

		admin = db.getAdmin();

		student50 = db.getStudents()[50];
		student100 = db.getStudents()[100];

		teacher1 = db.getTeachers()[1];

		teacher2 = db.getTeachers()[2];

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

	@Test
	void privacyStatement() throws Exception {
		mvc.perform(get("/privacy"))
				.andExpect(status().isOk())
				.andExpect(view().name("home/privacy"));
	}

	@Test
	void about() throws Exception {
		mvc.perform(get("/about"))
				.andExpect(status().isOk())
				.andExpect(view().name("home/about"));
	}

	@Test
	void indexChecksAuth() throws Exception {
		mvc.perform(get("/"))
				.andExpect(status().isOk())
				.andExpect(view().name("home/index"));
	}

	@Test
	@WithUserDetails("student50")
	void index() throws Exception {
		mvc.perform(get("/"))
				.andExpect(status().isOk())
				.andExpect(model().attribute("user", matchPersonId(student50.getId())))
				.andExpect(model().attributeExists("activeRoles", "editions", "labs"))
				.andExpect(model().attributeExists("sharedEditions", "sharedLabs"))
				.andExpect(view().name("home/dashboard"));

		verify(personApi).getPersonById(student50.getId());
	}

	@Test
	@WithUserDetails("admin")
	void indexRecognizesAdmin() throws Exception {
		var edition = new EditionSummaryDTO().name("my edition").id(1L);
		when(editionApi.getAllEditionsActiveAtDate(any(LocalDateTime.class)))
				.thenReturn(Flux.empty());
		when(editionApi.getAllEditionsActiveDuringPeriod(any(LocalDateTime.class), any(LocalDateTime.class)))
				.thenReturn(Flux.just(edition));

		mvc.perform(get("/"))
				.andExpect(status().isOk())
				.andExpect(model().attribute("user", matchPersonId(admin.getId())))
				.andExpect(model().attribute("activeRoles", List.of()))
				.andExpect(model().attribute("editions", Map.of()))
				.andExpect(view().name("home/dashboard"));

		verify(personApi).getPersonById(admin.getId());
	}

	@Test
	@WithUserDetails("teacher1")
	void indexRecognizesTeacher() throws Exception {
		var edition = new EditionSummaryDTO().name("my edition").id(1L);
		when(editionApi.getAllEditionsActiveDuringPeriod(any(LocalDateTime.class), any(LocalDateTime.class)))
				.thenReturn(Flux.just(edition));

		mvc.perform(get("/"))
				.andExpect(status().isOk())
				.andExpect(view().name("home/dashboard"));
	}

	@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(), PageRequest.of(0, 1));
		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"))
				.andExpect(view().name("home/feedback"));

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

	@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
	void specificFeedbackNeedsAuthentication() throws Exception {
		mvc.perform(get("/feedback/1"))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrl("http://localhost/login"));
	}

	@Test
	@WithUserDetails("student99")
	void finishedRolesInOrder() throws Exception {
		RoleDetailsDTO[] roles = db.getStudent99WithMultipleRoles();

		mvc.perform(get("/"))
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("finishedRoles"))
				.andExpect(model().attribute("finishedRoles",
						List.of(roles[0], roles[1], roles[2], roles[3]).stream()
								.map(r -> mapper.map(r, RoleEditionDetailsDTO.class))
								.collect(Collectors.toList())))
				.andExpect(view().name("home/dashboard"));
	}

	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);
			}
		};
	}
}
