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

import cz.muni.fi.pa165.core.company.Company;
import cz.muni.fi.pa165.core.device.Device;
import cz.muni.fi.pa165.core.helpers.exceptions.EntityDeletionException;
import cz.muni.fi.pa165.core.house.House;
import cz.muni.fi.pa165.core.metrics.MetricsService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

/**
 * SmartMeter unit tests
 *
 * @author xskacel
 */

public class SmartMeterServiceTest {

	@InjectMocks
	private SmartMeterService smartMeterService;

	@InjectMocks
	private MetricsService metricsService;
	@Mock
	private SmartMeterRepository smartMeterRepositoryMock;

	@BeforeEach
	void init() {
		openMocks(this);
	}

	@Test
	void shouldGetSmartMeterById() {
		Company company = Company.builder().name("Apple").build();

		Device device = Device
				.builder()
				.company(company)
				.name("device-1")
				.build();

		House house = House
				.builder()
				.state("Czechia")
				.city("Prague")
				.address("Somewhere 123")
				.zipcode("666 66")
				.build();

		SmartMeter smartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.device(device)
				.house(house)
				.build();

		house.setSmartMeterList(List.of(smartMeter));
		device.setSmartMeterList(List.of(smartMeter));


		when(smartMeterRepositoryMock.findById(smartMeter.getId())).thenReturn(Optional.of(smartMeter));

		SmartMeter result = smartMeterService.findById(smartMeter.getId());

		verify(smartMeterRepositoryMock).findById(smartMeter.getId());

		assertEquals(smartMeter, result);
	}

	@Test
	void shouldCreateNewSmartMeter() {
		Device device = Device
				.builder()
				.name("device-1")
				.build();

		House house = House
				.builder()
				.state("Czechia")
				.city("Prague")
				.address("Somewhere 123")
				.zipcode("666 66")
				.build();

		SmartMeter smartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.device(device)
				.house(house)
				.build();

		house.setSmartMeterList(List.of(smartMeter));
		device.setSmartMeterList(List.of(smartMeter));


		when(smartMeterRepositoryMock.save(smartMeter)).thenReturn(smartMeter);

		SmartMeter createdSmartMeter = smartMeterService.create(smartMeter);

		verify(smartMeterRepositoryMock).save(smartMeter);

		assertThat(createdSmartMeter).isEqualTo(smartMeter);
	}

	@Test
	void shouldFindSmartMeterByName() {
		SmartMeter smartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.build();

		when(smartMeterRepositoryMock.findByName(smartMeter.getName())).thenReturn(Optional.of(smartMeter));

		SmartMeter result = smartMeterService.findByName(smartMeter.getName());

		verify(smartMeterRepositoryMock).findByName(smartMeter.getName());

		assertEquals(smartMeter, result);
	}

	@Test
	void shouldFindAllSmartMeterPageableInt() {
		SmartMeter firstSmartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.build();

		SmartMeter secondSmartMeter = SmartMeter
				.builder()
				.name("my-second-smart-device")
				.build();

		Page<SmartMeter> page = new PageImpl<>(List.of(firstSmartMeter, secondSmartMeter));

		int pageNumber = 0;
		int pageSize = 10;

		when(smartMeterRepositoryMock.findAll(PageRequest.of(pageNumber, pageSize))).thenReturn(page);

		Page<SmartMeter> result = smartMeterService.findAllPageableInt(pageNumber);

		verify(smartMeterRepositoryMock).findAll(PageRequest.of(pageNumber, pageSize));

		assertThat(result).containsExactlyInAnyOrder(firstSmartMeter, secondSmartMeter);
	}

	@Test
	void shouldDeleteSmartMeterById() {
		SmartMeter smartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.build();

		when(smartMeterRepositoryMock.findById(smartMeter.getId())).thenReturn(Optional.of(smartMeter));

		try {
			SmartMeter deletedSmartMeter = smartMeterService.deleteById(smartMeter.getId());

			verify(smartMeterRepositoryMock).findById(smartMeter.getId());
			verify(smartMeterRepositoryMock).save(smartMeter); // soft delete

			assertNotNull(deletedSmartMeter.getDeletedAt());
			assertEquals(smartMeter, deletedSmartMeter);
		} catch (Exception e){
			Assertions.assertThat(false).isTrue();
		}
	}

	@Test
	void shouldUpdateDevice() {
		SmartMeter originalSmartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.build();

		SmartMeter updatedSmartMeter = SmartMeter
				.builder()
				.name("my-new-first-smart-device")
				.build();

		when(smartMeterRepositoryMock.findById(originalSmartMeter.getId())).thenReturn(Optional.of(originalSmartMeter));

		when(smartMeterRepositoryMock.save(updatedSmartMeter)).thenReturn(updatedSmartMeter);

		SmartMeter result = smartMeterService.update(updatedSmartMeter, originalSmartMeter.getId());

		verify(smartMeterRepositoryMock).findById(originalSmartMeter.getId());
		verify(smartMeterRepositoryMock).save(updatedSmartMeter);

		assertEquals(originalSmartMeter.getId(), result.getId());
		assertEquals(updatedSmartMeter.getName(), result.getName());
	}

	@Test
	void shouldHardDeleteAllSmartMeters() {
		smartMeterService.hardDeleteAll();
		verify(smartMeterRepositoryMock).deleteAll();
	}

	@Test
	void shouldDeleteAllSmartMeters() {
		var data = List.of(
				SmartMeter.builder().name("my-first-smart-device").build(),
				SmartMeter.builder().name("my-new-first-smart-device").build()
		);
		when(smartMeterRepositoryMock.findAll()).thenReturn(data);
		smartMeterService.deleteAll();
		verify(smartMeterRepositoryMock).findAll();
		verify(smartMeterRepositoryMock).saveAll(data);
	}

	@Test
	void shouldDeleteAllSmartMetersIds() throws EntityDeletionException {
		SmartMeter firstSmartMeter = SmartMeter
				.builder()
				.name("my-first-smart-device")
				.build();

		SmartMeter secondSmartMeter = SmartMeter
				.builder()
				.name("my-second-smart-device")
				.build();

		String[] ids = {firstSmartMeter.getId(), secondSmartMeter.getId()};
		when(smartMeterRepositoryMock.findById(ids[0])).thenReturn(Optional.of(firstSmartMeter));
		when(smartMeterRepositoryMock.findById(ids[1])).thenReturn(Optional.of(secondSmartMeter));
		try {
			smartMeterService.deleteAllById(ids);

			verify(smartMeterRepositoryMock).save(firstSmartMeter);
			verify(smartMeterRepositoryMock).save(secondSmartMeter);
		} catch (Exception e){
			Assertions.assertThat(false);
		}
	}

	@Test
	void shouldFindAllSmartMeters() {
		var data = List.of(
				SmartMeter.builder().name("my-first-smart-device").build(),
				SmartMeter.builder().name("my-second-smart-device").build()
		);

		when(smartMeterRepositoryMock.findAll()).thenReturn(data);
		List<SmartMeter> result = smartMeterService.findAll();
		verify(smartMeterRepositoryMock).findAll();
		assertEquals(data, result);
	}

	@Test
	void shouldFindAllSmartMetersPageable() {
		int pageNumber = 0;
		int pageSize = 10;
		Pageable pageable = PageRequest.of(pageNumber, pageSize);

		var data = List.of(
				SmartMeter.builder().name("my-first-smart-device").build(),
				SmartMeter.builder().name("my-second-smart-device").build()
		);

		Page<SmartMeter> pageResult = new PageImpl<>(data, pageable, data.size());
		when(smartMeterRepositoryMock.findAll(pageable)).thenReturn(pageResult);

		Page<SmartMeter> result = smartMeterService.findAllPageable(pageable);
		verify(smartMeterRepositoryMock).findAll(pageable);

		assertEquals(pageResult, result);
	}
	// TODO: Do constraint checking for soft-delete (non-cascade)

}
