package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.MembreRole;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.repository.MembreRoleRepository;
import dev.lions.unionflow.server.repository.RoleRepository;
import jakarta.persistence.EntityManager;
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.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.Field;
import java.util.Optional;
import java.util.UUID;

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.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * Tests unitaires pour {@link MembreRoleSyncService}.
 * Couverture 100% lignes/branches/méthodes.
 *
 * <p>Cas couverts :
 * <ul>
 *   <li>mo == null → retour immédiat</li>
 *   <li>mo.getId() == null → retour immédiat</li>
 *   <li>mo.getOrganisation() == null → retour immédiat</li>
 *   <li>MembreRole ORGADMIN existant → retour immédiat (idempotence)</li>
 *   <li>Rôle ORGADMIN absent de la table → log warn, pas de persist</li>
 *   <li>Rôle ORGADMIN présent → MembreRole créé et persisté (happy path)</li>
 *   <li>Happy path avec membre null (mo.getMembre() == null) → log utilise "?"</li>
 * </ul>
 */
@DisplayName("Tests MembreRoleSyncService")
class MembreRoleSyncServiceTest {

    @InjectMocks
    MembreRoleSyncService service;

    @Mock
    MembreRoleRepository membreRoleRepository;

    @Mock
    RoleRepository roleRepository;

    @Mock
    EntityManager entityManager;

    @BeforeEach
    void setUp() throws Exception {
        MockitoAnnotations.openMocks(this);
        // @PersistenceContext EntityManager non injecté par @InjectMocks — injection via réflexion
        injectField(service, "entityManager", entityManager);
    }

    private void injectField(Object target, String fieldName, Object value) throws Exception {
        Field f = target.getClass().getDeclaredField(fieldName);
        f.setAccessible(true);
        f.set(target, value);
    }

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

    private Organisation buildOrg() {
        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom("Org Test");
        return org;
    }

    private Membre buildMembre() {
        Membre m = new Membre();
        m.setId(UUID.randomUUID());
        return m;
    }

    private MembreOrganisation buildMO(Organisation org, Membre membre) {
        MembreOrganisation mo = new MembreOrganisation();
        mo.setId(UUID.randomUUID());
        mo.setOrganisation(org);
        mo.setMembre(membre);
        return mo;
    }

    private Role buildOrgAdminRole() {
        Role role = new Role();
        role.setId(UUID.randomUUID());
        role.setCode("ORGADMIN");
        role.setLibelle("Admin Organisation");
        return role;
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  Gardes initiales (null checks)
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("Gardes initiales — retour immédiat")
    class GardesInitiales {

        @Test
        @DisplayName("mo == null — retour immédiat sans aucune interaction")
        void moNull_retourImmediat() {
            assertThatCode(() -> service.ensureOrgAdminRole(null))
                    .doesNotThrowAnyException();

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }

        @Test
        @DisplayName("mo.getId() == null — retour immédiat sans aucune interaction")
        void moIdNull_retourImmediat() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setId(null); // id non défini
            mo.setOrganisation(buildOrg());

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }

        @Test
        @DisplayName("mo.getOrganisation() == null — retour immédiat sans aucune interaction")
        void moOrganisationNull_retourImmediat() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setId(UUID.randomUUID());
            mo.setOrganisation(null); // organisation non définie

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  Idempotence — MembreRole déjà existant
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("Idempotence — ORGADMIN déjà présent")
    class Idempotence {

        @Test
        @DisplayName("MembreRole ORGADMIN actif existant — retour immédiat, pas de persist")
        void orgAdminExistant_retourImmediat() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);

            // Simuler qu'il existe déjà un MembreRole ORGADMIN actif
            when(membreRoleRepository.count(
                    eq("membreOrganisation.id = ?1 AND role.code = ?2 AND actif = true"),
                    eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(1L);

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            verify(membreRoleRepository).count(anyString(), eq(mo.getId()), eq("ORGADMIN"));
            verifyNoInteractions(roleRepository, entityManager);
        }

        @Test
        @DisplayName("Plusieurs MembreRoles ORGADMIN existants — toujours retour immédiat")
        void plusieursOrgAdminExistants_retourImmediat() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);

            when(membreRoleRepository.count(anyString(), eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(3L);

            service.ensureOrgAdminRole(mo);

            verifyNoInteractions(roleRepository, entityManager);
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  Rôle ORGADMIN absent de la table roles
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("Rôle ORGADMIN absent de la table roles")
    class RoleOrgAdminAbsent {

        @Test
        @DisplayName("Rôle ORGADMIN introuvable — log warn, pas de persist")
        void roleOrgAdminAbsent_pasDePersist() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);

            when(membreRoleRepository.count(anyString(), eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(0L);
            when(roleRepository.findByCode("ORGADMIN"))
                    .thenReturn(Optional.empty());

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            verify(roleRepository).findByCode("ORGADMIN");
            verify(entityManager, never()).persist(any());
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  Happy path — création du MembreRole ORGADMIN
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("Happy path — création du MembreRole ORGADMIN")
    class CreationMembreRole {

        @Test
        @DisplayName("MembreRole ORGADMIN absent + rôle présent — MembreRole créé et persisté")
        void aucunOrgAdmin_rolePresent_membreRoleCreeEtPersite() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);
            Role orgAdminRole = buildOrgAdminRole();

            when(membreRoleRepository.count(anyString(), eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(0L);
            when(roleRepository.findByCode("ORGADMIN"))
                    .thenReturn(Optional.of(orgAdminRole));

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            ArgumentCaptor<MembreRole> captor = ArgumentCaptor.forClass(MembreRole.class);
            verify(entityManager).persist(captor.capture());

            MembreRole created = captor.getValue();
            assertThat(created.getMembreOrganisation()).isEqualTo(mo);
            assertThat(created.getOrganisation()).isEqualTo(org);
            assertThat(created.getRole()).isEqualTo(orgAdminRole);
            assertThat(created.getActif()).isTrue();
            assertThat(created.getDateDebut()).isNotNull();
        }

        @Test
        @DisplayName("Happy path — mo.getMembre() == null — log utilise '?' mais persist quand même")
        void moMembreNull_logUtiliseInterrogation_persistQuandMeme() {
            Organisation org = buildOrg();
            // MembreOrganisation sans membre (getMembre() retourne null)
            MembreOrganisation mo = new MembreOrganisation();
            mo.setId(UUID.randomUUID());
            mo.setOrganisation(org);
            mo.setMembre(null); // membre null — branche "mo.getMembre() != null ? ... : '?'"

            Role orgAdminRole = buildOrgAdminRole();

            when(membreRoleRepository.count(anyString(), eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(0L);
            when(roleRepository.findByCode("ORGADMIN"))
                    .thenReturn(Optional.of(orgAdminRole));

            assertThatCode(() -> service.ensureOrgAdminRole(mo))
                    .doesNotThrowAnyException();

            ArgumentCaptor<MembreRole> captor = ArgumentCaptor.forClass(MembreRole.class);
            verify(entityManager).persist(captor.capture());

            MembreRole created = captor.getValue();
            assertThat(created.getMembreOrganisation()).isEqualTo(mo);
            assertThat(created.getOrganisation()).isEqualTo(org);
            assertThat(created.getRole()).isEqualTo(orgAdminRole);
            assertThat(created.getActif()).isTrue();
            assertThat(created.getDateDebut()).isNotNull();
        }

        @Test
        @DisplayName("Vérification que count est appelé avec les bons arguments")
        void countAppeleAvecBonsArguments() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);

            when(membreRoleRepository.count(anyString(), any(), anyString()))
                    .thenReturn(1L); // Déjà existant → retour immédiat

            service.ensureOrgAdminRole(mo);

            verify(membreRoleRepository).count(
                    "membreOrganisation.id = ?1 AND role.code = ?2 AND actif = true",
                    mo.getId(),
                    "ORGADMIN");
        }

        @Test
        @DisplayName("MembreRole créé — dateDebut est aujourd'hui")
        void membreRoleCree_dateDebutAujourdhui() {
            Organisation org = buildOrg();
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre);
            Role orgAdminRole = buildOrgAdminRole();

            when(membreRoleRepository.count(anyString(), eq(mo.getId()), eq("ORGADMIN")))
                    .thenReturn(0L);
            when(roleRepository.findByCode("ORGADMIN"))
                    .thenReturn(Optional.of(orgAdminRole));

            service.ensureOrgAdminRole(mo);

            ArgumentCaptor<MembreRole> captor = ArgumentCaptor.forClass(MembreRole.class);
            verify(entityManager).persist(captor.capture());

            MembreRole created = captor.getValue();
            // dateDebut doit être la date d'aujourd'hui
            assertThat(created.getDateDebut()).isEqualTo(java.time.LocalDate.now());
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  Combinaisons de gardes — couverture complète de la condition composée
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("Couverture complète de la condition composée mo==null||id==null||org==null")
    class ConditionsComposees {

        @Test
        @DisplayName("mo non null mais id null et org null — retour immédiat")
        void idNullEtOrgNull_retourImmediat() {
            MembreOrganisation mo = new MembreOrganisation();
            // id null, organisation null

            service.ensureOrgAdminRole(mo);

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }

        @Test
        @DisplayName("mo non null, id présent, org null — retour immédiat")
        void idPresentEtOrgNull_retourImmediat() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setId(UUID.randomUUID());
            mo.setOrganisation(null);

            service.ensureOrgAdminRole(mo);

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }

        @Test
        @DisplayName("mo non null, id null, org présente — retour immédiat (id null)")
        void idNullEtOrgPresente_retourImmediat() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setId(null);
            mo.setOrganisation(buildOrg());

            service.ensureOrgAdminRole(mo);

            verifyNoInteractions(membreRoleRepository, roleRepository, entityManager);
        }
    }
}
