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

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.Mockito.*;

import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneRequest;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneResponse;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.StatutCompteEpargne;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.mapper.mutuelle.epargne.CompteEpargneMapper;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.service.OrganisationService;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.security.Principal;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@QuarkusTest
class CompteEpargneServiceTest {

    @Inject
    CompteEpargneService service;

    @InjectMock
    CompteEpargneRepository repository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    CompteEpargneMapper mapper;

    @InjectMock
    SecurityIdentity securityIdentity;

    @InjectMock
    OrganisationService organisationService;

    @Test
    @DisplayName("creerCompte initialise le statut et génère un numéro de compte")
    void creerCompte_success() {
        UUID membreId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        CompteEpargneRequest request = new CompteEpargneRequest();
        request.setMembreId(membreId.toString());
        request.setOrganisationId(orgId.toString());

        Membre membre = new Membre();
        membre.setId(membreId);
        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("Union Mutuelle");

        CompteEpargne entity = new CompteEpargne();

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.creerCompte(request);

        assertThat(entity.getStatut()).isEqualTo(StatutCompteEpargne.ACTIF);
        assertThat(entity.getDateOuverture()).isEqualTo(LocalDate.now());
        assertThat(entity.getNumeroCompte()).startsWith("UNI-"); // UNI de UNIon
        verify(repository).persist(any(CompteEpargne.class));
    }

    @Test
    @DisplayName("creerCompte avec membre introuvable lance NotFoundException")
    void creerCompte_membreInconnu_throws() {
        UUID membreId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        CompteEpargneRequest request = new CompteEpargneRequest();
        request.setMembreId(membreId.toString());
        request.setOrganisationId(orgId.toString());

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.creerCompte(request))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("creerCompte avec organisation introuvable lance NotFoundException")
    void creerCompte_orgInconnue_throws() {
        UUID membreId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        CompteEpargneRequest request = new CompteEpargneRequest();
        request.setMembreId(membreId.toString());
        request.setOrganisationId(orgId.toString());

        Membre membre = new Membre();
        membre.setId(membreId);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.creerCompte(request))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("creerCompte avec dateOuverture déjà définie ne l'écrase pas")
    void creerCompte_withExistingDateOuverture_preserves() {
        UUID membreId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        CompteEpargneRequest request = new CompteEpargneRequest();
        request.setMembreId(membreId.toString());
        request.setOrganisationId(orgId.toString());

        Membre membre = new Membre();
        membre.setId(membreId);
        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("AB");

        CompteEpargne entity = new CompteEpargne();
        LocalDate existingDate = LocalDate.of(2025, 6, 1);
        entity.setDateOuverture(existingDate);

        when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
        when(mapper.toEntity(request)).thenReturn(entity);
        when(mapper.toDto(entity)).thenReturn(null);

        service.creerCompte(request);

        assertThat(entity.getDateOuverture()).isEqualTo(existingDate);
    }

    @Test
    @DisplayName("getCompteById avec ID inexistant lance NotFoundException")
    void getCompteById_inexistant_throws() {
        UUID id = UUID.randomUUID();
        when(repository.findByIdOptional(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.getCompteById(id))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getCompteById retourne le DTO du compte trouvé")
    void getCompteById_found_returnsDto() {
        UUID id = UUID.randomUUID();
        CompteEpargne entity = new CompteEpargne();
        entity.setId(id);
        CompteEpargneResponse expectedDto = new CompteEpargneResponse();

        when(repository.findByIdOptional(id)).thenReturn(Optional.of(entity));
        when(mapper.toDto(entity)).thenReturn(expectedDto);

        CompteEpargneResponse result = service.getCompteById(id);
        assertThat(result).isEqualTo(expectedDto);
    }

    @Test
    @DisplayName("changerStatut avec ID inexistant lance NotFoundException")
    void changerStatut_inexistant_throws() {
        UUID id = UUID.randomUUID();
        when(repository.findByIdOptional(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.changerStatut(id, StatutCompteEpargne.CLOTURE))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("changerStatut met à jour le statut du compte")
    void changerStatut_found_updatesStatut() {
        UUID id = UUID.randomUUID();
        CompteEpargne entity = new CompteEpargne();
        entity.setId(id);
        entity.setStatut(StatutCompteEpargne.ACTIF);
        CompteEpargneResponse expectedDto = new CompteEpargneResponse();

        when(repository.findByIdOptional(id)).thenReturn(Optional.of(entity));
        when(mapper.toDto(entity)).thenReturn(expectedDto);

        CompteEpargneResponse result = service.changerStatut(id, StatutCompteEpargne.CLOTURE);
        assertThat(entity.getStatut()).isEqualTo(StatutCompteEpargne.CLOTURE);
        assertThat(result).isEqualTo(expectedDto);
    }

    @Test
    @SuppressWarnings("unchecked")
    @DisplayName("getComptesByMembre retourne une liste")
    void getComptesByMembre_returnsList() {
        UUID membreId = UUID.randomUUID();
        CompteEpargne entity = new CompteEpargne();
        CompteEpargneResponse dto = new CompteEpargneResponse();

        PanacheQuery<CompteEpargne> mockQuery = mock(PanacheQuery.class);
        when(repository.find(anyString(), (Object[]) any())).thenReturn(mockQuery);
        when(mockQuery.stream()).thenReturn(List.of(entity).stream());
        when(mapper.toDto(entity)).thenReturn(dto);

        List<CompteEpargneResponse> result = service.getComptesByMembre(membreId);

        assertThat(result).hasSize(1).containsExactly(dto);
    }

    @Test
    @SuppressWarnings("unchecked")
    @DisplayName("getComptesByOrganisation retourne une liste")
    void getComptesByOrganisation_returnsList() {
        UUID orgId = UUID.randomUUID();
        CompteEpargne entity = new CompteEpargne();
        CompteEpargneResponse dto = new CompteEpargneResponse();

        PanacheQuery<CompteEpargne> mockQuery = mock(PanacheQuery.class);
        when(repository.find(anyString(), (Object[]) any())).thenReturn(mockQuery);
        when(mockQuery.stream()).thenReturn(List.of(entity).stream());
        when(mapper.toDto(entity)).thenReturn(dto);

        List<CompteEpargneResponse> result = service.getComptesByOrganisation(orgId);

        assertThat(result).hasSize(1).containsExactly(dto);
    }

    @Test
    @DisplayName("getMesComptes avec principal null retourne liste vide")
    void getMesComptes_principalNull_retourneListeVide() {
        when(securityIdentity.getPrincipal()).thenReturn(null);

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("getMesComptes avec email vide retourne liste vide")
    void getMesComptes_emailVide_retourneListeVide() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("  ");

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("getMesComptes avec rôle MEMBRE et membre introuvable retourne liste vide")
    void getMesComptes_membreRole_membreInconnu_retourneListeVide() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("test@test.com");
        when(securityIdentity.getRoles()).thenReturn(Set.of("MEMBRE"));
        when(membreRepository.findByEmail("test@test.com")).thenReturn(Optional.empty());

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("getMesComptes avec rôle ADMIN et aucune organisation retourne liste vide")
    void getMesComptes_adminRole_aucuneOrg_retourneListeVide() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("admin@test.com");
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin@test.com"))
                .thenReturn(List.of());

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("getMesComptes avec rôle ADMIN et organisations null retourne liste vide (branche orgs == null)")
    void getMesComptes_adminRole_orgsNull_retourneListeVide() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("admin2@test.com");
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));
        when(organisationService.listerOrganisationsPourUtilisateur("admin2@test.com"))
                .thenReturn(null);

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @SuppressWarnings("unchecked")
    @DisplayName("getMesComptes avec rôle ADMIN et organisations retourne comptes")
    void getMesComptes_adminRole_avecOrgs_retourneComptes() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("admin@test.com");
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN"));

        UUID orgId = UUID.randomUUID();
        Organisation org = new Organisation();
        org.setId(orgId);
        when(organisationService.listerOrganisationsPourUtilisateur("admin@test.com"))
                .thenReturn(List.of(org));

        CompteEpargne entity = new CompteEpargne();
        CompteEpargneResponse dto = new CompteEpargneResponse();
        PanacheQuery<CompteEpargne> mockQuery = mock(PanacheQuery.class);
        when(repository.find(anyString(), (Object[]) any())).thenReturn(mockQuery);
        when(mockQuery.stream()).thenReturn(List.of(entity).stream());
        when(mapper.toDto(entity)).thenReturn(dto);

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).hasSize(1);
    }

    @Test
    @SuppressWarnings("unchecked")
    @DisplayName("getMesComptes avec rôle MEMBRE et membre trouvé couvre lambda .map(m -> getComptesByMembre(m.getId()))")
    void getMesComptes_membreRole_membreTrouve_retourneComptes() {
        // Arrange
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("membre@test.com");
        // Rôle non-ADMIN → branche membre → findByEmail → .map(m -> getComptesByMembre(...))
        when(securityIdentity.getRoles()).thenReturn(Set.of("MEMBRE"));

        Membre membre = new Membre();
        UUID membreId = UUID.randomUUID();
        membre.setId(membreId);
        when(membreRepository.findByEmail("membre@test.com")).thenReturn(Optional.of(membre));

        CompteEpargne entity = new CompteEpargne();
        CompteEpargneResponse dto = new CompteEpargneResponse();
        PanacheQuery<CompteEpargne> mockQuery = mock(PanacheQuery.class);
        when(repository.find(anyString(), (Object[]) any())).thenReturn(mockQuery);
        when(mockQuery.stream()).thenReturn(List.of(entity).stream());
        when(mapper.toDto(entity)).thenReturn(dto);

        // Act — déclenche lambda$4 : .map(m -> getComptesByMembre(m.getId()))
        List<CompteEpargneResponse> result = service.getMesComptes();

        // Assert
        assertThat(result).hasSize(1);
    }

    @Test
    @DisplayName("getMesComptes avec rôle ADMIN_ORGANISATION → branche L115 contains('ADMIN_ORGANISATION')=true")
    void getMesComptes_adminOrgRole_brancheAdminOrg() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("orgadmin@test.com");
        // ADMIN_ORGANISATION (sans ADMIN) → roles.contains("ADMIN")=false → contains("ADMIN_ORGANISATION")=true
        when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
        when(organisationService.listerOrganisationsPourUtilisateur("orgadmin@test.com"))
                .thenReturn(List.of());

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }

    @Test
    @DisplayName("getMesComptes avec rôles null → skip branche ADMIN → chemin membre (L109 roles == null)")
    void getMesComptes_rolesNull_cheminMembre() {
        Principal principal = mock(Principal.class);
        when(securityIdentity.getPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("rolenull@test.com");
        // roles == null → condition L115: roles != null = false → skip ADMIN → chemin membre
        when(securityIdentity.getRoles()).thenReturn(null);
        when(membreRepository.findByEmail("rolenull@test.com")).thenReturn(Optional.empty());

        List<CompteEpargneResponse> result = service.getMesComptes();

        assertThat(result).isEmpty();
    }
}
