/*
 * 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.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 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.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;

import jakarta.transaction.Transactional;
import lombok.SneakyThrows;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.api.dto.EditionSummaryDTO;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.queue.model.Feedback;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.repository.FeedbackRepository;
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 HomeControllerTest {
	@Autowired
	private MockMvc mvc;

	@Autowired
	private TestDatabaseLoader db;

	@Autowired
	private PersonControllerApi personApi;
	@Autowired
	private StudentGroupControllerApi sgApi;

	@Autowired
	private EditionControllerApi editionApi;

	@SpyBean
	private FeedbackRepository feedbackRepository;

	private PersonSummaryDTO admin;
	private PersonSummaryDTO student50;

	Feedback f1;

	Feedback f2;

	Feedback f3;

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

		admin = db.getAdmin();

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

		PersonSummaryDTO teacher1 = db.getTeachers()[1];

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

		LabRequest[] oopNowRegLab1Requests = db.getOopNowRegularLab1Requests();

		// fake feedback
		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();

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

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

	@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
	@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 -> new ModelMapper().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);
			}
		};
	}
}
