package dev.lions.unionflow.server.service;

import io.quarkus.mailer.MailTemplate;
import io.quarkus.mailer.Mailer;
import io.smallrye.mutiny.Uni;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.time.LocalDate;

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

/**
 * Tests unitaires pour {@link EmailTemplateService}.
 *
 * <p>Stratégie : les méthodes de Templates sont {@code static native} (Qute @CheckedTemplate).
 * Pour tester sans démarrer le moteur Qute, on injecte le {@code Mailer} via réflexion,
 * puis on stubbed les méthodes d'envoi avec {@link MockedStatic} sur la classe interne Templates.
 *
 * <p>Deux chemins couverts pour chaque méthode publique :
 * <ol>
 *   <li>Chemin nominal (happy path) — le mailer envoie sans erreur.</li>
 *   <li>Chemin d'erreur (catch) — le mailer lève une exception → pas de propagation.</li>
 * </ol>
 */
@DisplayName("Tests EmailTemplateService")
class EmailTemplateServiceTest {

    @InjectMocks
    EmailTemplateService service;

    @Mock
    Mailer mailer;

    @BeforeEach
    void setUp() throws Exception {
        MockitoAnnotations.openMocks(this);
        // Injecter le mailer mocké via réflexion (champ @Inject non accessible par @InjectMocks seul)
        injectField(service, "mailer", mailer);
    }

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

    /**
     * Helper : crée un MailTemplate.MailTemplateInstance mocké capable de retourner
     * le bon type pour chaque méthode chainée.
     */
    @SuppressWarnings("unchecked")
    private MailTemplate.MailTemplateInstance buildChainedMailMock() {
        MailTemplate.MailTemplateInstance instance = mock(MailTemplate.MailTemplateInstance.class);
        when(instance.to(anyString())).thenReturn(instance);
        when(instance.subject(anyString())).thenReturn(instance);
        Uni<Void> uniMock = mock(Uni.class);
        when(instance.send()).thenReturn(uniMock);
        io.smallrye.mutiny.groups.UniAwait<Void> awaitMock =
                mock(io.smallrye.mutiny.groups.UniAwait.class);
        when(uniMock.await()).thenReturn(awaitMock);
        // indefinitely() retourne Void — null est correct pour Void
        when(awaitMock.indefinitely()).thenReturn(null);
        return instance;
    }

    /**
     * Helper : crée un MailTemplate.MailTemplateInstance mocké qui lève une exception à l'envoi.
     */
    @SuppressWarnings("unchecked")
    private MailTemplate.MailTemplateInstance buildFailingMailMock(RuntimeException ex) {
        MailTemplate.MailTemplateInstance instance = mock(MailTemplate.MailTemplateInstance.class);
        when(instance.to(anyString())).thenReturn(instance);
        when(instance.subject(anyString())).thenReturn(instance);
        Uni<Void> uniMock = mock(Uni.class);
        when(instance.send()).thenReturn(uniMock);
        var awaitMock = mock(io.smallrye.mutiny.groups.UniAwait.class);
        when(uniMock.await()).thenReturn(awaitMock);
        when(awaitMock.indefinitely()).thenThrow(ex);
        return instance;
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  envoyerBienvenue
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("envoyerBienvenue")
    class EnvoyerBienvenue {

        @Test
        @DisplayName("Happy path — envoi réussi, pas d'exception")
        void happyPath_envoiReussi() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.bienvenue(
                        anyString(), anyString(), anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerBienvenue(
                        "marie@test.com", "Marie", "Dupont",
                        "Association Test", "https://app.unionflow.dev/login"))
                        .doesNotThrowAnyException();

                verify(mailMock).to("marie@test.com");
                verify(mailMock).subject(argThat(s -> s.contains("Association Test")));
            }
        }

        @Test
        @DisplayName("Exception lors de l'envoi — attrapée, pas de propagation")
        void exceptionEnvoi_attrapeeSansPropagation() {
            MailTemplate.MailTemplateInstance mailMock =
                    buildFailingMailMock(new RuntimeException("SMTP indisponible"));

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.bienvenue(
                        anyString(), anyString(), anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerBienvenue(
                        "fail@test.com", "Fail", "Test", "Org", "http://link"))
                        .doesNotThrowAnyException();
            }
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  envoyerConfirmationCotisation
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("envoyerConfirmationCotisation")
    class EnvoyerConfirmationCotisation {

        @Test
        @DisplayName("Happy path avec datePaiement non null — envoi réussi")
        void happyPath_datePaiementNonNull() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.cotisationConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationCotisation(
                        "marie@test.com", "Marie", "Dupont",
                        "Org Test", "04/2026",
                        "COT-2026-00000001", "WAVE",
                        LocalDate.of(2026, 4, 15), new BigDecimal("5000")))
                        .doesNotThrowAnyException();

                verify(mailMock).to("marie@test.com");
            }
        }

        @Test
        @DisplayName("datePaiement null — utilise '—' comme valeur de date")
        void datePaiementNull_utiliseTiret() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.cotisationConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), eq("—"), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationCotisation(
                        "test@test.com", "Test", "User",
                        "Org", "01/2026", "REF", "ESPECES",
                        null, new BigDecimal("2000")))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("Exception lors de l'envoi — attrapée, pas de propagation")
        void exceptionEnvoi_attrapeeSansPropagation() {
            MailTemplate.MailTemplateInstance mailMock =
                    buildFailingMailMock(new RuntimeException("Timeout"));

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.cotisationConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationCotisation(
                        "err@test.com", "Err", "User", "Org", "02/2026",
                        "REF-002", "VIREMENT", LocalDate.now(), BigDecimal.TEN))
                        .doesNotThrowAnyException();
            }
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  envoyerRappelCotisation
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("envoyerRappelCotisation")
    class EnvoyerRappelCotisation {

        @Test
        @DisplayName("Happy path avec dateLimite non null — envoi réussi")
        void happyPath_dateLimiteNonNull() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.rappelCotisation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerRappelCotisation(
                        "rappel@test.com", "Paul", "Martin",
                        "Club Lions", "03/2026",
                        new BigDecimal("1500"), LocalDate.of(2026, 3, 28),
                        "https://pay.unionflow.dev"))
                        .doesNotThrowAnyException();

                verify(mailMock).to("rappel@test.com");
            }
        }

        @Test
        @DisplayName("dateLimite null — utilise '—' comme valeur de date")
        void dateLimiteNull_utiliseTiret() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.rappelCotisation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), eq("—"), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerRappelCotisation(
                        "test@test.com", "Test", "User", "Org", "05/2026",
                        new BigDecimal("3000"), null, "https://link"))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("Exception lors de l'envoi — attrapée, pas de propagation")
        void exceptionEnvoi_attrapeeSansPropagation() {
            MailTemplate.MailTemplateInstance mailMock =
                    buildFailingMailMock(new RuntimeException("Connection refused"));

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.rappelCotisation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerRappelCotisation(
                        "err@test.com", "Err", "User", "Org", "06/2026",
                        BigDecimal.valueOf(500), LocalDate.now(), "http://link"))
                        .doesNotThrowAnyException();
            }
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  envoyerConfirmationSouscription
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("envoyerConfirmationSouscription")
    class EnvoyerConfirmationSouscription {

        @Test
        @DisplayName("Happy path — toutes dates et valeurs présentes — envoi réussi")
        void happyPath_toutesValeursPresentes() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "admin@test.com", "Admin User",
                        "Association Test", "Plan Pro",
                        new BigDecimal("50000"), "MENSUEL",
                        LocalDate.of(2026, 4, 1), LocalDate.of(2027, 4, 1),
                        100, 2048, true, true))
                        .doesNotThrowAnyException();

                verify(mailMock).to("admin@test.com");
            }
        }

        @Test
        @DisplayName("dateActivation null — utilise '—'")
        void dateActivationNull_utiliseTiret() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), eq("—"), anyString(),
                        anyString(), anyString(), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "test@test.com", "Admin",
                        "Org", "Plan Basique",
                        new BigDecimal("10000"), "ANNUEL",
                        null, LocalDate.of(2027, 1, 1),
                        50, 512, false, false))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("dateExpiration null — utilise '—'")
        void dateExpirationNull_utiliseTiret() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), eq("—"),
                        anyString(), anyString(), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "test@test.com", "Admin",
                        "Org", "Plan",
                        new BigDecimal("5000"), "MENSUEL",
                        LocalDate.of(2026, 4, 1), null,
                        null, null, false, true))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("maxMembres null — utilise 'Illimité'")
        void maxMembresNull_utiliseIllimite() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                // Capture l'appel pour vérifier "Illimité"
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(),
                        eq("Illimité"), anyString(), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "test@test.com", "Admin",
                        "Org", "Plan Illimité",
                        new BigDecimal("20000"), "MENSUEL",
                        LocalDate.now(), LocalDate.now().plusYears(1),
                        null, 1024, true, false))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("maxStockageMo null — utilise '1024' par défaut")
        void maxStockageMoNull_utilise1024() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(),
                        anyString(), eq("1024"), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "test@test.com", "Admin",
                        "Org", "Plan",
                        new BigDecimal("5000"), "MENSUEL",
                        LocalDate.now(), LocalDate.now().plusYears(1),
                        50, null, false, false))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("Exception lors de l'envoi — attrapée, pas de propagation")
        void exceptionEnvoi_attrapeeSansPropagation() {
            MailTemplate.MailTemplateInstance mailMock =
                    buildFailingMailMock(new RuntimeException("SSL error"));

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "err@test.com", "Admin",
                        "Org", "Plan",
                        new BigDecimal("100"), "MENSUEL",
                        null, null,
                        null, null, false, false))
                        .doesNotThrowAnyException();
            }
        }

        @Test
        @DisplayName("maxMembres présent et maxStockageMo présent — valeurs numériques utilisées")
        void maxMembresEtStockagePresents_valeursNumeriques() {
            MailTemplate.MailTemplateInstance mailMock = buildChainedMailMock();

            try (MockedStatic<EmailTemplateService.Templates> staticMock =
                         Mockito.mockStatic(EmailTemplateService.Templates.class)) {
                staticMock.when(() -> EmailTemplateService.Templates.souscriptionConfirmation(
                        anyString(), anyString(), anyString(), anyString(),
                        anyString(), anyString(), anyString(),
                        eq("200"), eq("4096"), anyBoolean(), anyBoolean()))
                        .thenReturn(mailMock);

                assertThatCode(() -> service.envoyerConfirmationSouscription(
                        "test@test.com", "Admin",
                        "Org", "Plan Enterprise",
                        new BigDecimal("100000"), "ANNUEL",
                        LocalDate.of(2026, 1, 1), LocalDate.of(2027, 1, 1),
                        200, 4096, true, true))
                        .doesNotThrowAnyException();
            }
        }
    }
}
