package org.mte.numecoeval.calculs.domain.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique;
import org.mte.numecoeval.calculs.domain.data.demande.OptionsCalcul;
import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique;
import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul;
import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielHypothese;
import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielTypeEquipement;
import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie;
import org.mte.numecoeval.calculs.domain.exception.CalculImpactException;
import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService;
import org.mte.numecoeval.calculs.domain.port.input.service.impl.DureeDeVieEquipementPhysiqueServiceImpl;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;

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

@ExtendWith(MockitoExtension.class)
class DureeDeVieEquipementPhysiqueServiceTest {

    private final DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService = new DureeDeVieEquipementPhysiqueServiceImpl();

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/MM/yyyy");

    @Test
    void whenTypeEquipementDoesntHaveDureeDeVie_shouldUseHypotheseForDureeDeVie() throws Exception {

        //Given
        EquipementPhysique equipementPhysique = EquipementPhysique.builder()
                .type("serveur")
                .build();
        ReferentielHypothese hypothese = ReferentielHypothese.builder()
                .code("dureeVieParDefaut")
                .valeur(1.17d)
                .build();
        ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder()
                .type("serveur")
                .dureeVieDefaut(null)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipementPhysique)
                .hypotheses(Collections.singletonList(hypothese))
                .typeEquipement(typeEquipement)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();

        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(1.17d, actual.getValeurRetenue());
    }


    @Test
    void shouldCalculerDureeVie1d_whenEquipementPhyisqueWithValidDates() throws Exception {
        //Given
        LocalDate dateAchat = LocalDate.parse("15/08/2020", formatter);
        LocalDate dateRetrait = LocalDate.parse("15/06/2021", formatter);
        var equipement = EquipementPhysique.builder()
                .dateAchat(dateAchat)
                .dateRetrait(dateRetrait)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(1d, actual.getValeurRetenue());
    }

    @Test
    void shouldCalculerDureeVie365_whenEquipementPhyisqueWithValidDates() throws Exception {
        //Given
        LocalDate dateAchat = LocalDate.parse("15/08/2020", formatter);
        LocalDate dateRetrait = LocalDate.parse("15/10/2021", formatter);
        var equipement = EquipementPhysique.builder()
                .dateAchat(dateAchat)
                .dateRetrait(dateRetrait)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);
        var expected = 426d / 365d;
        //Then
        assertEquals(expected, actual.getValeurRetenue());
    }

    @Test
    void taiga864_whenEquipementPhysiqueDateRetraitIsNullShouldUseCurrentDayAsDateRetrait() throws Exception {
        //Given
        LocalDate dateAchat = LocalDate.now().minusDays(370);
        var equipement = EquipementPhysique.builder()
                .dateAchat(dateAchat)
                .dateRetrait(null)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);
        var expected = 370 / 365d;
        //Then
        assertEquals(expected, actual.getValeurRetenue());
        assertEquals(dateAchat.format(DateTimeFormatter.ISO_DATE), actual.getDateAchat());
        assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), actual.getDateRetrait());
    }

    @Test
    void shouldThrowException_whenInvalideDatesOrder() throws Exception {
        //Given
        LocalDate dateRetrait = LocalDate.parse("15/08/2020", formatter);
        LocalDate dateAchat = LocalDate.parse("15/10/2021", formatter);
        var equipement = EquipementPhysique.builder()
                .dateAchat(dateAchat)
                .dateRetrait(dateRetrait)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        CalculImpactException exception =
                //Then
                assertThrows(CalculImpactException.class,
                        () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        String expectedMessage = "La durée de vie de l'équipement n'a pas pu être déterminée";
        String actualMessage = exception.getMessage();
        assertTrue(actualMessage.contains(expectedMessage));
    }

    @Test
    void shouldReturnDureeVieDefaut_whenMissingDate() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .type("laptop")
                .build();
        ReferentielHypothese hypothese = ReferentielHypothese.builder()
                .code("dureeVieParDefaut")
                .valeur(1.17d)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .hypotheses(Collections.singletonList(hypothese))
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        DureeDeVie actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(1.17d, actual.getValeurRetenue());
    }

    @Test
    void whenMissingDate_ShouldUseDureeDeVieTypeEquipementInPriority() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .type("laptop")
                .build();
        ReferentielHypothese hypothese = ReferentielHypothese.builder()
                .code("dureeVieParDefaut")
                .valeur(1.17d)
                .build();
        ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder()
                .type("laptop")
                .dureeVieDefaut(3.5d)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(typeEquipement)
                .hypotheses(Collections.singletonList(hypothese))
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        DureeDeVie actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(3.5d, actual.getValeurRetenue());
    }

    @Test
    void whenMissingDureeForTypeAndHypothese_ShouldThrowException() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .type("laptop")
                .build();
        ReferentielHypothese hypothese = ReferentielHypothese.builder()
                .code("dureeVieParDefaut")
                .valeur(null)
                .build();
        ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder()
                .type("laptop")
                .dureeVieDefaut(null)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(typeEquipement)
                .hypotheses(Collections.singletonList(hypothese))
                .optionsCalcul(new OptionsCalcul("REEL"))
                .build();
        //When
        CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        //Then
        assertNotNull(exception, "Une exception doit être levée");
        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        assertEquals("La durée de vie par défaut de l'équipement n'a pas pu être déterminée", exception.getMessage());
    }

    //Tests pour CalculerDuree avec methodeDureeUsage = FIXE
    @Test
    void shouldCalculerDureeVie1an_whenEquipementPhyisqueWithDureeUsageInterneInf1() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(0.8)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();
        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(1d, actual.getValeurRetenue());
    }

    @Test
    void shouldCalculerDureeVie1an_whenEquipementPhyisqueWithDureeUsageInterneEtAvalEtAmontInf1() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(0.1)
                .dureeUsageAmont(0.2)
                .dureeUsageAval(0.2)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();
        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(0.1, actual.getDureeUsageInterne());
        assertEquals(0.2, actual.getDureeUsageAmont());
        assertEquals(1d, actual.getValeurRetenue());
    }

    @Test
    void shouldCalculerDureeVie_whenEquipementPhysiqueWithDureeUsageInterneSup1() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(3.0)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(3.0, actual.getValeurRetenue());
    }

    @Test
    void shouldCalculerDureeVie_whenEquipementPhysiqueWithDureeUsageInterne_dureeUsageAmontAndAval() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(3.0)
                .dureeUsageAmont(2.0)
                .dureeUsageAval(1.0)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(6.0, actual.getValeurRetenue());
    }

    @Test
    void shouldReturnError_whenEquipementPhysiqueWithDureeUsageInterneNegative() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(-1.5)
                .build();

        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        //Then
        assertNotNull(exception, "Une exception doit être levée");
        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        assertEquals("La durée d'usage interne ne peut pas être négative ou nulle", exception.getMessage());
    }

    @Test
    void shouldCalculerDureeVie1d_whenEquipementPhyisqueWithDureeUsageZero() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(0.0)
                .build();
        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .typeEquipement(null)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        //Then
        assertNotNull(exception, "Une exception doit être levée");
        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        assertEquals("La durée d'usage interne ne peut pas être négative ou nulle", exception.getMessage());
    }

    @Test
    void shouldReturnError_whenEquipementPhysiqueWithDureeUsageInterne_dureeUsageAmontNegative() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(3.0)
                .dureeUsageAmont(-1.0)
                .dureeUsageAval(-2.0)
                .build();

        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        //Then
        assertNotNull(exception, "Une exception doit être levée");
        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        assertEquals("La durée d'usage amont ne peut pas être négative", exception.getMessage());
    }

    @Test
    void shouldReturnError_whenEquipementPhysiqueWithDureeUsageInterne_dureeUsageAmontZero() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(3.0)
                .dureeUsageAmont(0.0)
                .dureeUsageAval(1.0)
                .build();

        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);

        //Then
        assertEquals(4.0, actual.getValeurRetenue());
    }

    @Test
    void shouldReturnError_whenEquipementPhysiqueWithDureeUsageAvalNegative() throws Exception {
        //Given
        var equipement = EquipementPhysique.builder()
                .dureeUsageInterne(1.0)
                .dureeUsageAval(-3.4)
                .build();

        DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique
                .builder()
                .equipementPhysique(equipement)
                .optionsCalcul(new OptionsCalcul("FIXE"))
                .build();

        //When
        CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul));

        //Then
        assertNotNull(exception, "Une exception doit être levée");
        assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType());
        assertEquals("La durée d'usage aval ne peut pas être négative", exception.getMessage());
    }
}