package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.entity.AlerteLcbFt;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.AlerteLcbFtRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.math.BigDecimal;
import java.util.Optional;
import java.util.UUID;

/**
 * Tests unitaires pour AlerteLcbFtService.
 *
 * <p>Couvre toutes les branches de genererAlerteSeuilDepasse et genererAlerteJustificationManquante,
 * notamment les calculs de sévérité (INFO / WARNING / CRITICAL) et les cas d'erreur silencieux.
 *
 * @author UnionFlow Team
 */
@QuarkusTest
class AlerteLcbFtServiceTest {

    @Inject
    AlerteLcbFtService alerteLcbFtService;

    @InjectMock
    AlerteLcbFtRepository alerteLcbFtRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    MembreRepository membreRepository;

    private UUID organisationId;
    private UUID membreId;
    private Organisation organisation;
    private Membre membre;

    @BeforeEach
    void setup() {
        organisationId = UUID.randomUUID();
        membreId = UUID.randomUUID();

        organisation = new Organisation();
        organisation.setId(organisationId);
        organisation.setNom("Org Test");

        membre = new Membre();
        membre.setId(membreId);
        membre.setEmail("membre@test.com");
    }

    // =========================================================================
    // genererAlerteSeuilDepasse — sévérité INFO
    // =========================================================================

    @Test
    @DisplayName("genererAlerteSeuilDepasse - ratio < 0.5 → sévérité INFO")
    void genererAlerteSeuilDepasse_severiteInfo() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        BigDecimal montant = new BigDecimal("110000");  // seuil * 1.1 → ratio=0.1 → INFO
        BigDecimal seuil   = new BigDecimal("100000");

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "VIREMENT", montant, seuil,
                "REF-001", "Salaire"))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());

        AlerteLcbFt alerte = captor.getValue();
        assertThatCode(() -> {
            assert "INFO".equals(alerte.getSeverite());
            assert "SEUIL_DEPASSE".equals(alerte.getTypeAlerte());
            assert !alerte.getTraitee();
        }).doesNotThrowAnyException();
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - ratio >= 0.5 → sévérité WARNING")
    void genererAlerteSeuilDepasse_severiteWarning() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        BigDecimal seuil   = new BigDecimal("100000");
        BigDecimal montant = new BigDecimal("160000");  // ratio = 60000/100000 = 0.6 → WARNING

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "DEPOT", montant, seuil,
                "REF-002", "Commerce"))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());
        AlerteLcbFt alerte = captor.getValue();
        assertThat(alerte.getSeverite()).isEqualTo("WARNING");
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - ratio >= 2.0 → sévérité CRITICAL")
    void genererAlerteSeuilDepasse_severiteCritical() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        BigDecimal seuil   = new BigDecimal("100000");
        BigDecimal montant = new BigDecimal("310000");  // ratio = 210000/100000 = 2.1 → CRITICAL

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "TRANSFERT", montant, seuil,
                "REF-003", "Héritage"))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());
        AlerteLcbFt alerte = captor.getValue();
        assertThat(alerte.getSeverite()).isEqualTo("CRITICAL");
    }

    // =========================================================================
    // genererAlerteSeuilDepasse — origineFonds manquante → force WARNING minimum
    // =========================================================================

    @Test
    @DisplayName("genererAlerteSeuilDepasse - origineFonds null → WARNING minimum + JUSTIFICATION MANQUANTE dans details")
    void genererAlerteSeuilDepasse_origineFondsNull_forceWarning() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        // ratio faible → INFO mais origineFonds null → forcé WARNING
        BigDecimal montant = new BigDecimal("105000");
        BigDecimal seuil   = new BigDecimal("100000");

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "RETRAIT", montant, seuil,
                "REF-004", null))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());
        AlerteLcbFt alerte = captor.getValue();
        assertThat(alerte.getSeverite()).isEqualTo("WARNING");
        assertThat(alerte.getDetails()).contains("JUSTIFICATION MANQUANTE");
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - origineFonds blank → WARNING minimum")
    void genererAlerteSeuilDepasse_origineFondsBlank_forceWarning() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        BigDecimal montant = new BigDecimal("102000");
        BigDecimal seuil   = new BigDecimal("100000");

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "PAIEMENT", montant, seuil,
                "REF-005", "  "))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());
        AlerteLcbFt alerte = captor.getValue();
        assertThat(alerte.getSeverite()).isEqualTo("WARNING");
    }

    // =========================================================================
    // genererAlerteSeuilDepasse — valeurs null
    // =========================================================================

    @Test
    @DisplayName("genererAlerteSeuilDepasse - montant et seuil null → sévérité INFO par défaut")
    void genererAlerteSeuilDepasse_montantSeuilNull_severiteInfo() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, null, null, null,
                null, null))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - seuil zero → ratio BigDecimal.ZERO (sévérité INFO)")
    void genererAlerteSeuilDepasse_seuilZero_severiteInfo() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        // seuil = 0 → pas de division → BigDecimal.ZERO → INFO
        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "VIREMENT", new BigDecimal("50000"), BigDecimal.ZERO,
                "REF-006", "Epargne"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - membreId null → membre null dans alerte")
    void genererAlerteSeuilDepasse_membreIdNull_alerteCreee() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, null, "DEPOT", new BigDecimal("200000"), new BigDecimal("100000"),
                "REF-007", "Loyer"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
        verify(membreRepository, never()).findByIdOptional(any());
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - organisation introuvable → alerte créée avec organisation null")
    void genererAlerteSeuilDepasse_organisationIntrouvable_alerteCreee() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.empty());
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "VIREMENT", new BigDecimal("150000"), new BigDecimal("100000"),
                "REF-008", "Travaux"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    // =========================================================================
    // genererAlerteSeuilDepasse — erreur silencieuse
    // =========================================================================

    @Test
    @DisplayName("genererAlerteSeuilDepasse - exception dans persist → absorbée silencieusement")
    void genererAlerteSeuilDepasse_persistException_absorbee() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));
        doThrow(new RuntimeException("DB down")).when(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));

        // Ne doit pas propager l'exception
        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "DEPOT", new BigDecimal("120000"), new BigDecimal("100000"),
                "REF-009", "Salaire"))
                .doesNotThrowAnyException();
    }

    // =========================================================================
    // genererAlerteJustificationManquante — happy path
    // =========================================================================

    @Test
    @DisplayName("genererAlerteJustificationManquante - happy path")
    void genererAlerteJustificationManquante_happyPath() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, membreId, "RETRAIT", new BigDecimal("80000"), "REF-010"))
                .doesNotThrowAnyException();

        ArgumentCaptor<AlerteLcbFt> captor = ArgumentCaptor.forClass(AlerteLcbFt.class);
        verify(alerteLcbFtRepository).persist(captor.capture());

        AlerteLcbFt alerte = captor.getValue();
        assertThatCode(() -> {
            assert "JUSTIFICATION_MANQUANTE".equals(alerte.getTypeAlerte());
            assert "WARNING".equals(alerte.getSeverite());
            assert !alerte.getTraitee();
            assert alerte.getDetails().contains("REF-010");
        }).doesNotThrowAnyException();
    }

    @Test
    @DisplayName("genererAlerteJustificationManquante - typeOperation null")
    void genererAlerteJustificationManquante_typeOperationNull() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, membreId, null, new BigDecimal("50000"), "REF-011"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteJustificationManquante - montant null")
    void genererAlerteJustificationManquante_montantNull() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, membreId, "VIREMENT", null, null))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteJustificationManquante - membreId null")
    void genererAlerteJustificationManquante_membreIdNull() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, null, "DEPOT", new BigDecimal("30000"), "REF-012"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
        verify(membreRepository, never()).findByIdOptional(any());
    }

    @Test
    @DisplayName("genererAlerteJustificationManquante - organisation introuvable → alerte créée")
    void genererAlerteJustificationManquante_orgIntrouvable() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.empty());
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, membreId, "PAIEMENT", new BigDecimal("40000"), "REF-013"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteSeuilDepasse - montant non-null mais seuil null → branche (montant != null && seuil == null) dans calcul écart → N/A")
    void genererAlerteSeuilDepasse_montantNonNullSeuilNull_ecartNA() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));

        // montant != null mais seuil == null → `montant != null && seuil != null` = false → "N/A"
        assertThatCode(() -> alerteLcbFtService.genererAlerteSeuilDepasse(
                organisationId, membreId, "DEPOT", new BigDecimal("100000"), null,
                "REF-015", "Salaire"))
                .doesNotThrowAnyException();

        verify(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));
    }

    @Test
    @DisplayName("genererAlerteJustificationManquante - exception dans persist → absorbée")
    void genererAlerteJustificationManquante_persistException_absorbee() {
        when(organisationRepository.findByIdOptional(organisationId))
                .thenReturn(Optional.of(organisation));
        when(membreRepository.findByIdOptional(membreId))
                .thenReturn(Optional.of(membre));
        doThrow(new RuntimeException("DB timeout")).when(alerteLcbFtRepository).persist(any(AlerteLcbFt.class));

        assertThatCode(() -> alerteLcbFtService.genererAlerteJustificationManquante(
                organisationId, membreId, "DEPOT", new BigDecimal("20000"), "REF-014"))
                .doesNotThrowAnyException();
    }
}
