package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.when;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository;

/**
 * Tests des branches privées de MembreImportExportService inaccessibles via l'API publique.
 * Utilise la réflexion Java pour appeler directement les méthodes privées.
 */
@ExtendWith(MockitoExtension.class)
class MembreImportExportServicePrivateBranchesTest {

    @Mock
    MembreRepository membreRepository;
    @Mock
    OrganisationRepository organisationRepository;
    @Mock
    MembreService membreService;
    @Mock
    SouscriptionOrganisationRepository souscriptionOrganisationRepository;
    @Mock
    MembreOrganisationRepository membreOrganisationRepository;

    @InjectMocks
    MembreImportExportService service;

    private Row validRow;
    private XSSFWorkbook workbook;

    @BeforeEach
    void setUp() throws Exception {
        workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Test");
        validRow = sheet.createRow(0);
        validRow.createCell(0).setCellValue("TestValue");
    }

    // =========================================================================
    // getCellValueAsString — branches L479 (columnIndex==null || row==null)
    // =========================================================================

    @Test
    @DisplayName("getCellValueAsString: columnIndex==null → L479 true → retourne null")
    void getCellValueAsString_columnIndexNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsString", Row.class, Integer.class);
        m.setAccessible(true);

        Object result = m.invoke(service, validRow, (Integer) null);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("getCellValueAsString: row==null → L479 true → retourne null")
    void getCellValueAsString_rowNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsString", Row.class, Integer.class);
        m.setAccessible(true);

        Object result = m.invoke(service, (Row) null, 0);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("getCellValueAsString: cell==null (index valide mais pas de cellule) → L483 true → retourne null")
    void getCellValueAsString_cellNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsString", Row.class, Integer.class);
        m.setAccessible(true);

        // La colonne 99 n'a pas de cellule dans validRow → row.getCell(99) == null
        Object result = m.invoke(service, validRow, 99);

        assertThat(result).isNull();
    }

    // =========================================================================
    // getCellValueAsDate — branches L509 (columnIndex==null || row==null)
    // =========================================================================

    @Test
    @DisplayName("getCellValueAsDate: columnIndex==null → L509 true → retourne null")
    void getCellValueAsDate_columnIndexNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Object result = m.invoke(service, validRow, (Integer) null);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("getCellValueAsDate: row==null → L509 true → retourne null")
    void getCellValueAsDate_rowNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Object result = m.invoke(service, (Row) null, 0);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("getCellValueAsDate: cell==null (index valide sans cellule) → L513 true → retourne null")
    void getCellValueAsDate_cellNull_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        // Colonne 99 n'a pas de cellule
        Object result = m.invoke(service, validRow, 99);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("getCellValueAsDate: cellule STRING avec date valide → parseDate appelé → retourne LocalDate")
    void getCellValueAsDate_stringCellAvecDateValide_retourneLocalDate() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(1);
        Cell dateCell = row.createCell(0);
        dateCell.setCellValue("15/03/1990"); // STRING cell avec date au format dd/MM/yyyy

        Object result = m.invoke(service, row, 0);

        assertThat(result).isNotNull().isInstanceOf(LocalDate.class);
    }

    @Test
    @DisplayName("getCellValueAsDate: cellule STRING avec date invalide → exception catchée → retourne null")
    void getCellValueAsDate_stringCellDateInvalide_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(2);
        Cell dateCell = row.createCell(0);
        dateCell.setCellValue("NOT_A_DATE"); // STRING cell avec format invalide

        Object result = m.invoke(service, row, 0);

        assertThat(result).isNull();
    }

    // =========================================================================
    // parseDate — branches L535 (dateStr==null || dateStr.trim().isEmpty())
    // =========================================================================

    @Test
    @DisplayName("parseDate: null → L535 dateStr==null → true → retourne null")
    void parseDate_null_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod("parseDate", String.class);
        m.setAccessible(true);

        Object result = m.invoke(service, (String) null);

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("parseDate: chaîne vide → L535 trim().isEmpty() → true → retourne null")
    void parseDate_chaineVide_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod("parseDate", String.class);
        m.setAccessible(true);

        Object result = m.invoke(service, "");

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("parseDate: chaîne espaces → L535 trim().isEmpty() → true → retourne null")
    void parseDate_chaineEspaces_retourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod("parseDate", String.class);
        m.setAccessible(true);

        Object result = m.invoke(service, "   ");

        assertThat(result).isNull();
    }

    @Test
    @DisplayName("parseDate: format dd/MM/yyyy valide → L535 false → retourne LocalDate")
    void parseDate_formatValide_retourneLocalDate() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod("parseDate", String.class);
        m.setAccessible(true);

        Object result = m.invoke(service, "15/03/1990");

        assertThat(result).isNotNull().isInstanceOf(LocalDate.class);
    }

    @Test
    @DisplayName("parseDate: format yyyy-MM-dd valide → L535 false → retourne LocalDate")
    void parseDate_formatISOValide_retourneLocalDate() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod("parseDate", String.class);
        m.setAccessible(true);

        Object result = m.invoke(service, "1990-03-15");

        assertThat(result).isNotNull().isInstanceOf(LocalDate.class);
    }

    // =========================================================================
    // chiffrerExcel — branche L789 (protection == null → false)
    // Pour couvrir la branche false (protection != null), on doit passer
    // les bytes d'un workbook qui a DÉJÀ une protection WorkbookProtection.
    // =========================================================================

    @Test
    @DisplayName("chiffrerExcel: workbook sans protection existante → L789 protection==null → true → addNewWorkbookProtection")
    void chiffrerExcel_sansProtectionExistante_L789True() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "chiffrerExcel", byte[].class, String.class);
        m.setAccessible(true);

        // Créer un workbook SANS protection
        byte[] excelData = createExcelWithoutProtection();

        byte[] result = (byte[]) m.invoke(service, excelData, "MonMotDePasse");

        assertThat(result).isNotNull().hasSizeGreaterThan(0);

        // Vérifier que la protection a été ajoutée
        try (XSSFWorkbook wb = new XSSFWorkbook(new java.io.ByteArrayInputStream(result))) {
            assertThat(wb.getCTWorkbook().getWorkbookProtection()).isNotNull();
        }
    }

    @Test
    @DisplayName("chiffrerExcel: workbook avec protection déjà présente → L789 protection==null → false → utilise protection existante")
    void chiffrerExcel_avecProtectionExistante_L789False() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "chiffrerExcel", byte[].class, String.class);
        m.setAccessible(true);

        // Créer un workbook AVEC protection déjà présente
        byte[] excelDataWithProtection = createExcelWithExistingProtection();

        byte[] result = (byte[]) m.invoke(service, excelDataWithProtection, "NouveauMotDePasse");

        assertThat(result).isNotNull().hasSizeGreaterThan(0);

        // La protection existante doit être réutilisée (pas ajout d'une nouvelle)
        try (XSSFWorkbook wb = new XSSFWorkbook(new java.io.ByteArrayInputStream(result))) {
            assertThat(wb.getCTWorkbook().getWorkbookProtection()).isNotNull();
        }
    }

    // =========================================================================
    // getCellValueAsString — branche NUMERIC date formatée
    // =========================================================================

    @Test
    @DisplayName("getCellValueAsString: cellule NUMERIC date formatée → retourne toString de la date")
    void getCellValueAsString_numericDateFormatee_retourneString() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsString", Row.class, Integer.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(3);

        CellStyle dateStyle = workbook.createCellStyle();
        CreationHelper helper = workbook.getCreationHelper();
        dateStyle.setDataFormat(helper.createDataFormat().getFormat("dd/MM/yyyy"));

        Cell dateCell = row.createCell(0);
        dateCell.setCellValue(java.time.LocalDateTime.of(1990, 3, 15, 0, 0));
        dateCell.setCellStyle(dateStyle);

        Object result = m.invoke(service, row, 0);

        assertThat(result).isNotNull().asString().isNotEmpty();
    }

    // =========================================================================
    // getCellValueAsDate — NUMERIC date-formatted cell (L518)
    // =========================================================================

    @Test
    @DisplayName("getCellValueAsDate: cellule NUMERIC date-formatée → L518 true → retourne LocalDate")
    void getCellValueAsDate_numericDateFormatted_retourneLocalDate() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(10);

        // Créer une cellule NUMERIC avec format date → DateUtil.isCellDateFormatted() = true
        CellStyle dateStyle = workbook.createCellStyle();
        CreationHelper helper = workbook.getCreationHelper();
        dateStyle.setDataFormat(helper.createDataFormat().getFormat("dd/MM/yyyy"));

        Cell dateCell = row.createCell(0);
        dateCell.setCellValue(java.time.LocalDateTime.of(1990, 3, 15, 0, 0));
        dateCell.setCellStyle(dateStyle);

        Object result = m.invoke(service, row, 0);

        assertThat(result).isNotNull().isInstanceOf(LocalDate.class);
    }

    // =========================================================================
    // lireLigneExcel — branches L353 (email empty), L356 (telephone empty), L385 (org présente)
    // =========================================================================

    @Test
    @DisplayName("lireLigneExcel: email vide (not null) → L353 B=true → IllegalArgumentException")
    void lireLigneExcel_emailEmpty_l353BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(20);
        row.createCell(0).setCellValue("Dupont");     // nom
        row.createCell(1).setCellValue("Jean");       // prenom
        row.createCell(2).setCellValue("");           // email vide → isEmpty()=true
        row.createCell(3).setCellValue("+22177000000"); // telephone

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        colonnes.put("email", 2);
        colonnes.put("telephone", 3);

        assertThatThrownBy(() -> m.invoke(service, row, colonnes, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("email");
    }

    @Test
    @DisplayName("lireLigneExcel: telephone vide (not null) → L356 B=true → IllegalArgumentException")
    void lireLigneExcel_telephoneEmpty_l356BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(21);
        row.createCell(0).setCellValue("Martin");
        row.createCell(1).setCellValue("Marie");
        row.createCell(2).setCellValue("marie@test.com");
        row.createCell(3).setCellValue("");   // telephone vide

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        colonnes.put("email", 2);
        colonnes.put("telephone", 3);

        assertThatThrownBy(() -> m.invoke(service, row, colonnes, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("t\u00e9l\u00e9phone");
    }

    @Test
    @DisplayName("lireLigneExcel: organisationId non-null + org présente → L385 true → retourne membre")
    void lireLigneExcel_orgPresente_l385TrueRetoursMembre() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(22);
        row.createCell(0).setCellValue("Bernard");
        row.createCell(1).setCellValue("Paul");
        row.createCell(2).setCellValue("paul@test.com");
        row.createCell(3).setCellValue("+22188000000");

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        colonnes.put("email", 2);
        colonnes.put("telephone", 3);

        UUID orgId = UUID.randomUUID();
        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("Organisation Test L385");
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));

        Object result = m.invoke(service, row, colonnes, orgId, "ACTIF");

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // lireLigneCSV — branches L407/L410/L413/L416 (champs vides),
    //                L428/L440 (dates non-vides), L449 (org présente)
    // =========================================================================

    @Test
    @DisplayName("lireLigneCSV: nom vide (not null) → L407 B=true → IllegalArgumentException")
    void lireLigneCSV_nomEmpty_l407BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("", "User", "user@test.com", "+1234567890", "", "");

        assertThatThrownBy(() -> m.invoke(service, record, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("nom");
    }

    @Test
    @DisplayName("lireLigneCSV: prenom vide (not null) → L410 B=true → IllegalArgumentException")
    void lireLigneCSV_prenomEmpty_l410BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Dupont", "", "user@test.com", "+1234567890", "", "");

        assertThatThrownBy(() -> m.invoke(service, record, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("pr\u00e9nom");
    }

    @Test
    @DisplayName("lireLigneCSV: email vide (not null) → L413 B=true → IllegalArgumentException")
    void lireLigneCSV_emailEmpty_l413BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Dupont", "Jean", "", "+1234567890", "", "");

        assertThatThrownBy(() -> m.invoke(service, record, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("email");
    }

    @Test
    @DisplayName("lireLigneCSV: telephone vide (not null) → L416 B=true → IllegalArgumentException")
    void lireLigneCSV_telephoneEmpty_l416BTrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Dupont", "Jean", "jean@test.com", "", "", "");

        assertThatThrownBy(() -> m.invoke(service, record, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("t\u00e9l\u00e9phone");
    }

    @Test
    @DisplayName("lireLigneCSV: dateNaissance non-vide → L428 true → setDateNaissance appelé")
    void lireLigneCSV_dateNaissanceNonEmpty_l428TrueSetDate() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Dupont", "Jean", "jean@test.com", "+22177000000",
                "15/03/1990", "");

        Object result = m.invoke(service, record, null, "ACTIF");

        assertThat(result).isNotNull();
        dev.lions.unionflow.server.entity.Membre membre = (dev.lions.unionflow.server.entity.Membre) result;
        assertThat(membre.getDateNaissance()).isEqualTo(LocalDate.of(1990, 3, 15));
    }

    @Test
    @DisplayName("lireLigneCSV: dateAdhesion non-vide → L440 true → branche couverte (body no-op)")
    void lireLigneCSV_dateAdhesionNonEmpty_l440TrueBranchCovered() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Martin", "Sophie", "sophie@test.com", "+22188000000",
                "", "20/06/2020");

        Object result = m.invoke(service, record, null, "ACTIF");

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("lireLigneCSV: organisationId non-null + org présente → L449 true → branche couverte")
    void lireLigneCSV_orgPresente_l449TrueRetoursMembre() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Bernard", "Alice", "alice@test.com", "+22199000000",
                "", "");

        UUID orgId = UUID.randomUUID();
        Organisation org = new Organisation();
        org.setId(orgId);
        org.setNom("Organisation CSV L449");
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));

        Object result = m.invoke(service, record, orgId, "ACTIF");

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // Helpers privés
    // =========================================================================

    /**
     * Crée un CSVRecord avec les colonnes standard de lireLigneCSV.
     */
    private CSVRecord createCsvRecord(String nom, String prenom, String email, String telephone,
            String dateNaissance, String dateAdhesion) throws Exception {
        String csvLine = String.format("%s,%s,%s,%s,%s,%s",
                escapeCsvValue(nom), escapeCsvValue(prenom), escapeCsvValue(email),
                escapeCsvValue(telephone), escapeCsvValue(dateNaissance), escapeCsvValue(dateAdhesion));

        CSVFormat format = CSVFormat.DEFAULT.builder()
                .setHeader("nom", "prenom", "email", "telephone", "date_naissance", "date_adhesion")
                .build();
        try (CSVParser parser = CSVParser.parse(csvLine, format)) {
            return parser.getRecords().get(0);
        }
    }

    private String escapeCsvValue(String value) {
        if (value == null) return "";
        if (value.contains(",") || value.contains("\"")) {
            return "\"" + value.replace("\"", "\"\"") + "\"";
        }
        return value;
    }

    private byte[] createExcelWithoutProtection() throws Exception {
        try (XSSFWorkbook wb = new XSSFWorkbook();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            wb.createSheet("Membres");
            wb.write(out);
            return out.toByteArray();
        }
    }

    // =========================================================================
    // lireLigneExcel — L353 A=true (email==null) : colonnes sans "email" → null
    //                  L356 A=true (telephone==null) : colonnes sans "telephone" → null
    //                  L385 false : org.isPresent()=false
    // =========================================================================

    @Test
    @DisplayName("lireLigneExcel: email==null (pas dans colonnes) → L353 A=true → IllegalArgumentException")
    void lireLigneExcel_emailNull_l353ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(40);
        row.createCell(0).setCellValue("Dupont");  // nom
        row.createCell(1).setCellValue("Jean");    // prenom
        // pas de cellule email

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        // "email" absent → colonnes.get("email")=null → getCellValueAsString(row,null)=null → email=null → L353 A=true

        assertThatThrownBy(() -> m.invoke(service, row, colonnes, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("email");
    }

    @Test
    @DisplayName("lireLigneExcel: telephone==null (pas dans colonnes) → L356 A=true → IllegalArgumentException")
    void lireLigneExcel_telephoneNull_l356ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(41);
        row.createCell(0).setCellValue("Martin");
        row.createCell(1).setCellValue("Sophie");
        row.createCell(2).setCellValue("sophie@test.com");
        // pas de cellule telephone

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        colonnes.put("email", 2);
        // "telephone" absent → getCellValueAsString(row,null)=null → L356 A=true

        assertThatThrownBy(() -> m.invoke(service, row, colonnes, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("t\u00e9l\u00e9phone");
    }

    @Test
    @DisplayName("lireLigneExcel: org non trouvée → L385 false (isPresent=false) → retourne membre sans org")
    void lireLigneExcel_orgAbsente_l385FalseBranch() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneExcel", Row.class, Map.class, UUID.class, String.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(42);
        row.createCell(0).setCellValue("Bernard");
        row.createCell(1).setCellValue("Paul");
        row.createCell(2).setCellValue("paul@test.com");
        row.createCell(3).setCellValue("+22177000000");

        Map<String, Integer> colonnes = new HashMap<>();
        colonnes.put("nom", 0);
        colonnes.put("prenom", 1);
        colonnes.put("email", 2);
        colonnes.put("telephone", 3);

        UUID orgId = UUID.randomUUID();
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.empty());

        Object result = m.invoke(service, row, colonnes, orgId, "ACTIF");

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // lireLigneCSV — branches A=true (null) pour L407/L410/L413/L416
    // Utilise un mock de CSVRecord pour retourner null sur record.get(column)
    // =========================================================================

    @Test
    @DisplayName("lireLigneCSV: nom==null → L407 A=true → IllegalArgumentException")
    void lireLigneCSV_nomNull_l407ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn(null);

        assertThatThrownBy(() -> m.invoke(service, mockRecord, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("nom");
    }

    @Test
    @DisplayName("lireLigneCSV: prenom==null → L410 A=true → IllegalArgumentException")
    void lireLigneCSV_prenomNull_l410ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn("Dupont");
        org.mockito.Mockito.when(mockRecord.get("prenom")).thenReturn(null);

        assertThatThrownBy(() -> m.invoke(service, mockRecord, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("pr\u00e9nom");
    }

    @Test
    @DisplayName("lireLigneCSV: email==null → L413 A=true → IllegalArgumentException")
    void lireLigneCSV_emailNull_l413ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn("Dupont");
        org.mockito.Mockito.when(mockRecord.get("prenom")).thenReturn("Jean");
        org.mockito.Mockito.when(mockRecord.get("email")).thenReturn(null);

        assertThatThrownBy(() -> m.invoke(service, mockRecord, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("email");
    }

    @Test
    @DisplayName("lireLigneCSV: telephone==null → L416 A=true → IllegalArgumentException")
    void lireLigneCSV_telephoneNull_l416ATrueThrows() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn("Dupont");
        org.mockito.Mockito.when(mockRecord.get("prenom")).thenReturn("Jean");
        org.mockito.Mockito.when(mockRecord.get("email")).thenReturn("jean@test.com");
        org.mockito.Mockito.when(mockRecord.get("telephone")).thenReturn(null);

        assertThatThrownBy(() -> m.invoke(service, mockRecord, null, "ACTIF"))
                .cause()
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("t\u00e9l\u00e9phone");
    }

    // =========================================================================
    // lireLigneCSV — branches L454 triple-OR : typeMembreDefaut null/vide/autre
    // L454: membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty()
    //                        || "ACTIF".equals(typeMembreDefaut));
    // =========================================================================

    @Test
    @DisplayName("lireLigneCSV: typeMembreDefaut==null → L454 A=true → setActif(true)")
    void lireLigneCSV_typeMembreDefautNull_l454ATrueSetActifTrue() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Dupont", "Jean", "jean@test.com", "+22177000000", "", "");

        dev.lions.unionflow.server.entity.Membre result =
                (dev.lions.unionflow.server.entity.Membre) m.invoke(service, record, null, (String) null);

        assertThat(result).isNotNull();
        assertThat(result.getActif()).isTrue();
    }

    @Test
    @DisplayName("lireLigneCSV: typeMembreDefaut='' → L454 A=false B=true (isEmpty) → setActif(true)")
    void lireLigneCSV_typeMembreDefautVide_l454BTrueSetActifTrue() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Martin", "Sophie", "sophie@test.com", "+22188000000", "", "");

        dev.lions.unionflow.server.entity.Membre result =
                (dev.lions.unionflow.server.entity.Membre) m.invoke(service, record, null, "");

        assertThat(result).isNotNull();
        assertThat(result.getActif()).isTrue();
    }

    @Test
    @DisplayName("lireLigneCSV: typeMembreDefaut='MEMBRE' → L454 A=false B=false C=false → setActif(false)")
    void lireLigneCSV_typeMembreDefautAutre_l454CFalseSetActifFalse() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Bernard", "Alice", "alice@test.com", "+22199000000", "", "");

        dev.lions.unionflow.server.entity.Membre result =
                (dev.lions.unionflow.server.entity.Membre) m.invoke(service, record, null, "MEMBRE");

        assertThat(result).isNotNull();
        assertThat(result.getActif()).isFalse();
    }

    // =========================================================================
    // lireLigneCSV — L428 A=false (dateNaissanceStr==null) via mock CSVRecord
    //                L440 A=false (dateAdhesionStr==null) via mock CSVRecord
    //                L449 false (org.isPresent()=false)
    // =========================================================================

    @Test
    @DisplayName("lireLigneCSV: dateNaissanceStr==null → L428 A=false → skip if body")
    void lireLigneCSV_dateNaissanceNull_l428AFalseBranch() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn("Dupont");
        org.mockito.Mockito.when(mockRecord.get("prenom")).thenReturn("Jean");
        org.mockito.Mockito.when(mockRecord.get("email")).thenReturn("jean@test.com");
        org.mockito.Mockito.when(mockRecord.get("telephone")).thenReturn("+22177000000");
        org.mockito.Mockito.when(mockRecord.get("date_naissance")).thenReturn(null);
        org.mockito.Mockito.when(mockRecord.get("date_adhesion")).thenReturn("");

        Object result = m.invoke(service, mockRecord, null, "ACTIF");

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("lireLigneCSV: dateAdhesionStr==null → L440 A=false → skip if body")
    void lireLigneCSV_dateAdhesionNull_l440AFalseBranch() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord mockRecord = org.mockito.Mockito.mock(CSVRecord.class);
        org.mockito.Mockito.when(mockRecord.get("nom")).thenReturn("Martin");
        org.mockito.Mockito.when(mockRecord.get("prenom")).thenReturn("Alice");
        org.mockito.Mockito.when(mockRecord.get("email")).thenReturn("alice@test.com");
        org.mockito.Mockito.when(mockRecord.get("telephone")).thenReturn("+22188000000");
        org.mockito.Mockito.when(mockRecord.get("date_naissance")).thenReturn("");
        org.mockito.Mockito.when(mockRecord.get("date_adhesion")).thenReturn(null);

        Object result = m.invoke(service, mockRecord, null, "ACTIF");

        assertThat(result).isNotNull();
    }

    @Test
    @DisplayName("lireLigneCSV: org non trouvée → L449 false (isPresent=false) → retourne membre sans org")
    void lireLigneCSV_orgAbsente_l449FalseBranch() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "lireLigneCSV", CSVRecord.class, UUID.class, String.class);
        m.setAccessible(true);

        CSVRecord record = createCsvRecord("Diallo", "Fatou", "fatou@test.com", "+22199000000", "", "");

        UUID orgId = UUID.randomUUID();
        when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.empty());

        Object result = m.invoke(service, record, orgId, "ACTIF");

        assertThat(result).isNotNull();
    }

    // =========================================================================
    // getCellValueAsDate — L522 false : cellule NUMERIC sans format date
    // → ni NUMERIC-date-formatée ni STRING → return null
    // =========================================================================

    @Test
    @DisplayName("getCellValueAsDate: cellule NUMERIC sans format date → L522 false → return null")
    void getCellValueAsDate_numericSansDateFormat_l522FalseRetourneNull() throws Exception {
        Method m = MembreImportExportService.class.getDeclaredMethod(
                "getCellValueAsDate", Row.class, Integer.class);
        m.setAccessible(true);

        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.createRow(30);
        Cell cell = row.createCell(0);
        cell.setCellValue(42.5); // NUMERIC, aucun style date → DateUtil.isCellDateFormatted=false

        Object result = m.invoke(service, row, 0);

        assertThat(result).isNull();
    }

    private byte[] createExcelWithExistingProtection() throws Exception {
        try (XSSFWorkbook wb = new XSSFWorkbook();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            wb.createSheet("Membres");

            // Ajouter la protection WorkbookProtection AVANT de sauvegarder
            org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbookProtection protection =
                    wb.getCTWorkbook().addNewWorkbookProtection();
            protection.setLockStructure(true);

            wb.write(out);
            return out.toByteArray();
        }
    }
}
