package dev.lions.unionflow.server.service.mutuelle.parts;

import dev.lions.unionflow.server.api.dto.mutuelle.parts.ComptePartsSocialesRequest;
import dev.lions.unionflow.server.api.dto.mutuelle.parts.TransactionPartsSocialesRequest;
import dev.lions.unionflow.server.api.enums.mutuelle.parts.StatutComptePartsSociales;
import dev.lions.unionflow.server.api.enums.mutuelle.parts.TypeTransactionPartsSociales;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.mutuelle.parts.ComptePartsSociales;
import dev.lions.unionflow.server.entity.mutuelle.parts.TransactionPartsSociales;
import dev.lions.unionflow.server.mapper.mutuelle.parts.ComptePartsSocialesMapper;
import dev.lions.unionflow.server.mapper.mutuelle.parts.TransactionPartsSocialesMapper;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.mutuelle.ParametresFinanciersMutuellRepository;
import dev.lions.unionflow.server.repository.mutuelle.parts.ComptePartsSocialesRepository;
import dev.lions.unionflow.server.repository.mutuelle.parts.TransactionPartsSocialesRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@QuarkusTest
@DisplayName("ComptePartsSocialesService")
class ComptePartsSocialesServiceTest {

    @Inject ComptePartsSocialesService service;

    @InjectMock ComptePartsSocialesRepository compteRepo;
    @InjectMock TransactionPartsSocialesRepository txRepo;
    @InjectMock MembreRepository membreRepository;
    @InjectMock OrganisationRepository organisationRepository;
    @InjectMock ParametresFinanciersMutuellRepository parametresRepo;
    @InjectMock ComptePartsSocialesMapper compteMapper;
    @InjectMock TransactionPartsSocialesMapper txMapper;

    private Membre membre;
    private Organisation org;
    private UUID membreId;
    private UUID orgId;

    @BeforeEach
    void setUp() {
        membreId = UUID.randomUUID();
        orgId = UUID.randomUUID();
        membre = new Membre();
        membre.setId(membreId);
        membre.setNom("GBANE");
        membre.setPrenom("Allassane");
        org = new Organisation();
        org.setId(orgId);
        org.setNom("Mutuelle GBANE");
        org.setNomCourt("MEG");
    }

    @Nested
    @DisplayName("ouvrirCompte")
    class OuvrirCompte {

        @Test
        @DisplayName("Ouvre un compte avec la valeur nominale de la requête")
        void ouvrirCompte_valeurNominaleExplicite_compteCreé() {
            ComptePartsSocialesRequest req = ComptePartsSocialesRequest.builder()
                    .membreId(membreId.toString())
                    .organisationId(orgId.toString())
                    .nombreParts(2)
                    .valeurNominale(new BigDecimal("5000"))
                    .build();

            when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
            when(compteRepo.findByMembreAndOrg(membreId, orgId)).thenReturn(Optional.empty());
            // count(String, Object) — use typed matcher to resolve count(String, Map) vs count(String, Parameters)
            when(compteRepo.count(anyString(), any(UUID.class))).thenReturn(0L);
            // any(ComptePartsSociales.class) resolves persist(Entity) unambiguously
            doNothing().when(compteRepo).persist(any(ComptePartsSociales.class));
            doNothing().when(txRepo).persist(any(TransactionPartsSociales.class));
            when(compteMapper.toDto(any())).thenReturn(null);

            service.ouvrirCompte(req);

            verify(compteRepo).persist(argThat((ComptePartsSociales c) ->
                    c.getValeurNominale().compareTo(new BigDecimal("5000")) == 0
                    && c.getOrganisation() == org
                    && c.getMembre() == membre));
        }

        @Test
        @DisplayName("Lève NotFoundException si le membre est inconnu")
        void ouvrirCompte_membreInconnu_leveNotFoundException() {
            ComptePartsSocialesRequest req = ComptePartsSocialesRequest.builder()
                    .membreId(UUID.randomUUID().toString())
                    .organisationId(orgId.toString())
                    .nombreParts(1)
                    .build();
            when(membreRepository.findByIdOptional(any())).thenReturn(Optional.empty());

            assertThatThrownBy(() -> service.ouvrirCompte(req))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("Lève IllegalStateException si un compte existe déjà pour ce membre+org")
        void ouvrirCompte_compteExistant_leveIllegalState() {
            ComptePartsSocialesRequest req = ComptePartsSocialesRequest.builder()
                    .membreId(membreId.toString())
                    .organisationId(orgId.toString())
                    .nombreParts(1)
                    .build();
            when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
            when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
            when(compteRepo.findByMembreAndOrg(membreId, orgId))
                    .thenReturn(Optional.of(new ComptePartsSociales()));

            assertThatThrownBy(() -> service.ouvrirCompte(req))
                    .isInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("existe déjà");
        }
    }

    @Nested
    @DisplayName("enregistrerTransaction — règles métier")
    class EnregistrerTransaction {

        private ComptePartsSociales compte;

        @BeforeEach
        void setup() {
            compte = ComptePartsSociales.builder()
                    .membre(membre)
                    .organisation(org)
                    .numeroCompte("PS-MEG-00001")
                    .nombreParts(10)
                    .valeurNominale(new BigDecimal("5000"))
                    .montantTotal(new BigDecimal("50000"))
                    .totalDividendesRecus(BigDecimal.ZERO)
                    .statut(StatutComptePartsSociales.ACTIF)
                    .build();
        }

        @Test
        @DisplayName("SOUSCRIPTION augmente le solde de parts")
        void souscription_augmenteSolde() {
            doNothing().when(txRepo).persist(any(TransactionPartsSociales.class));
            service.enregistrerTransaction(compte, TypeTransactionPartsSociales.SOUSCRIPTION, 5, null, "test", null);
            assertThat(compte.getNombreParts()).isEqualTo(15);
            assertThat(compte.getMontantTotal()).isEqualByComparingTo(new BigDecimal("75000"));
        }

        @Test
        @DisplayName("CESSION_PARTIELLE réduit le solde de parts")
        void cession_réduitSolde() {
            doNothing().when(txRepo).persist(any(TransactionPartsSociales.class));
            service.enregistrerTransaction(compte, TypeTransactionPartsSociales.CESSION_PARTIELLE, 3, null, "test", null);
            assertThat(compte.getNombreParts()).isEqualTo(7);
        }

        @Test
        @DisplayName("CESSION_PARTIELLE lève IllegalArgumentException si parts insuffisantes")
        void cession_soldeInsuffisant_leveException() {
            assertThatThrownBy(() ->
                    service.enregistrerTransaction(compte, TypeTransactionPartsSociales.CESSION_PARTIELLE, 20, null, "test", null))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessageContaining("supérieur au solde");
        }

        @Test
        @DisplayName("RACHAT_TOTAL clôture le compte")
        void rachat_cloreLe_compte() {
            doNothing().when(txRepo).persist(any(TransactionPartsSociales.class));
            service.enregistrerTransaction(compte, TypeTransactionPartsSociales.RACHAT_TOTAL, 10, null, "rachat", null);
            assertThat(compte.getNombreParts()).isZero();
            assertThat(compte.getStatut()).isEqualTo(StatutComptePartsSociales.CLOS);
        }

        @Test
        @DisplayName("PAIEMENT_DIVIDENDE accumule totalDividendesRecus")
        void dividende_accumuleTotalDividendes() {
            doNothing().when(txRepo).persist(any(TransactionPartsSociales.class));
            BigDecimal dividende = new BigDecimal("2500");
            service.enregistrerTransaction(compte, TypeTransactionPartsSociales.PAIEMENT_DIVIDENDE, 1, dividende, "dividende", null);
            assertThat(compte.getTotalDividendesRecus()).isEqualByComparingTo(dividende);
        }

        @Test
        @DisplayName("Transaction sur compte non-ACTIF lève IllegalArgumentException")
        void transaction_compteInactif_leveException() {
            compte.setStatut(StatutComptePartsSociales.SUSPENDU);
            assertThatThrownBy(() ->
                    service.enregistrerTransaction(compte, TypeTransactionPartsSociales.SOUSCRIPTION, 1, null, "test", null))
                    .isInstanceOf(IllegalArgumentException.class);
        }
    }
}
