package org.mte.numecoeval.expositiondonneesentrees.infrastructure.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementPhysique;
import org.mte.numecoeval.expositiondonneesentrees.domain.model.OperationNonIT;
import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort;
import org.mte.numecoeval.expositiondonneesentrees.infrastructure.config.MessageProperties;
import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.CorrespondanceRefEquipementDTO;
import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.CritereDTO;
import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.EtapeDTO;
import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.FacteurCaracterisationDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;

@ExtendWith({MockitoExtension.class, SpringExtension.class})
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
@EnableConfigurationProperties(value = MessageProperties.class)
public class ErrorManagementServiceTest {
    private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());

    @Autowired
    MessageProperties messageProperties;
    @InjectMocks
    ErrorManagementService errorManagementService;
    @Mock
    ReferentielServicePort referentielServicePort;

    @BeforeEach
    public void init() {
        errorManagementService = new ErrorManagementService(messageProperties, referentielServicePort, new ArrayList<>());
    }

    @Test
    void importEqPhysique_with_DateEntreeAfterDateRetrait_shouldReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .modele("P2719")
                .quantite(1.0)
                .dateAchat(LocalDate.of(2023, 3, 30))
                .dateRetrait(LocalDate.of(2023, 1, 17))
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .modeUtilisation("COPE").build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        Assertions.assertTrue(actual.getKey().stream().anyMatch("L'âge de l'équipement physical-eq-001 ne peut être calculé car sa date de retrait précède sa date d'achat"::equals));
    }

    @Test
    void importEqPhysique_with_DateEntreeBeforeDateRetrait_shouldNotReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .modele("P2719")
                .quantite(1.0)
                .dateAchat(LocalDate.of(2021, 3, 30))
                .dateRetrait(LocalDate.of(2023, 1, 17))
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .modeUtilisation("COPE").build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        Assertions.assertFalse(actual.getKey().stream().anyMatch("L'âge de l'équipement physical-eq-001 ne peut être calculé car sa date de retrait précède sa date d'achat"::equals));
    }

    @Test
    void importEqPhysique_with_DureeUsageInterneNegative_shouldReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(-3.6)
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        assertTrue(actual.getKey().stream().anyMatch(ds -> "La durée d'usage interne de l'équipement physical-eq-001 est négative ou nulle donc invalide".equals(ds)));
    }

    @Test
    void importEqPhysique_with_DureeUsageInterneZero_shouldReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(0.0)
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        assertTrue(actual.getKey().stream().anyMatch(ds -> "La durée d'usage interne de l'équipement physical-eq-001 est négative ou nulle donc invalide".equals(ds)));
    }

    @Test
    void importEqPhysique_with_DureeUsageInternePositive_shouldNotReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(3.6)
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        assertFalse(actual.getKey().stream().anyMatch(ds -> "La durée d'usage interne de l'équipement physical-eq-001 est négative ou nulle donc invalide".equals(ds)));
    }

    @Test
    void importEqPhysique_with_DureeUsageAvalNegative_shouldReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(1.0)
                .dureeUsageAval(-1.0)
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        assertTrue(actual.getKey().stream().anyMatch(ds -> "La durée d'usage aval de l'équipement physical-eq-001 est négative donc invalide".equals(ds)));
    }

    @Test
    void importEqPhysique_with_DureeUsageAvalZero_shouldNotReturnReportWith1Error() throws IOException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(1.0)
                .dureeUsageAval(0.0)
                .type("Monitor")
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        assertFalse(actual.getKey().stream().anyMatch(ds -> "La durée d'usage aval de l'équipement physical-eq-001 est négative donc invalide".equals(ds)));
    }

    @Test
    void importEqPhysique_shouldReturnReportWithoutError() {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .dureeUsageInterne(1.0)
                .dureeUsageAval(0.0)
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        var actual = errorManagementService.checkEquipementPhysique(eq1, "ref");
        Assertions.assertFalse(actual.getKey().stream().anyMatch("L'équipement physical-eq-001 de type null ne possède pas de référence d'équipement par défaut dans la table ref_type_item et pas de correspondance dans la table ref_correspondance_ref_eqp"::equals));
    }

    @Test
    void importEqPhysique_with_ModeleNull_shouldReturnReportWith1Error() {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .type("phone")
                .dureeUsageInterne(1.0)
                .dureeUsageAval(0.0)
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        Mockito.lenient().when(referentielServicePort.getCorrespondance(eq1.getType())).thenReturn(null);

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        Assertions.assertTrue(actual.getKey().stream().anyMatch("L'équipement physical-eq-001 de type phone ne possède pas de référence d'équipement par défaut dans la table ref_type_item et pas de correspondance dans la table ref_correspondance_ref_eqp"::equals));
    }

    @Test
    void importEqPhysique_with_ModeleAndCorrepondance_shouldReportWithoutError() throws JsonProcessingException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .modele("P123")
                .type("phone")
                .dureeUsageInterne(1.0)
                .dureeUsageAval(0.0)
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        /* MOCK REFERENTIEL : CorrespondanceRefEquipement */
        Mockito.lenient().when(referentielServicePort.getCorrespondance(eq1.getModele())).thenReturn(mapper.readValue("""
                {
                  "modeleEquipementSource": "P123",
                  "refEquipementCible": "smartphone-1"
                }
                """, CorrespondanceRefEquipementDTO.class));
        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        Assertions.assertFalse(actual.getKey().stream().anyMatch("L'équipement physical-eq-001 de type null ne possède pas de référence d'équipement par défaut dans la table ref_type_item et pas de correspondance dans la table ref_correspondance_ref_eqp"::equals));
    }

    @Test
    void importEqPhysique_with_Modele_without_Correspondance_shouldReturnReportWith1Error() throws JsonProcessingException {
        EquipementPhysique eq1 = EquipementPhysique.builder()
                .nomEquipementPhysique("physical-eq-001")
                .quantite(1.0)
                .type("phone")
                .dureeUsageInterne(1.0)
                .dureeUsageAval(0.0)
                .statut("In use")
                .paysDUtilisation("France")
                .build();

        /* MOCK REFERENTIEL : CorrespondanceRefEquipement */
        Mockito.lenient().when(referentielServicePort.getCorrespondance(eq1.getModele())).thenReturn(mapper.readValue("""
                {
                  "modeleEquipementSource": "P123",
                  "refEquipementCible": "smartphone-1"
                }
                """, CorrespondanceRefEquipementDTO.class));

        var actual = errorManagementService.checkEquipementPhysique(eq1, "");
        Assertions.assertTrue(actual.getKey().stream().anyMatch("L'équipement physical-eq-001 de type phone ne possède pas de référence d'équipement par défaut dans la table ref_type_item et pas de correspondance dans la table ref_correspondance_ref_eqp"::equals));
    }

    @Test
    void importOperationNonIT_with_TypeNotInRefType_shouldNotReturnError() {
        OperationNonIT eq1 = OperationNonIT.builder()
                .nomItemNonIT("Batiment_datacenter_St_Malo")
                .quantite(1.0)
                .dureeDeVie(5.0)
                .type("Monitor")
                .localisation("France").build();
        var actual = errorManagementService.checkOperationNonIT(eq1, null);
        Assertions.assertEquals("L'item Batiment_datacenter_St_Malo de type Monitor ne possède pas de référence d'item par défaut dans la table ref_type_item", actual.getKey().get(0));

    }

    @Test
    void importOperationNonIT_with_TypeNotRefTypeItem_shouldReturnReportWith1Error() throws JsonProcessingException {
        OperationNonIT eq1 = OperationNonIT.builder()
                .nomItemNonIT("Batiment_datacenter_St_Malo")
                .quantite(1.0)
                .dureeDeVie(5.0)
                .type("batiment")
                .localisation("France").build();

        /* MOCK REFERENTIEL : FacteurCaracterisation */
        Mockito.lenient().when(referentielServicePort.getFacteurCaracterisation(any(), any(), any())).thenReturn(null);

        /* MOCK REFERENTIEL : Etapes */
        Mockito.lenient().when(referentielServicePort.getAllEtapes()).thenReturn(Arrays.asList(mapper.readValue("""
                [{ "code": "UTILISATION", "libelle": "Using" }]
                """, EtapeDTO[].class)));

        /* MOCK REFERENTIEL : Criteres */
        Mockito.lenient().when(referentielServicePort.getAllCriteres()).thenReturn(Arrays.asList(mapper.readValue("""
                [{
                  "nomCritere": "Climate change",
                  "unite": "kg CO2 eq",
                  "description": "Greenhouse gases (GHG)"
                }]
                """, CritereDTO[].class)));
        var actual = errorManagementService.checkOperationNonIT(eq1, eq1.getType());
        Assertions.assertEquals("L'impact de l'item de référence batiment pour l'étape UTILISATION et le critère Climate change ne pourra pas être calculé en raison de l'absence de facteur d'impact sur cet item", actual.getValue().get(0));
    }

    @Test
    void importOperationNonIT_with_TypeInRefTypeItem_shouldNotReturnError() throws JsonProcessingException {
        OperationNonIT eq1 = OperationNonIT.builder()
                .nomItemNonIT("Batiment_datacenter_St_Malo")
                .quantite(1.0)
                .dureeDeVie(5.0)
                .type("batiment")
                .localisation("France").build();

        /* MOCK REFERENTIEL : FacteurCaracterisation */
        Mockito.lenient().when(referentielServicePort.getFacteurCaracterisation(any(), any(), any())).thenReturn(mapper.readValue("""
                {
                  "nom": "reseau-fixe-1",
                  "etape" : "UTILISATION",
                  "critere" : "Climate change",
                  "localisation": "France",
                  "consoElecMoyenne": 12.7,
                  "valeur" : "8.34"
                }
                """, FacteurCaracterisationDTO.class));

        /* MOCK REFERENTIEL : Etapes */
        Mockito.lenient().when(referentielServicePort.getAllEtapes()).thenReturn(Arrays.asList(mapper.readValue("""
                [{ "code": "UTILISATION", "libelle": "Using" }]
                """, EtapeDTO[].class)));

        /* MOCK REFERENTIEL : Criteres */
        Mockito.lenient().when(referentielServicePort.getAllCriteres()).thenReturn(Arrays.asList(mapper.readValue("""
                [{
                  "nomCritere": "Climate change",
                  "unite": "kg CO2 eq",
                  "description": "Greenhouse gases (GHG)"
                }]
                """, CritereDTO[].class)));
        Pair<List<String>, List<String>> actual = errorManagementService.checkOperationNonIT(eq1, eq1.getType());
        Assertions.assertEquals(0, actual.getKey().size());
    }

    @Test
    void importDataCenter_with_QualiteGoodValue_shouldWork() {
        Assertions.assertNull(errorManagementService.checkQualiteDonnees("BASSE", "L'équipement physique", "eq1"));
    }

    @Test
    void importDataCenter_with_QualiteNull_shouldNotReportWarning() {
        Assertions.assertNull(errorManagementService.checkQualiteDonnees(null, "L'équipement physique", "eq1"));
    }

    @Test
    void importDataCenter_with_QualiteWrongValue_shouldReturnReportWith1Warning() {
        Assertions.assertEquals(
                "L'équipement physique 'eq1' possède une valeur de qualité invalide renseignée. La valeur 'SUPER BASSE' est ignorée car elle n'est pas l'une de ces valeurs: BASSE, MOYENNE, HAUTE",
                errorManagementService.checkQualiteDonnees("SUPER BASSE", "L'équipement physique", "eq1")
        );
    }

    @Test
    void importEqPhysique_with_NomTypeInvalide() {
        Assertions.assertEquals(
                "L'équipement physique 'eq1' possède un type dont la valeur : 'Monitor/' est invalide. La valeur ne doit pas contenir les caractères '/' et '\\'.",
                errorManagementService.checkNomTypeItem("Monitor/", "L'équipement physique", "eq1")
        );
    }
}
