package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
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 org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

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

/**
 * Tests pour MembreService
 *
 * @author Lions Dev Team
 * @since 2025-01-10
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("Tests MembreService")
class MembreServiceTest {

    @InjectMocks
    MembreService membreService;

    @Mock
    MembreRepository membreRepository;

    private Membre membreTest;

    @BeforeEach
    void setUp() {
        membreTest = Membre.builder()
                .prenom("Jean")
                .nom("Dupont")
                .email("jean.dupont@test.com")
                .telephone("221701234567")
                .dateNaissance(LocalDate.of(1990, 5, 15))
                .dateAdhesion(LocalDate.now())
                .actif(true)
                .build();
    }

    @Nested
    @DisplayName("Tests creerMembre")
    class CreerMembreTests {

        @Test
        @DisplayName("Création réussie d'un membre")
        void testCreerMembreReussi() {
            // Given
            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty());

            // When
            Membre result = membreService.creerMembre(membreTest);

            // Then
            assertThat(result).isNotNull();
            assertThat(result.getNumeroMembre()).isNotNull();
            assertThat(result.getNumeroMembre()).startsWith("UF2025-");
            verify(membreRepository).persist(membreTest);
        }

        @Test
        @DisplayName("Erreur si email déjà existant")
        void testCreerMembreEmailExistant() {
            // Given
            when(membreRepository.findByEmail(membreTest.getEmail()))
                    .thenReturn(Optional.of(membreTest));

            // When & Then
            assertThatThrownBy(() -> membreService.creerMembre(membreTest))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("Un membre avec cet email existe déjà");
        }

        @Test
        @DisplayName("Erreur si numéro de membre déjà existant")
        void testCreerMembreNumeroExistant() {
            // Given
            membreTest.setNumeroMembre("UF2025-EXIST");
            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre("UF2025-EXIST"))
                    .thenReturn(Optional.of(membreTest));

            // When & Then
            assertThatThrownBy(() -> membreService.creerMembre(membreTest))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("Un membre avec ce numéro existe déjà");
        }

        @Test
        @DisplayName("Génération automatique du numéro de membre")
        void testGenerationNumeroMembre() {
            // Given
            membreTest.setNumeroMembre(null);
            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty());

            // When
            membreService.creerMembre(membreTest);

            // Then
            ArgumentCaptor<Membre> captor = ArgumentCaptor.forClass(Membre.class);
            verify(membreRepository).persist(captor.capture());
            assertThat(captor.getValue().getNumeroMembre()).isNotNull();
            assertThat(captor.getValue().getNumeroMembre()).startsWith("UF2025-");
        }

        @Test
        @DisplayName("Génération automatique du numéro de membre avec chaîne vide")
        void testGenerationNumeroMembreChainVide() {
            // Given
            membreTest.setNumeroMembre(""); // Chaîne vide
            when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
            when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty());

            // When
            membreService.creerMembre(membreTest);

            // Then
            ArgumentCaptor<Membre> captor = ArgumentCaptor.forClass(Membre.class);
            verify(membreRepository).persist(captor.capture());
            assertThat(captor.getValue().getNumeroMembre()).isNotNull();
            assertThat(captor.getValue().getNumeroMembre()).isNotEmpty();
            assertThat(captor.getValue().getNumeroMembre()).startsWith("UF2025-");
        }
    }

    @Nested
    @DisplayName("Tests mettreAJourMembre")
    class MettreAJourMembreTests {

        @Test
        @DisplayName("Mise à jour réussie d'un membre")
        void testMettreAJourMembreReussi() {
            // Given
            Long id = 1L;
            membreTest.id = id; // Utiliser le champ directement
            Membre membreModifie = Membre.builder()
                    .prenom("Pierre")
                    .nom("Martin")
                    .email("pierre.martin@test.com")
                    .telephone("221701234568")
                    .dateNaissance(LocalDate.of(1985, 8, 20))
                    .actif(false)
                    .build();

            when(membreRepository.findById(id)).thenReturn(membreTest);
            when(membreRepository.findByEmail("pierre.martin@test.com")).thenReturn(Optional.empty());

            // When
            Membre result = membreService.mettreAJourMembre(id, membreModifie);

            // Then
            assertThat(result.getPrenom()).isEqualTo("Pierre");
            assertThat(result.getNom()).isEqualTo("Martin");
            assertThat(result.getEmail()).isEqualTo("pierre.martin@test.com");
            assertThat(result.getTelephone()).isEqualTo("221701234568");
            assertThat(result.getDateNaissance()).isEqualTo(LocalDate.of(1985, 8, 20));
            assertThat(result.getActif()).isFalse();
        }

        @Test
        @DisplayName("Erreur si membre non trouvé")
        void testMettreAJourMembreNonTrouve() {
            // Given
            Long id = 999L;
            when(membreRepository.findById(id)).thenReturn(null);

            // When & Then
            assertThatThrownBy(() -> membreService.mettreAJourMembre(id, membreTest))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("Membre non trouvé avec l'ID: " + id);
        }

        @Test
        @DisplayName("Erreur si nouvel email déjà existant")
        void testMettreAJourMembreEmailExistant() {
            // Given
            Long id = 1L;
            membreTest.id = id; // Utiliser le champ directement
            membreTest.setEmail("ancien@test.com");

            Membre membreModifie = Membre.builder()
                    .email("nouveau@test.com")
                    .build();

            Membre autreMembreAvecEmail = Membre.builder()
                    .email("nouveau@test.com")
                    .build();
            autreMembreAvecEmail.id = 2L; // Utiliser le champ directement

            when(membreRepository.findById(id)).thenReturn(membreTest);
            when(membreRepository.findByEmail("nouveau@test.com"))
                    .thenReturn(Optional.of(autreMembreAvecEmail));

            // When & Then
            assertThatThrownBy(() -> membreService.mettreAJourMembre(id, membreModifie))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("Un membre avec cet email existe déjà");
        }

        @Test
        @DisplayName("Mise à jour sans changement d'email")
        void testMettreAJourMembreSansChangementEmail() {
            // Given
            Long id = 1L;
            membreTest.id = id; // Utiliser le champ directement
            membreTest.setEmail("meme@test.com");

            Membre membreModifie = Membre.builder()
                    .prenom("Pierre")
                    .nom("Martin")
                    .email("meme@test.com") // Même email
                    .telephone("221701234568")
                    .dateNaissance(LocalDate.of(1985, 8, 20))
                    .actif(false)
                    .build();

            when(membreRepository.findById(id)).thenReturn(membreTest);
            // Pas besoin de mocker findByEmail car l'email n'a pas changé

            // When
            Membre result = membreService.mettreAJourMembre(id, membreModifie);

            // Then
            assertThat(result.getPrenom()).isEqualTo("Pierre");
            assertThat(result.getNom()).isEqualTo("Martin");
            assertThat(result.getEmail()).isEqualTo("meme@test.com");
            // Vérifier que findByEmail n'a pas été appelé
            verify(membreRepository, never()).findByEmail("meme@test.com");
        }
    }

    @Test
    @DisplayName("Test trouverParId")
    void testTrouverParId() {
        // Given
        Long id = 1L;
        when(membreRepository.findById(id)).thenReturn(membreTest);

        // When
        Optional<Membre> result = membreService.trouverParId(id);

        // Then
        assertThat(result).isPresent();
        assertThat(result.get()).isEqualTo(membreTest);
    }

    @Test
    @DisplayName("Test trouverParEmail")
    void testTrouverParEmail() {
        // Given
        String email = "jean.dupont@test.com";
        when(membreRepository.findByEmail(email)).thenReturn(Optional.of(membreTest));

        // When
        Optional<Membre> result = membreService.trouverParEmail(email);

        // Then
        assertThat(result).isPresent();
        assertThat(result.get()).isEqualTo(membreTest);
    }

    @Test
    @DisplayName("Test listerMembresActifs")
    void testListerMembresActifs() {
        // Given
        List<Membre> membresActifs = Arrays.asList(membreTest);
        when(membreRepository.findAllActifs()).thenReturn(membresActifs);

        // When
        List<Membre> result = membreService.listerMembresActifs();

        // Then
        assertThat(result).hasSize(1);
        assertThat(result.get(0)).isEqualTo(membreTest);
    }

    @Test
    @DisplayName("Test rechercherMembres")
    void testRechercherMembres() {
        // Given
        String recherche = "Jean";
        List<Membre> resultatsRecherche = Arrays.asList(membreTest);
        when(membreRepository.findByNomOrPrenom(recherche)).thenReturn(resultatsRecherche);

        // When
        List<Membre> result = membreService.rechercherMembres(recherche);

        // Then
        assertThat(result).hasSize(1);
        assertThat(result.get(0)).isEqualTo(membreTest);
    }

    @Test
    @DisplayName("Test desactiverMembre - Succès")
    void testDesactiverMembreReussi() {
        // Given
        Long id = 1L;
        membreTest.id = id; // Utiliser le champ directement
        when(membreRepository.findById(id)).thenReturn(membreTest);

        // When
        membreService.desactiverMembre(id);

        // Then
        assertThat(membreTest.getActif()).isFalse();
    }

    @Test
    @DisplayName("Test desactiverMembre - Membre non trouvé")
    void testDesactiverMembreNonTrouve() {
        // Given
        Long id = 999L;
        when(membreRepository.findById(id)).thenReturn(null);

        // When & Then
        assertThatThrownBy(() -> membreService.desactiverMembre(id))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Membre non trouvé avec l'ID: " + id);
    }

    @Test
    @DisplayName("Test compterMembresActifs")
    void testCompterMembresActifs() {
        // Given
        when(membreRepository.countActifs()).thenReturn(5L);

        // When
        long result = membreService.compterMembresActifs();

        // Then
        assertThat(result).isEqualTo(5L);
    }
}
