package org.fuseri.modulelanguageschool.user;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.fuseri.model.dto.course.CourseDto;
import org.fuseri.model.dto.course.LanguageTypeDto;
import org.fuseri.model.dto.course.ProficiencyLevelDto;
import org.fuseri.model.dto.user.UserAddLanguageDto;
import org.fuseri.model.dto.user.UserCreateDto;
import org.fuseri.model.dto.user.UserDto;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
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.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    private final UserCreateDto USER_CREATE_DTO = new UserCreateDto(
            "xnovak", "xnovak@emample.com", "Peter", "Novak");
    private final UserDto USER_DTO = new UserDto(
            "xnovak", "xnovak@emample.com", "Peter", "Novak", new HashMap<>());
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private UserFacade userFacade;

    private static Stream<UserCreateDto> invalidUsers() {
        return Stream.of(
                new UserCreateDto("", "xnovak@emample.com", "Peter", "Novak"),
                new UserCreateDto("xnovak", "", "Peter", "Novak"),
                new UserCreateDto("xnovak", "xnovak@emample.com", "", "Novak"),
                new UserCreateDto("xnovak", "xnovak@emample.com", "Peter", ""),
                new UserCreateDto(null, "xnovak@emample.com", "Peter", "Novak"),
                new UserCreateDto("xnovak", null, "Peter", "Novak"),
                new UserCreateDto("xnovak", "xnovak@emample.com", null, "Novak"),
                new UserCreateDto("xnovak", "xnovak@emample.com", "Peter", null)
        );
    }

    private static String asJsonString(final Object obj) throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(obj);
    }

    @WithMockUser(authorities = {"SCOPE_test_1"})
    @Test
    void createUser() throws Exception {
        Mockito.when(userFacade.create(ArgumentMatchers.isA(UserCreateDto.class))).thenReturn(USER_DTO);
        mockMvc.perform(post("/users")
                        .content(asJsonString(USER_CREATE_DTO))
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated());
    }

    @WithMockUser(authorities = {"SCOPE_test_1"})
    @ParameterizedTest
    @MethodSource("invalidUsers")
    void createInvalidUser(UserCreateDto user) throws Exception {
        Mockito.when(userFacade.create(ArgumentMatchers.isA(UserCreateDto.class))).thenReturn(USER_DTO);
        mockMvc.perform(post("/users")
                        .content(asJsonString(user))
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is4xxClientError());
    }

    @WithMockUser(authorities = {})
    @Test
    void findUser() throws Exception {
        long id = 1;
        Mockito.when(userFacade.find(id)).thenReturn(USER_DTO);
        mockMvc.perform(get("/users/{id}", id))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.username").value(USER_DTO.getUsername()));
    }

    @WithMockUser(authorities = {})
    @Test
    void findAll() throws Exception {
        int page = 0;
        Page<UserDto> pageUserDto = new PageImpl<>(List.of(USER_DTO));
        Mockito.when(userFacade.findAll(page)).thenReturn(pageUserDto);

        mockMvc.perform(get("/users")
                        .param("page", Integer.toString(page)))
                .andExpect(status().isOk())
                .andExpect(content().string(objectMapper.writeValueAsString(pageUserDto)));
    }

    @WithMockUser(authorities = {"SCOPE_test_1"})
    @Test
    void deleteUser() throws Exception {
        mockMvc.perform(delete("/users/{id}", 1L)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNoContent());
    }

    @WithMockUser(authorities = {"SCOPE_test_1"})
    @Test
    void update() throws Exception {
        Long id = 1L;
        Mockito.when(userFacade.update(id, USER_CREATE_DTO)).thenReturn(USER_DTO);

        mockMvc.perform(put("/users/{id}", id)
                        .content(asJsonString(USER_CREATE_DTO))
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }

    @WithMockUser(authorities = {})
    @Test
    void getFinishedCourses() throws Exception {
        Long id = 1L;
        String name = "History Spanish";
        List<CourseDto> courses = List.of(
                new CourseDto(name, 10, LanguageTypeDto.SPANISH, ProficiencyLevelDto.B2)
        );
        Mockito.when(userFacade.getFinished(id)).thenReturn(courses);
        mockMvc.perform(get("/users/{id}/finished-courses", 1L))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(1)))
                .andExpect(jsonPath("$[0].name", equalTo(name)));
    }

    @WithMockUser(authorities = {})
    @Test
    void getEnrolledCourses() throws Exception {
        Long id = 1L;
        String name = "History Spanish";
        List<CourseDto> courses = List.of(
                new CourseDto(name, 10, LanguageTypeDto.SPANISH, ProficiencyLevelDto.B2)
        );
        Mockito.when(userFacade.getEnrolled(id)).thenReturn(courses);
        mockMvc.perform(get("/users/{id}/courses", id))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(1)))
                .andExpect(jsonPath("$[0].name", equalTo(name)));
    }

    @WithMockUser(authorities = {"SCOPE_test_1"})
    @Test
    void addLanguage() throws Exception {
        Long id = 1L;
        var language = LanguageTypeDto.ENGLISH;
        var proficiency = ProficiencyLevelDto.B2;
        UserAddLanguageDto languageDto = new UserAddLanguageDto(language, proficiency);
        UserDto userWithLanguages = USER_DTO;
        userWithLanguages.setLanguageProficiency(Map.of(language, proficiency));

        Mockito.when(userFacade.addLanguageProficiency(ArgumentMatchers.isA(Long.class), ArgumentMatchers.isA(UserAddLanguageDto.class))).thenReturn(userWithLanguages);
        mockMvc.perform(put("/users/{id}/languages", id)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(languageDto)))
                .andExpect(status().isOk());
    }
}