package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.config.request.UpdateConfigurationRequest;
import dev.lions.unionflow.server.api.dto.config.response.ConfigurationResponse;
import dev.lions.unionflow.server.repository.ConfigurationRepository;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.TestTransaction;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@QuarkusTest
class ConfigurationServiceTest {

    @Inject
    ConfigurationService configurationService;
    @Inject
    ConfigurationRepository configurationRepository;

    @Test
    @TestTransaction
    @DisplayName("listerConfigurations retourne une liste")
    void listerConfigurations_returnsList() {
        List<ConfigurationResponse> list = configurationService.listerConfigurations();
        assertThat(list).isNotNull();
    }

    @Test
    @TestTransaction
    @DisplayName("obtenirConfiguration avec clé inexistante lance NotFoundException")
    void obtenirConfiguration_cleInexistante_throwsNotFound() {
        assertThatThrownBy(() -> configurationService.obtenirConfiguration("CLE_INEXISTANTE_" + System.currentTimeMillis()))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @TestTransaction
    @DisplayName("obtenirConfiguration avec config inactive (actif=false) lance NotFoundException (L45 !actif = true)")
    void obtenirConfiguration_configInactive_throwsNotFound() {
        // Créer une config inactive via mettreAJourConfiguration puis désactiver
        String cle = "TEST_INACTIF_" + System.currentTimeMillis();
        dev.lions.unionflow.server.api.dto.config.request.UpdateConfigurationRequest createReq =
                dev.lions.unionflow.server.api.dto.config.request.UpdateConfigurationRequest.builder()
                        .cle(cle)
                        .valeur("valeur-inactif")
                        .type("STRING")
                        .categorie("TEST")
                        .description("Config inactive test")
                        .modifiable(true)
                        .visible(true)
                        .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Désactiver la config en accès direct au repository
        dev.lions.unionflow.server.entity.Configuration config =
                configurationRepository.findByCle(cle).get();
        config.setActif(false);
        // La config est dans la transaction courante → actif = false

        // obtenirConfiguration doit lever NotFoundException car !actif
        assertThatThrownBy(() -> configurationService.obtenirConfiguration(cle))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining(cle);
    }

    @Test
    @TestTransaction
    @DisplayName("mettreAJourConfiguration crée une nouvelle config et on peut la récupérer")
    void mettreAJourConfiguration_createsThenObtenir() {
        String cle = "TEST_SERVICE_" + System.currentTimeMillis();
        UpdateConfigurationRequest request = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-test")
                .type("STRING")
                .categorie("TEST")
                .description("Config test service")
                .modifiable(true)
                .visible(true)
                .build();

        ConfigurationResponse created = configurationService.mettreAJourConfiguration(cle, request);
        assertThat(created).isNotNull();
        assertThat(created.getCle()).isEqualTo(cle);
        assertThat(created.getValeur()).isEqualTo("valeur-test");

        ConfigurationResponse obtained = configurationService.obtenirConfiguration(cle);
        assertThat(obtained.getCle()).isEqualTo(cle);
        assertThat(obtained.getValeur()).isEqualTo("valeur-test");
    }

    // -------------------------------------------------------------------------
    // mettreAJourConfiguration — branche existant modifiable (lignes 59-74)
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("mettreAJourConfiguration met à jour une config existante modifiable")
    void mettreAJourConfiguration_existingModifiable_updates() {
        String cle = "TEST_UPDATE_" + System.currentTimeMillis();
        // Créer d'abord
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-initiale")
                .type("STRING")
                .categorie("TEST")
                .description("Config initiale")
                .modifiable(true)
                .visible(true)
                .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Mettre à jour
        UpdateConfigurationRequest updateReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-mise-a-jour")
                .type("INTEGER")
                .categorie("TEST")
                .description("Config mise à jour")
                .modifiable(true)
                .visible(true)
                .build();
        ConfigurationResponse updated = configurationService.mettreAJourConfiguration(cle, updateReq);

        assertThat(updated).isNotNull();
        assertThat(updated.getCle()).isEqualTo(cle);
        assertThat(updated.getValeur()).isEqualTo("valeur-mise-a-jour");
        assertThat(updated.getType()).isEqualTo("INTEGER");
    }

    // -------------------------------------------------------------------------
    // mettreAJourConfiguration — branche existant non modifiable (ligne 61)
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("mettreAJourConfiguration lance IllegalArgumentException si la config n'est pas modifiable")
    void mettreAJourConfiguration_nonModifiable_throwsIllegalArgument() {
        String cle = "TEST_NON_MOD_" + System.currentTimeMillis();
        // Créer une config non modifiable
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-fixe")
                .type("STRING")
                .categorie("SYSTEME")
                .description("Config non modifiable")
                .modifiable(false)
                .visible(true)
                .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Tenter de la modifier → doit lever IllegalArgumentException
        UpdateConfigurationRequest updateReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("nouvelle-valeur")
                .type("STRING")
                .categorie("SYSTEME")
                .description("Tentative de modification")
                .modifiable(false)
                .visible(true)
                .build();

        assertThatThrownBy(() -> configurationService.mettreAJourConfiguration(cle, updateReq))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining(cle);
    }

    // -------------------------------------------------------------------------
    // mettreAJourConfiguration — avec métadonnées (lignes 67-73 et 124-130)
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("mettreAJourConfiguration crée une config avec métadonnées")
    void mettreAJourConfiguration_withMetadonnees_createsAndSerializes() {
        String cle = "TEST_META_" + System.currentTimeMillis();
        Map<String, Object> metadonnees = Map.of("key1", "value1", "maxLength", 255);

        UpdateConfigurationRequest request = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-meta")
                .type("STRING")
                .categorie("TEST")
                .description("Config avec métadonnées")
                .modifiable(true)
                .visible(true)
                .metadonnees(metadonnees)
                .build();

        ConfigurationResponse created = configurationService.mettreAJourConfiguration(cle, request);
        assertThat(created).isNotNull();
        assertThat(created.getCle()).isEqualTo(cle);
        // Les métadonnées doivent avoir été sérialisées et désérialisées
        assertThat(created.getMetadonnees()).isNotNull();
        assertThat(created.getMetadonnees()).containsKey("key1");
    }

    // -------------------------------------------------------------------------
    // mettreAJourConfiguration — mise à jour d'une config existante avec métadonnées (lignes 67-74)
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("mettreAJourConfiguration met à jour une config existante avec métadonnées")
    void mettreAJourConfiguration_existingWithMetadonnees_updatesAndSerializes() {
        String cle = "TEST_META_UPD_" + System.currentTimeMillis();
        // Créer d'abord sans métadonnées
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur-init")
                .type("STRING")
                .categorie("TEST")
                .description("Initiale")
                .modifiable(true)
                .visible(true)
                .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Mettre à jour avec métadonnées
        Map<String, Object> metadonnees = Map.of("unit", "percent", "min", 0, "max", 100);
        UpdateConfigurationRequest updateReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("50")
                .type("INTEGER")
                .categorie("TEST")
                .description("Mise à jour avec métadonnées")
                .modifiable(true)
                .visible(true)
                .metadonnees(metadonnees)
                .build();
        ConfigurationResponse updated = configurationService.mettreAJourConfiguration(cle, updateReq);

        assertThat(updated).isNotNull();
        assertThat(updated.getMetadonnees()).isNotNull();
        assertThat(updated.getMetadonnees()).containsKey("unit");
    }

    // -------------------------------------------------------------------------
    // toDTO — retourne null si configuration null (ligne 89)
    // Testé indirectement via obtenirConfiguration sur une config inactive
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("obtenirConfiguration lance NotFoundException si la config est inactive")
    void obtenirConfiguration_inactiveConfig_throwsNotFound() {
        String cle = "TEST_INACTIVE_" + System.currentTimeMillis();
        // Créer une config active
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("valeur")
                .type("STRING")
                .categorie("TEST")
                .description("Active puis désactivée")
                .modifiable(true)
                .visible(true)
                .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Désactiver directement via repository
        configurationRepository.findByCle(cle).ifPresent(c -> {
            c.setActif(false);
            configurationRepository.persist(c);
        });

        // La config inactive ne doit pas être trouvée (findByCle filtre sur actif=true)
        assertThatThrownBy(() -> configurationService.obtenirConfiguration(cle))
                .isInstanceOf(NotFoundException.class);
    }

    // -------------------------------------------------------------------------
    // toDTO — branche métadonnées vide (isEmpty() == true) → métadonnees reste null
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("toDTO avec métadonnées string vide ne désérialise pas les métadonnées")
    void toDTO_emptyMetadonneesString_returnsNullMetadonnees() throws Exception {
        String cle = "TEST_EMPTY_META_" + System.currentTimeMillis();
        // Créer une config via service (sans métadonnées)
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle)
                .valeur("val")
                .type("STRING")
                .categorie("TEST")
                .description("No meta")
                .modifiable(true)
                .visible(true)
                .build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Forcer metadonnees = "" (empty string) via repository
        configurationRepository.findByCle(cle).ifPresent(c -> {
            c.setMetadonnees("");
            configurationRepository.persist(c);
        });

        // obtenirConfiguration appelle toDTO → metadonnees == "" → isEmpty() true → skip deserialisation
        ConfigurationResponse response = configurationService.obtenirConfiguration(cle);
        assertThat(response).isNotNull();
        assertThat(response.getMetadonnees()).isNull();
    }

    // -------------------------------------------------------------------------
    // toDTO null et toEntity null — via réflexion sur les méthodes privées
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("toDTO avec configuration null retourne null (via réflexion)")
    void toDTO_null_returnsNull() throws Exception {
        java.lang.reflect.Method toDTO = ConfigurationService.class.getDeclaredMethod(
                "toDTO", dev.lions.unionflow.server.entity.Configuration.class);
        toDTO.setAccessible(true);
        Object result = toDTO.invoke(configurationService, (Object) null);
        assertThat(result).isNull();
    }

    @Test
    @DisplayName("toEntity avec dto null retourne null (via réflexion)")
    void toEntity_null_returnsNull() throws Exception {
        java.lang.reflect.Method toEntity = ConfigurationService.class.getDeclaredMethod(
                "toEntity", dev.lions.unionflow.server.api.dto.config.request.UpdateConfigurationRequest.class);
        toEntity.setAccessible(true);
        Object result = toEntity.invoke(configurationService, (Object) null);
        assertThat(result).isNull();
    }

    // -------------------------------------------------------------------------
    // obtenirConfiguration — config trouvée mais actif=false (branche !actif)
    // -------------------------------------------------------------------------

    @Test
    @TestTransaction
    @DisplayName("obtenirConfiguration avec config trouvée mais actif=false lance NotFoundException")
    void obtenirConfiguration_configFoundButInactive_throwsNotFound() {
        String cle = "TEST_FOUND_INACTIVE_" + System.currentTimeMillis();
        // Créer active
        UpdateConfigurationRequest req = UpdateConfigurationRequest.builder()
                .cle(cle).valeur("v").type("STRING").categorie("T")
                .description("d").modifiable(true).visible(true).build();
        configurationService.mettreAJourConfiguration(cle, req);

        // Marquer comme inactif
        configurationRepository.findByCle(cle).ifPresent(c -> {
            c.setActif(false);
            configurationRepository.persist(c);
        });

        assertThatThrownBy(() -> configurationService.obtenirConfiguration(cle))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining(cle);
    }

    // -------------------------------------------------------------------------
    // mettreAJourConfiguration — catch block: ObjectMapper.writeValueAsString lance Exception
    // toDTO — catch block: ObjectMapper.readValue lance Exception
    // toEntity — catch block: ObjectMapper.writeValueAsString lance Exception (création)
    // Ces blocs catch sont couverts via réflexion + remplacement du champ objectMapper
    // par un mock qui lance une exception.
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("mettreAJourConfiguration — catch block ObjectMapper.writeValueAsString (update existant) couvert via mock ObjectMapper")
    void mettreAJourConfiguration_existingMetadonneesSerializationError_catchCovered() throws Exception {
        String cle = "TEST_CATCH_UPD_" + System.currentTimeMillis();
        // Créer d'abord normalement
        UpdateConfigurationRequest createReq = UpdateConfigurationRequest.builder()
                .cle(cle).valeur("v1").type("STRING").categorie("T")
                .description("d1").modifiable(true).visible(true).build();
        configurationService.mettreAJourConfiguration(cle, createReq);

        // Remplacer objectMapper par un mock qui lance sur writeValueAsString
        java.lang.reflect.Field omField = ConfigurationService.class.getDeclaredField("objectMapper");
        omField.setAccessible(true);
        com.fasterxml.jackson.databind.ObjectMapper badMapper =
                new com.fasterxml.jackson.databind.ObjectMapper() {
                    @Override
                    public String writeValueAsString(Object value)
                            throws com.fasterxml.jackson.core.JsonProcessingException {
                        throw new com.fasterxml.jackson.core.JsonProcessingException("forced error") {};
                    }
                };

        // Récupérer l'instance réelle (derrière le proxy CDI)
        ConfigurationService actualService = configurationService;
        try {
            // Quarkus CDI proxy → accéder à l'instance réelle
            actualService = (ConfigurationService)
                    ((io.quarkus.arc.ClientProxy) configurationService).arc_contextualInstance();
        } catch (Exception e) {
            // pas un proxy — utiliser directement
        }

        com.fasterxml.jackson.databind.ObjectMapper originalMapper =
                (com.fasterxml.jackson.databind.ObjectMapper) omField.get(actualService);
        omField.set(actualService, badMapper);

        try {
            // Mettre à jour avec metadonnees → writeValueAsString lancera → catch block couvert
            UpdateConfigurationRequest updateReq = UpdateConfigurationRequest.builder()
                    .cle(cle).valeur("v2").type("STRING").categorie("T")
                    .description("d2").modifiable(true).visible(true)
                    .metadonnees(java.util.Map.of("k", "v"))
                    .build();
            // Ne doit PAS lancer (catch absorbe l'erreur)
            ConfigurationResponse result = configurationService.mettreAJourConfiguration(cle, updateReq);
            assertThat(result).isNotNull();
        } finally {
            omField.set(actualService, originalMapper);
        }
    }

    @Test
    @DisplayName("toDTO — catch block ObjectMapper.readValue couvert via metadonnees JSON invalide dans entité")
    void toDTO_invalidMetadonneesJson_catchCovered() throws Exception {
        String cle = "TEST_CATCH_DTO_" + System.currentTimeMillis();
        // Créer une config sans métadonnées
        UpdateConfigurationRequest req = UpdateConfigurationRequest.builder()
                .cle(cle).valeur("v").type("STRING").categorie("T")
                .description("d").modifiable(true).visible(true).build();
        configurationService.mettreAJourConfiguration(cle, req);

        // Forcer metadonnees = JSON invalide dans la BD
        configurationRepository.findByCle(cle).ifPresent(c -> {
            c.setMetadonnees("{invalid json}");
            configurationRepository.persist(c);
        });

        // obtenirConfiguration → toDTO → readValue lance → catch absorbe → metadonnees reste null
        ConfigurationResponse response = configurationService.obtenirConfiguration(cle);
        assertThat(response).isNotNull();
        // metadonnees est null car désérialisation a échoué (catch absorbe)
        assertThat(response.getMetadonnees()).isNull();
    }

    @Test
    @DisplayName("mettreAJourConfiguration — catch block ObjectMapper.writeValueAsString (création) couvert via mock ObjectMapper")
    void mettreAJourConfiguration_newConfigMetadonneesSerializationError_catchCovered() throws Exception {
        String cle = "TEST_CATCH_NEW_" + System.currentTimeMillis();

        // Récupérer l'instance réelle derrière le proxy CDI
        ConfigurationService actualService = configurationService;
        try {
            actualService = (ConfigurationService)
                    ((io.quarkus.arc.ClientProxy) configurationService).arc_contextualInstance();
        } catch (Exception e) {
            // pas un proxy
        }

        java.lang.reflect.Field omField = ConfigurationService.class.getDeclaredField("objectMapper");
        omField.setAccessible(true);
        com.fasterxml.jackson.databind.ObjectMapper badMapper =
                new com.fasterxml.jackson.databind.ObjectMapper() {
                    @Override
                    public String writeValueAsString(Object value)
                            throws com.fasterxml.jackson.core.JsonProcessingException {
                        throw new com.fasterxml.jackson.core.JsonProcessingException("forced") {};
                    }
                };

        com.fasterxml.jackson.databind.ObjectMapper originalMapper =
                (com.fasterxml.jackson.databind.ObjectMapper) omField.get(actualService);
        omField.set(actualService, badMapper);

        try {
            // Créer une nouvelle config avec metadonnees → toEntity → writeValueAsString lance → catch couvert
            UpdateConfigurationRequest req = UpdateConfigurationRequest.builder()
                    .cle(cle).valeur("v").type("STRING").categorie("T")
                    .description("d").modifiable(true).visible(true)
                    .metadonnees(java.util.Map.of("x", 1))
                    .build();
            ConfigurationResponse result = configurationService.mettreAJourConfiguration(cle, req);
            assertThat(result).isNotNull();
        } finally {
            omField.set(actualService, originalMapper);
        }
    }
}
