package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

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

/**
 * Tests unitaires pour {@link MemberLifecycleService}.
 *
 * <p>Couvre les transitions de statut manuel (activer, suspendre, radier, archiver, inviter)
 * ainsi que les traitements automatiques (expiration des invitations, rappels).
 */
@DisplayName("MemberLifecycleService — transitions de statut")
class MemberLifecycleServiceTest {

    @InjectMocks
    MemberLifecycleService service;

    @Mock
    MembreOrganisationRepository membreOrgRepository;

    @Mock
    NotificationService notificationService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    // ─── Helpers ─────────────────────────────────────────────────────────────

    private MembreOrganisation buildLien(StatutMembre statut) {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Dupont");
        membre.setPrenom("Marie");

        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom("Association Test");

        MembreOrganisation lien = MembreOrganisation.builder()
                .membre(membre)
                .organisation(org)
                .statutMembre(statut)
                .build();
        lien.setId(UUID.randomUUID());
        return lien;
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  activerMembre
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("activerMembre: EN_ATTENTE_VALIDATION → ACTIF")
    void activerMembre_enAttenteValidation_becomesActif() {
        var lien = buildLien(StatutMembre.EN_ATTENTE_VALIDATION);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.activerMembre(lien.getId(), UUID.randomUUID(), "validation admin");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
        assertThat(result.getMotifStatut()).isEqualTo("validation admin");
        verify(membreOrgRepository).persist(lien);
    }

    @Test
    @DisplayName("activerMembre: INVITE → ACTIF (validation directe)")
    void activerMembre_invite_becomesActif() {
        var lien = buildLien(StatutMembre.INVITE);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.activerMembre(lien.getId(), UUID.randomUUID(), null);

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
        assertThat(result.getMotifStatut()).isEqualTo("Validation par l'administrateur");
    }

    @Test
    @DisplayName("activerMembre: SUSPENDU → ACTIF (réactivation)")
    void activerMembre_suspendu_becomesActif() {
        var lien = buildLien(StatutMembre.SUSPENDU);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.activerMembre(lien.getId(), UUID.randomUUID(), "levée suspension");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
    }

    @Test
    @DisplayName("activerMembre: RADIE → IllegalStateException (transition non autorisée)")
    void activerMembre_radie_throwsIllegalState() {
        var lien = buildLien(StatutMembre.RADIE);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));

        assertThatThrownBy(() -> service.activerMembre(lien.getId(), UUID.randomUUID(), null))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("Transition non autorisée");

        verify(membreOrgRepository, never()).persist(any(MembreOrganisation.class));
    }

    @Test
    @DisplayName("activerMembre: ARCHIVE → IllegalStateException")
    void activerMembre_archive_throwsIllegalState() {
        var lien = buildLien(StatutMembre.ARCHIVE);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));

        assertThatThrownBy(() -> service.activerMembre(lien.getId(), UUID.randomUUID(), null))
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    @DisplayName("activerMembre: ID inexistant → IllegalArgumentException")
    void activerMembre_unknownId_throwsIllegalArgument() {
        UUID unknown = UUID.randomUUID();
        when(membreOrgRepository.findByIdOptional(unknown)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.activerMembre(unknown, UUID.randomUUID(), null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("introuvable");
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  suspendreMembre
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("suspendreMembre: ACTIF → SUSPENDU avec motif")
    void suspendreMembre_actif_becomesSuspenduWithMotif() {
        var lien = buildLien(StatutMembre.ACTIF);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.suspendreMembre(lien.getId(), UUID.randomUUID(), "comportement inapproprié");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.SUSPENDU);
        assertThat(result.getMotifStatut()).isEqualTo("comportement inapproprié");
    }

    @Test
    @DisplayName("suspendreMembre: motif null → motif par défaut")
    void suspendreMembre_nullMotif_usesDefaultMotif() {
        var lien = buildLien(StatutMembre.ACTIF);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.suspendreMembre(lien.getId(), UUID.randomUUID(), null);

        assertThat(result.getMotifStatut()).isEqualTo("Suspension par l'administrateur");
    }

    @Test
    @DisplayName("suspendreMembre: INVITE → IllegalStateException")
    void suspendreMembre_invite_throwsIllegalState() {
        var lien = buildLien(StatutMembre.INVITE);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));

        assertThatThrownBy(() -> service.suspendreMembre(lien.getId(), UUID.randomUUID(), null))
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    @DisplayName("suspendreMembre: EN_ATTENTE_VALIDATION → IllegalStateException")
    void suspendreMembre_enAttente_throwsIllegalState() {
        var lien = buildLien(StatutMembre.EN_ATTENTE_VALIDATION);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));

        assertThatThrownBy(() -> service.suspendreMembre(lien.getId(), UUID.randomUUID(), null))
                .isInstanceOf(IllegalStateException.class);
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  radierMembre
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("radierMembre: ACTIF → RADIE")
    void radierMembre_actif_becomesRadie() {
        var lien = buildLien(StatutMembre.ACTIF);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.radierMembre(lien.getId(), UUID.randomUUID(), "grave faute");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
        assertThat(result.getMotifStatut()).isEqualTo("grave faute");
    }

    @Test
    @DisplayName("radierMembre: SUSPENDU → RADIE (radiation depuis suspension)")
    void radierMembre_suspendu_becomesRadie() {
        var lien = buildLien(StatutMembre.SUSPENDU);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.radierMembre(lien.getId(), UUID.randomUUID(), null);

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
        assertThat(result.getMotifStatut()).isEqualTo("Radiation par l'administrateur");
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  archiverMembre
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("archiverMembre: RADIE → ARCHIVE")
    void archiverMembre_radie_becomesArchive() {
        var lien = buildLien(StatutMembre.RADIE);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.archiverMembre(lien.getId(), "nettoyage annuel");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ARCHIVE);
        assertThat(result.getMotifArchivage()).isEqualTo("nettoyage annuel");
    }

    @Test
    @DisplayName("archiverMembre: ACTIF → ARCHIVE (archivage direct)")
    void archiverMembre_actif_becomesArchive() {
        var lien = buildLien(StatutMembre.ACTIF);
        when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.archiverMembre(lien.getId(), null);

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ARCHIVE);
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  inviterMembre
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("inviterMembre: crée un lien INVITE avec token 32 chars et expiration 7j")
    void inviterMembre_createsInviteLienWithToken() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Sow");
        membre.setPrenom("Amadou");

        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom("Coopérative Agricole");

        when(membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), org.getId()))
                .thenReturn(Optional.empty());
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.inviterMembre(membre, org, UUID.randomUUID(), "TRESORIER");

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.INVITE);
        assertThat(result.getTokenInvitation()).isNotNull().hasSize(32);
        assertThat(result.getRoleOrg()).isEqualTo("TRESORIER");
        assertThat(result.getDateExpirationInvitation())
                .isAfter(LocalDateTime.now().plusDays(6));
    }

    @Test
    @DisplayName("inviterMembre: roleOrg null accepté")
    void inviterMembre_nullRoleOrg_accepted() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Diallo");
        membre.setPrenom("Fatou");
        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom("Tontine");

        when(membreOrgRepository.findByMembreIdAndOrganisationId(any(), any()))
                .thenReturn(Optional.empty());
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.inviterMembre(membre, org, UUID.randomUUID(), null);

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.INVITE);
        assertThat(result.getRoleOrg()).isNull();
    }

    @Test
    @DisplayName("inviterMembre: membre déjà lié → IllegalStateException")
    void inviterMembre_alreadyLinked_throwsIllegalState() {
        Membre membre = new Membre();
        membre.setId(UUID.randomUUID());
        membre.setNom("Test");
        membre.setPrenom("Test");
        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom("Org");

        var existingLien = buildLien(StatutMembre.ACTIF);
        when(membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), org.getId()))
                .thenReturn(Optional.of(existingLien));

        assertThatThrownBy(() -> service.inviterMembre(membre, org, UUID.randomUUID(), null))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("déjà lié");
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  accepterInvitation
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("accepterInvitation: token valide → EN_ATTENTE_VALIDATION, token invalidé")
    @SuppressWarnings("unchecked")
    void accepterInvitation_validToken_becomesEnAttenteValidation() {
        String token = "abc123validtokenabcde1234567890a";
        var lien = buildLien(StatutMembre.INVITE);
        lien.setTokenInvitation(token);
        lien.setDateExpirationInvitation(LocalDateTime.now().plusDays(7));

        PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
        when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
                .thenReturn(mockQuery);
        when(mockQuery.firstResultOptional()).thenReturn(Optional.of(lien));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        var result = service.accepterInvitation(token);

        assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.EN_ATTENTE_VALIDATION);
        assertThat(result.getTokenInvitation()).isNull(); // Token invalidé
    }

    @Test
    @DisplayName("accepterInvitation: invitation expirée → IllegalStateException")
    @SuppressWarnings("unchecked")
    void accepterInvitation_expiredToken_throwsIllegalState() {
        String token = "expiredtokenabcde1234567890abc12";
        var lien = buildLien(StatutMembre.INVITE);
        lien.setTokenInvitation(token);
        lien.setDateExpirationInvitation(LocalDateTime.now().minusDays(1)); // Expiré hier

        PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
        when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
                .thenReturn(mockQuery);
        when(mockQuery.firstResultOptional()).thenReturn(Optional.of(lien));

        assertThatThrownBy(() -> service.accepterInvitation(token))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("expir");
    }

    @Test
    @DisplayName("accepterInvitation: token inexistant → IllegalArgumentException")
    @SuppressWarnings("unchecked")
    void accepterInvitation_unknownToken_throwsIllegalArgument() {
        String token = "unknowntoken0000000000000000000";

        PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
        when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
                .thenReturn(mockQuery);
        when(mockQuery.firstResultOptional()).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.accepterInvitation(token))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Invitation introuvable");
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  expirerInvitations (automatique)
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("expirerInvitations: expire 3 invitations périmées → retourne 3")
    void expirerInvitations_expiresAll_returnsCount() {
        var lien1 = buildLien(StatutMembre.INVITE);
        var lien2 = buildLien(StatutMembre.INVITE);
        var lien3 = buildLien(StatutMembre.INVITE);

        when(membreOrgRepository.findInvitationsExpirees(any(LocalDateTime.class)))
                .thenReturn(List.of(lien1, lien2, lien3));
        doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));

        int count = service.expirerInvitations();

        assertThat(count).isEqualTo(3);
        assertThat(lien1.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
        assertThat(lien2.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
        assertThat(lien3.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
        assertThat(lien1.getMotifStatut()).isEqualTo("Invitation expirée sans réponse");
    }

    @Test
    @DisplayName("expirerInvitations: aucune invitation expirée → retourne 0")
    void expirerInvitations_noneExpired_returnsZero() {
        when(membreOrgRepository.findInvitationsExpirees(any(LocalDateTime.class)))
                .thenReturn(List.of());

        int count = service.expirerInvitations();

        assertThat(count).isZero();
        verify(membreOrgRepository, never()).persist(any(MembreOrganisation.class));
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  envoyerRappelsInvitation (automatique)
    // ═════════════════════════════════════════════════════════════════════════

    @Test
    @DisplayName("envoyerRappelsInvitation: 2 invitations expirant bientôt → retourne 2")
    void envoyerRappelsInvitation_twoExpiringSoon_sendsTwo() {
        var lien1 = buildLien(StatutMembre.INVITE);
        var lien2 = buildLien(StatutMembre.INVITE);

        when(membreOrgRepository.findInvitationsExpirantBientot(
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(lien1, lien2));

        int count = service.envoyerRappelsInvitation();

        assertThat(count).isEqualTo(2);
    }

    @Test
    @DisplayName("envoyerRappelsInvitation: aucune invitation → retourne 0")
    void envoyerRappelsInvitation_none_returnsZero() {
        when(membreOrgRepository.findInvitationsExpirantBientot(
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());

        int count = service.envoyerRappelsInvitation();

        assertThat(count).isZero();
    }
}
