package cz.muni.fi.pa165.core.company;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.muni.fi.pa165.model.dto.common.Result;
import cz.muni.fi.pa165.model.dto.company.CompanyCreateDto;
import cz.muni.fi.pa165.model.dto.company.CompanyDto;
import cz.muni.fi.pa165.model.dto.company.CompanyUpdateDto;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.AfterEach;
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.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * Integration tests for the {@link CompanyController} class.
 *
 * @author xskacel
 */
@SpringBootTest
@WithMockUser
@AutoConfigureMockMvc(addFilters = false)
@TestPropertySource(locations = "classpath:application-test.properties")
public class CompanyControllerTest {

	@Autowired
	private MockMvc mockMvc;
	@Autowired
	private ObjectMapper objectMapper;

	@Autowired
	private CompanyFacade companyFacade;

	@Autowired
	private CompanyService companyService;

	private final static String URL = "/api/company";
	private final static String CONTENT_TYPE = "application/json";

	@BeforeEach
	void setUp() {
		CompanyCreateDto companyCreateDto1 = new CompanyCreateDto();
		companyCreateDto1.setName("company uno");
		companyFacade.create(companyCreateDto1);

		CompanyCreateDto companyCreateDto2 = new CompanyCreateDto();
		companyCreateDto2.setName("company duo");
		companyFacade.create(companyCreateDto2);
	}

	@AfterEach
	void cleanUp() {
		companyService.hardDeleteAll();
	}

	/**
	 * Tests the {@link CompanyController#create(CompanyCreateDto)} method with valid input.
	 * Expects the response status code to be 201 (Created).
	 */
	@Test
	void createNonExistingCompanyTest() throws Exception {
		// Prepare
		CompanyCreateDto createDto = new CompanyCreateDto();
		createDto.setName("superDuperCompany");

		assertNull(companyService.findByName(createDto.getName()));

		// Execute
		String response = mockMvc.perform(post(URL)
						.contentType(CONTENT_TYPE)
						.content(objectMapper.writeValueAsString(createDto)))
				.andExpect(status().isCreated())
				.andReturn()
				.getResponse()
				.getContentAsString();

		// Verify
		CompanyDto companyDto = objectMapper.readValue(response, CompanyDto.class);
		assertThat(companyDto.getId()).isNotNull();
		assertThat(companyDto.getName()).isEqualTo(createDto.getName());
	}

	/**
	 * Tests the {@link CompanyController#create(CompanyCreateDto)} method with invalid input.
	 * Expects the response status code to be 409 (Created).
	 */
	@Test
	void shouldNotCreateCompanyWithSameNameTest() throws Exception {
		// Prepare
		CompanyCreateDto createDto = new CompanyCreateDto();
		createDto.setName("company uno");

		// Testing if created user is in Repository (should be)
		assertNotNull(companyService.findByName(createDto.getName()));

		// Execute
		mockMvc.perform(post(URL)
						.contentType(CONTENT_TYPE)
						.content(objectMapper.writeValueAsString(createDto)))
				.andExpect(status().isConflict());
	}

	/**
	 * Tests the {@link CompanyController#findById(String)}  method with valid input.
	 * Expects the response status code to be 200 and found Company
	 */
	@Test
	void shouldFindValidCompanyById() throws Exception {
		List<CompanyDto> companyDtoList = companyFacade.findAll();
		assertTrue(companyDtoList.size() > 0);
		CompanyDto companyToFind = companyDtoList.get(0);
		assertDoesNotThrow(() -> companyService.findById(companyToFind.getId()));

		// Execute
		String response = mockMvc.perform(get(URL + "/{id}", companyToFind.getId())
						.contentType(CONTENT_TYPE))
				.andExpect(status().isOk())
				.andReturn()
				.getResponse()
				.getContentAsString();

		// Verify
		CompanyDto companyDto = objectMapper.readValue(response, CompanyDto.class);
		assertThat(companyDto.getId()).isEqualTo(companyToFind.getId());
		assertThat(companyDto.getName()).isEqualTo(companyToFind.getName());
	}

	/**
	 * Tests the {@link CompanyController#findById(String)}  method with invalid input.
	 * Expects the response status code to be 404 (Not found)
	 */
	@Test
	void shouldNotFindCompanyByNonExistingId() throws Exception {
		// nonsense ID
		final String id = "0000xxxx1111";
		assertThrows(EntityNotFoundException.class, () -> companyService.findById(id));

		// Execute
		mockMvc.perform(get(URL + "/{id}", id)
						.contentType(CONTENT_TYPE))
				.andExpect(status().isNotFound());
	}

	/**
	 * Tests the {@link CompanyController#findAll(int, int)}
	 * Expects the response status code to be 200 and all Companies
	 */
	@Test
	void shouldFindAllCompanies() throws Exception {
		// Execute
		String response = mockMvc.perform(get(URL + "/all" + "?page=0")
						.contentType(CONTENT_TYPE))
				.andExpect(status().isOk())
				.andReturn()
				.getResponse()
				.getContentAsString();
		System.out.println(response);
		Result<CompanyDto> companyDtoResult = objectMapper.readValue(response, new TypeReference<>() {
		});

		assertThat(companyDtoResult.getPage()).isEqualTo(0);
		assertThat(companyDtoResult.getPageSize()).isEqualTo(10);
		assertThat(companyDtoResult.getTotal()).isEqualTo(2);

		final CompanyDto firstCompanyFromResponse = companyDtoResult.getItems().get(0);
		CompanyDto companyDto1 = new CompanyDto();
		companyDto1.setName("company uno");
		assertThat(firstCompanyFromResponse.getName()).isEqualTo(companyDto1.getName());

		final CompanyDto secondCompanyFromResponse = companyDtoResult.getItems().get(1);
		CompanyDto companyDto2 = new CompanyDto();
		companyDto2.setName("company duo");
		assertThat(secondCompanyFromResponse.getName()).isEqualTo(companyDto2.getName());
	}

	/**
	 * Tests the {@link CompanyController#updateById(String, CompanyUpdateDto)} with valid input.
	 * Expects the response status code to be 200 and updated Company
	 */
	@Test
	void shouldUpdateById() throws Exception {
		CompanyUpdateDto companyUpdateDto = new CompanyUpdateDto();
		companyUpdateDto.setName("new company");

		List<CompanyDto> companyDtoList = companyFacade.findAll();
		assertTrue(companyDtoList.size() > 0);
		CompanyDto companyToUpdate = companyDtoList.get(0);


		assertNull(companyService.findByName(companyUpdateDto.getName()));
		assertNotNull(companyService.findByName(companyToUpdate.getName()));

		// Execute
		String response = mockMvc.perform(put(URL + "/{id}", companyToUpdate.getId())
						.contentType(CONTENT_TYPE)
						.content(objectMapper.writeValueAsString(companyUpdateDto)))
				.andExpect(status().isOk())
				.andReturn()
				.getResponse()
				.getContentAsString();

		// Verify
		CompanyDto companyDto = objectMapper.readValue(response, CompanyDto.class);
		assertThat(companyDto.getId()).isEqualTo(companyToUpdate.getId());
		assertThat(companyDto.getName()).isEqualTo(companyUpdateDto.getName());

		assertDoesNotThrow(() -> companyService.findById(companyDto.getId()));

		assertNotNull(companyService.findByName(companyUpdateDto.getName()));
		assertNull(companyService.findByName(companyToUpdate.getName()));
		assertEquals(companyService.findAll().size(), companyDtoList.size());
	}

	/**
	 * Tests the {@link CompanyController#updateById(String, CompanyUpdateDto)} with invalid input.
	 * Expects the response status code to be 409 (Conflict)
	 */
	@Test
	void shouldNotUpdateCompanyWithExistingNameTest() throws Exception {
		CompanyUpdateDto companyUpdateDto = new CompanyUpdateDto();
		companyUpdateDto.setName("company duo");

		List<CompanyDto> companyDtoList = companyFacade.findAll();
		assertTrue(companyDtoList.size() > 0);
		CompanyDto companyToUpdate = companyDtoList.get(0);


		assertNotNull(companyService.findByName(companyUpdateDto.getName()));
		assertNotNull(companyService.findByName(companyToUpdate.getName()));

		mockMvc.perform(put(URL + "/{id}", companyToUpdate.getId())
						.contentType(CONTENT_TYPE)
						.content(objectMapper.writeValueAsString(companyUpdateDto)))
				.andExpect(status().isConflict());
	}

	/**
	 * Tests the {@link CompanyController#deleteById(String)} with valid input.
	 * Expects the response status code to be 200 and deleted Company
	 */
	@Test
	void shouldDeleteById() throws Exception {
		List<CompanyDto> companyDtoList = companyFacade.findAll();
		assertTrue(companyDtoList.size() > 0);
		CompanyDto companyToDelete = companyDtoList.get(0);
		assertDoesNotThrow(() -> companyService.findById(companyToDelete.getId()));

		// Execute
		String response = mockMvc.perform(delete(
						URL + "/{id}", companyToDelete.getId())
						.contentType(CONTENT_TYPE))
				.andExpect(status().isOk())
				.andReturn()
				.getResponse()
				.getContentAsString();

		// Verify
		CompanyDto companyDto = objectMapper.readValue(response, CompanyDto.class);
		assertThat(companyDto.getId()).isEqualTo(companyToDelete.getId());
		assertThat(companyDto.getName()).isEqualTo(companyToDelete.getName());
		assertThrows(EntityNotFoundException.class, () -> companyService.findById(companyToDelete.getId()));
	}

	@Test
	void shouldNotDeleteCompanyThatDoesNotExists() throws Exception {
		// nonsense ID
		final String id = "0000xxxx1111";
		assertThrows(EntityNotFoundException.class, () -> companyService.findById(id));

		// Execute
		mockMvc.perform(delete(
						URL + "/{id}", id)
						.contentType(CONTENT_TYPE))
				.andExpect(status().isNotFound());
	}
}
