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

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.muni.fi.pa165.core.company.Company;
import cz.muni.fi.pa165.core.company.CompanyService;
import cz.muni.fi.pa165.core.metrics.MetricsService;
import cz.muni.fi.pa165.core.smartmeter.SmartMeterService;
import cz.muni.fi.pa165.model.dto.common.Result;
import cz.muni.fi.pa165.model.dto.device.DeviceCreateDto;
import cz.muni.fi.pa165.model.dto.device.DeviceDto;
import cz.muni.fi.pa165.model.dto.device.DeviceUpdateDto;
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;

@SpringBootTest
@WithMockUser
@AutoConfigureMockMvc(addFilters = false)
@TestPropertySource(locations = "classpath:application-test.properties")
class DeviceControllerTest {
	@Autowired
	private MockMvc mockMvc;
	@Autowired
	private ObjectMapper objectMapper;

	@Autowired
	private DeviceFacade deviceFacade;

	@Autowired
	private DeviceService deviceService;

	@Autowired
	private SmartMeterService smService;

	@Autowired
	private MetricsService metricsService;

	@Autowired
	private CompanyService companyService;

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

	@BeforeEach
	void setUp() {
		Company company = Company.builder().name("Apple").build();
		companyService.create(company);

		DeviceCreateDto deviceCreateDto1 = new DeviceCreateDto();
		deviceCreateDto1.setName("dev01");
		deviceCreateDto1.setCompanyId(company.getId());
		deviceFacade.create(deviceCreateDto1);

		DeviceCreateDto deviceCreateDto2 = new DeviceCreateDto();
		deviceCreateDto2.setName("dev02");
		deviceCreateDto2.setCompanyId(company.getId());
		deviceFacade.create(deviceCreateDto2);
	}

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

	private String getCompanyId() {
		List<Company> companyList = companyService.findAll();
		return companyList.get(0).getId();
	}

	/**
	 * Tests the {@link DeviceController#create(DeviceCreateDto)} method with valid input.
	 * Expects the response status code to be 201 (Created).
	 */
	@Test
	void createNonExistingDeviceTest() throws Exception {
		// Prepare
		DeviceCreateDto deviceCreateDto = new DeviceCreateDto();
		deviceCreateDto.setName("dev03");
		deviceCreateDto.setCompanyId(getCompanyId());

		assertNull(deviceService.findByName(deviceCreateDto.getName()));

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

		// Verify
		DeviceDto deviceDto = objectMapper.readValue(response, DeviceDto.class);
		assertThat(deviceDto.getId()).isNotNull();
		assertThat(deviceDto.getName()).isEqualTo(deviceCreateDto.getName());
		assertNotNull(deviceService.findByName(deviceCreateDto.getName()));
	}

	/**
	 * Tests the {@link DeviceController#create(DeviceCreateDto)}  method with invalid input.
	 * Expects the response status code to be 409 (Conflict).
	 */
	@Test
	void shouldNotCreateDeviceWithSameNameTest() throws Exception {
		// Prepare
		DeviceCreateDto deviceCreateDto = new DeviceCreateDto();
		deviceCreateDto.setName("dev02");
		deviceCreateDto.setCompanyId(getCompanyId());

		assertNotNull(deviceService.findByName(deviceCreateDto.getName()));

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

	/**
	 * Tests the {@link DeviceController#create(DeviceCreateDto)}  method with invalid input.
	 * Expects the response status code to be 404 (Not found).
	 */
	@Test
	void shouldNotCreateDeviceWithNonExistingCompanyTest() throws Exception {
		// Prepare
		DeviceCreateDto deviceCreateDto = new DeviceCreateDto();
		deviceCreateDto.setName("dev03");
		deviceCreateDto.setCompanyId("11xxx11");

		assertNull(deviceService.findByName(deviceCreateDto.getName()));

		// Execute
		mockMvc.perform(post(URL)
						.contentType(CONTENT_TYPE)
						.content(objectMapper.writeValueAsString(deviceCreateDto)))
				.andExpect(status().isNotFound());

		assertNull(deviceService.findByName(deviceCreateDto.getName()));
	}

	/**
	 * Tests the {@link DeviceController#findById(String)}  method with valid input.
	 * Expects the response status code to be 200 and found Device
	 */
	@Test
	void shouldFindValidDeviceById() throws Exception {
		List<DeviceDto> deviceDtoList = deviceFacade.findAll();
		assertTrue(deviceDtoList.size() > 0);
		DeviceDto deviceToFind = deviceDtoList.get(0);
		assertDoesNotThrow(() -> deviceService.findById(deviceToFind.getId()));

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

		// Verify
		DeviceDto deviceDto = objectMapper.readValue(response, DeviceDto.class);
		assertThat(deviceDto.getId()).isEqualTo(deviceToFind.getId());
		assertThat(deviceDto.getName()).isEqualTo(deviceToFind.getName());
	}

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

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

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

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

		final DeviceDto firstDeviceFromResponse = deviceDtoResult.getItems().get(0);
		DeviceDto deviceDto1 = new DeviceDto();
		deviceDto1.setName("dev01");
		assertThat(firstDeviceFromResponse.getName()).isEqualTo(deviceDto1.getName());

		final DeviceDto secondDeviceFromResponse = deviceDtoResult.getItems().get(1);
		DeviceDto deviceDto2 = new DeviceDto();
		deviceDto2.setName("dev02");
		assertThat(secondDeviceFromResponse.getName()).isEqualTo(deviceDto2.getName());
	}

	/**
	 * Tests the {@link DeviceController#updateById(String, DeviceUpdateDto)} with valid input.
	 * Expects the response status code to be 200 and updated Device
	 */
	@Test
	void shouldUpdateById() throws Exception {
		DeviceUpdateDto deviceUpdateDto = new DeviceUpdateDto();
		deviceUpdateDto.setName("new device");

		List<DeviceDto> deviceDtoList = deviceFacade.findAll();
		assertTrue(deviceDtoList.size() > 0);
		DeviceDto deviceToUpdate = deviceDtoList.get(0);


		assertNull(deviceService.findByName(deviceUpdateDto.getName()));
		assertNotNull(deviceService.findByName(deviceToUpdate.getName()));

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

		// Verify
		DeviceDto deviceDto = objectMapper.readValue(response, DeviceDto.class);
		assertThat(deviceDto.getId()).isEqualTo(deviceToUpdate.getId());
		assertThat(deviceDto.getName()).isEqualTo(deviceUpdateDto.getName());

		assertDoesNotThrow(() -> deviceService.findById(deviceDto.getId()));

		assertNotNull(deviceService.findByName(deviceUpdateDto.getName()));
		assertNull(deviceService.findByName(deviceToUpdate.getName()));
		assertEquals(deviceService.findAll().size(), deviceDtoList.size());
	}

	/**
	 * Tests the {@link DeviceController#updateById(String, DeviceUpdateDto)} with invalid input.
	 * Expects the response status code to be 409 (Conflict)
	 */
	@Test
	void shouldNotUpdateDeviceWithExistingNameTest() throws Exception {
		DeviceUpdateDto deviceUpdateDto = new DeviceUpdateDto();
		deviceUpdateDto.setName("dev01");

		List<DeviceDto> deviceDtoList = deviceFacade.findAll();
		assertTrue(deviceDtoList.size() > 0);
		DeviceDto deviceToUpdate = deviceDtoList.get(0);


		assertNotNull(deviceService.findByName(deviceUpdateDto.getName()));
		assertNotNull(deviceService.findByName(deviceToUpdate.getName()));

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

	/**
	 * Tests the {@link DeviceController#deleteById(String)} with valid input.
	 * Expects the response status code to be 200 and deleted Device
	 */
	@Test
	void shouldDeleteById() throws Exception {
		List<DeviceDto> deviceDtoList = deviceFacade.findAll();
		assertTrue(deviceDtoList.size() > 0);
		DeviceDto deviceToDelete = deviceDtoList.get(0);
		assertDoesNotThrow(() -> deviceService.findById(deviceToDelete.getId()));

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

		// Verify
		DeviceDto deviceDto = objectMapper.readValue(response, DeviceDto.class);
		assertThat(deviceDto.getId()).isEqualTo(deviceToDelete.getId());
		assertThat(deviceDto.getName()).isEqualTo(deviceToDelete.getName());
		assertThrows(EntityNotFoundException.class, () -> deviceService.findById(deviceToDelete.getId()));
	}

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

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