package org.mte.numecoeval.referentiel.infrastructure.restapi.facade;

import org.hibernate.exception.ConstraintViolationException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mte.numecoeval.referentiel.domain.exception.ReferentielException;
import org.mte.numecoeval.referentiel.domain.model.ImpactReseau;
import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId;
import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort;
import org.mte.numecoeval.referentiel.factory.TestDataFactory;
import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapper;
import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapperImpl;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.Arrays;
import java.util.Collection;

import static org.junit.jupiter.api.Assertions.*;

class ImpactReseauFacadeTest {

    @InjectMocks
    ImpactReseauFacade facadeToTest;

    @Mock
    private ReferentielPersistencePort<ImpactReseau, ImpactReseauId> persistencePort;

    private ImpactReseauMapper mapper = new ImpactReseauMapperImpl();

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
        ReflectionTestUtils.setField(facadeToTest, "mapper", mapper);
    }

    @Test
    void get_shouldReturnMatchingDTO() throws ReferentielException {
        var wantedDTOId = TestDataFactory.ImpactReseauFactory.idDTO(
                TestDataFactory.EtapeFactory.idDTO(TestDataFactory.DEFAULT_ETAPE),
                TestDataFactory.CritereFactory.idDTO(TestDataFactory.DEFAULT_CRITERE),
                "impactReseauMobileMoyen"
        );
        var expectedDomainID = TestDataFactory.ImpactReseauFactory.idDomain(
                TestDataFactory.EtapeFactory.idDomain(TestDataFactory.DEFAULT_ETAPE),
                TestDataFactory.CritereFactory.idDomain(TestDataFactory.DEFAULT_CRITERE),
                "impactReseauMobileMoyen"
        );
        var expectedDomain = TestDataFactory.ImpactReseauFactory.domain(
                TestDataFactory.EtapeFactory.domain(TestDataFactory.DEFAULT_ETAPE, "Test"),
                TestDataFactory.CritereFactory.domain(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE),
                "impactReseauMobileMoyen", "Test", 0.12, 0.0012
        );

        Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain);

        var dto = assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId));


        Assertions.assertEquals(expectedDomain.getEtape(), dto.getEtapeACV());
        Assertions.assertEquals(expectedDomain.getCritere(), dto.getCritere());
        Assertions.assertEquals(expectedDomain.getRefReseau(), dto.getRefReseau());
        Assertions.assertEquals(expectedDomain.getSource(), dto.getSource());
        Assertions.assertEquals(expectedDomain.getValeur(), dto.getValeur());
        Assertions.assertEquals(expectedDomain.getConsoElecMoyenne(), dto.getConsoElecMoyenne());
    }

    @Test
    void addOrUpdate_shouldCallSaveThenReturnMatchingDTO() throws ReferentielException {
        var dtoToSave = TestDataFactory.ImpactReseauFactory.dto(
                TestDataFactory.EtapeFactory.dto(TestDataFactory.DEFAULT_ETAPE, "Test"),
                TestDataFactory.CritereFactory.dto(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE),
                "impactReseauMobileMoyen", "Test", 0.12, 0.0012
        );
        Mockito.when(persistencePort.save(Mockito.any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
        ArgumentCaptor<ImpactReseau> valueCapture = ArgumentCaptor.forClass(ImpactReseau.class);

        var savedDTO = assertDoesNotThrow(() -> facadeToTest.addOrUpdate(dtoToSave));

        Mockito.verify(persistencePort, Mockito.times(1)).save(valueCapture.capture());

        var domainSaved = valueCapture.getValue();
        assertEquals(domainSaved.getEtape(), savedDTO.getEtapeACV());
        assertEquals(domainSaved.getCritere(), savedDTO.getCritere());
        assertEquals(domainSaved.getRefReseau(), savedDTO.getRefReseau());
        assertEquals(domainSaved.getSource(), savedDTO.getSource());
        assertEquals(domainSaved.getValeur(), savedDTO.getValeur());
        assertEquals(domainSaved.getConsoElecMoyenne(), savedDTO.getConsoElecMoyenne());

        assertEquals(dtoToSave, savedDTO);
    }

    @Test
    void addOrUpdate_whenErrorOnSave_shouldThrowException() throws ReferentielException {
        var dtoToSave = TestDataFactory.ImpactReseauFactory.dto(
                TestDataFactory.EtapeFactory.dto("NonExistante", "Test"),
                TestDataFactory.CritereFactory.dto("NonExistant", TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE),
                "impactReseauMobileMoyen", "Test", 0.12, 0.0012
        );
        ConstraintViolationException expectedSaveException = new ConstraintViolationException("Erreur de contrainte sur la table parente", null, "ETAPE");
        Mockito.when(persistencePort.save(Mockito.any())).thenThrow(expectedSaveException);

        var exception = assertThrows(RuntimeException.class, () -> facadeToTest.addOrUpdate(dtoToSave));

        assertEquals(expectedSaveException, exception);
    }

    @Test
    void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException {
        var dtosToSave = Arrays.asList(
                TestDataFactory.ImpactReseauFactory.dto(
                        TestDataFactory.EtapeFactory.dto(TestDataFactory.DEFAULT_ETAPE, "Test"),
                        TestDataFactory.CritereFactory.dto(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE),
                        "impactReseauMobileMoyen", "Monitor", 0.12, 0.0012
                ),
                TestDataFactory.ImpactReseauFactory.dto(
                        TestDataFactory.EtapeFactory.dto("FIN_DE_VIE", "Autre Test"),
                        TestDataFactory.CritereFactory.dto("Acidification", "m3", "Autre Test"),
                        "impactReseauMobileMoyen", "Monitor", 0.12, 0.0012
                )
        );
        ArgumentCaptor<Collection<ImpactReseau>> valueCapture = ArgumentCaptor.forClass(Collection.class);

        assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave));

        Mockito.verify(persistencePort, Mockito.times(1)).purge();
        Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture());

        var expectedDomains = valueCapture.getValue();
        assertNotNull(expectedDomains);
        assertEquals(dtosToSave.size(), expectedDomains.size());
        dtosToSave.forEach(dto -> {
            var matchingDomain = expectedDomains.stream()
                    .filter(domain ->
                            dto.getEtapeACV().equals(domain.getEtape())
                            && dto.getCritere().equals(domain.getCritere())
                            && dto.getRefReseau().equals(domain.getRefReseau()))
                    .findAny();
            assertTrue(matchingDomain.isPresent());
            var domain = matchingDomain.get();
            assertEquals(dto.getEtapeACV(), domain.getEtape());
            assertEquals(dto.getCritere(), domain.getCritere());
            assertEquals(dto.getRefReseau(), domain.getRefReseau());
            assertEquals(dto.getSource(), domain.getSource());
            assertEquals(dto.getValeur(), domain.getValeur());
            assertEquals(dto.getConsoElecMoyenne(), domain.getConsoElecMoyenne());
        });
    }
}
