package org.github.tess1o.geopulse.admin.service;

import jakarta.enterprise.event.Event;
import org.github.tess1o.geopulse.admin.model.SettingInfo;
import org.github.tess1o.geopulse.admin.repository.SystemSettingsRepository;
import org.github.tess1o.geopulse.ai.service.AIEncryptionService;
import org.github.tess1o.geopulse.user.model.DistanceUnit;
import org.github.tess1o.geopulse.user.model.TemperatureUnit;
import org.github.tess1o.geopulse.weather.event.WeatherSettingsChangedEvent;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@Tag("unit")
class SystemSettingsServiceGeocodingEnvFallbackTest {

    private static final String GEOPULSE_GOOGLE_MAPS_ENABLED = "GEOPULSE_GEOCODING_GOOGLE_MAPS_ENABLED";
    private static final String GEOPULSE_GOOGLE_MAPS_API_KEY = "GEOPULSE_GEOCODING_GOOGLE_MAPS_API_KEY";
    private static final String GEOPULSE_GOOGLE_MAPS_LANGUAGE = "GEOPULSE_GEOCODING_GOOGLE_MAPS_LANGUAGE";
    private static final String GEOPULSE_GEOAPIFY_API_KEY = "GEOPULSE_GEOCODING_GEOAPIFY_API_KEY";
    private static final String GEOPULSE_CHIBIGEO_API_KEY = "GEOPULSE_GEOCODING_CHIBIGEO_API_KEY";
    private static final String PROVIDER_GOOGLE_MAPS_ENABLED = "geocoding.provider.googlemaps.enabled";
    private static final String PROVIDER_GOOGLE_MAPS_API_KEY = "geocoding.provider.googlemaps.api-key";
    private static final String PROVIDER_GOOGLE_MAPS_LANGUAGE = "geocoding.provider.googlemaps.language";
    private static final String PROVIDER_GEOAPIFY_API_KEY = "geocoding.provider.geoapify.api-key";
    private static final String PROVIDER_CHIBIGEO_API_KEY = "geocoding.provider.chibigeo.api-key";
    private static final String GEOPULSE_USER_DEFAULT_DISTANCE_UNIT = "GEOPULSE_USER_DEFAULT_DISTANCE_UNIT";
    private static final String USER_DEFAULT_DISTANCE_UNIT = "geopulse.user.default-distance-unit";
    private static final String GEOPULSE_USER_DEFAULT_TEMPERATURE_UNIT = "GEOPULSE_USER_DEFAULT_TEMPERATURE_UNIT";
    private static final String USER_DEFAULT_TEMPERATURE_UNIT = "geopulse.user.default-temperature-unit";
    private static final List<String> GEOPULSE_MAP_MATCHING_ENV_VARS = List.of(
            "GEOPULSE_TIMELINE_MAP_MATCHING_ENABLED",
            "GEOPULSE_TIMELINE_MAP_MATCHING_AUTOMATIC_ENABLED",
            "GEOPULSE_TIMELINE_MAP_MATCHING_BACKFILL_ENABLED",
            "GEOPULSE_TIMELINE_MAP_MATCHING_AUTOMATIC_QUIET_PERIOD_MINUTES",
            "GEOPULSE_TIMELINE_MAP_MATCHING_PROVIDER",
            "GEOPULSE_TIMELINE_MAP_MATCHING_VALHALLA_BASE_URL",
            "GEOPULSE_TIMELINE_MAP_MATCHING_CONNECT_TIMEOUT_SECONDS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_READ_TIMEOUT_SECONDS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_MAX_INPUT_POINTS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_MAX_TRIP_DURATION_HOURS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_WORKER_BATCH_SIZE",
            "GEOPULSE_TIMELINE_MAP_MATCHING_MAX_ATTEMPTS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MIN_RAW_DISTANCE_METERS",
            "GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MIN_DISTANCE_COVERAGE_PERCENT",
            "GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MAX_DISCONTINUITY_PERCENT",
            "GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MAX_SHORT_DISCONTINUITY_METERS"
    );

    @AfterEach
    void cleanUpSystemProperties() {
        System.clearProperty(GEOPULSE_GOOGLE_MAPS_ENABLED);
        System.clearProperty(GEOPULSE_GOOGLE_MAPS_API_KEY);
        System.clearProperty(GEOPULSE_GOOGLE_MAPS_LANGUAGE);
        System.clearProperty(GEOPULSE_GEOAPIFY_API_KEY);
        System.clearProperty(GEOPULSE_CHIBIGEO_API_KEY);
        System.clearProperty(PROVIDER_GOOGLE_MAPS_ENABLED);
        System.clearProperty(PROVIDER_GOOGLE_MAPS_API_KEY);
        System.clearProperty(PROVIDER_GOOGLE_MAPS_LANGUAGE);
        System.clearProperty(PROVIDER_GEOAPIFY_API_KEY);
        System.clearProperty(PROVIDER_CHIBIGEO_API_KEY);
        System.clearProperty(GEOPULSE_USER_DEFAULT_DISTANCE_UNIT);
        System.clearProperty(USER_DEFAULT_DISTANCE_UNIT);
        System.clearProperty(GEOPULSE_USER_DEFAULT_TEMPERATURE_UNIT);
        System.clearProperty(USER_DEFAULT_TEMPERATURE_UNIT);
        GEOPULSE_MAP_MATCHING_ENV_VARS.forEach(System::clearProperty);
    }

    @Test
    void shouldResolveGoogleMapsEnabledFromGeopulseEnvVariableStyle() {
        System.setProperty(GEOPULSE_GOOGLE_MAPS_ENABLED, "true");

        SystemSettingsService service = createService();

        assertTrue(service.getBoolean("geocoding.googlemaps.enabled"));
    }

    @Test
    void shouldResolveGoogleMapsEnabledFromProviderPropertyStyle() {
        System.setProperty(PROVIDER_GOOGLE_MAPS_ENABLED, "true");

        SystemSettingsService service = createService();

        assertTrue(service.getBoolean("geocoding.googlemaps.enabled"));
    }

    @Test
    void shouldResolveGoogleMapsApiKeyFromGeopulseEnvVariableStyle() {
        System.setProperty(GEOPULSE_GOOGLE_MAPS_API_KEY, "from-geopulse");

        SystemSettingsService service = createService();

        assertEquals("from-geopulse", service.getString("geocoding.googlemaps.api-key"));
    }

    @Test
    void shouldResolveGoogleMapsApiKeyFromProviderPropertyStyle() {
        System.setProperty(PROVIDER_GOOGLE_MAPS_API_KEY, "from-provider");

        SystemSettingsService service = createService();

        assertEquals("from-provider", service.getString("geocoding.googlemaps.api-key"));
    }

    @Test
    void shouldResolveGoogleMapsLanguageFromGeopulseEnvVariableStyle() {
        System.setProperty(GEOPULSE_GOOGLE_MAPS_LANGUAGE, "uk");

        SystemSettingsService service = createService();

        assertEquals("uk", service.getString("geocoding.googlemaps.language"));
    }

    @Test
    void shouldResolveGoogleMapsLanguageFromProviderPropertyStyle() {
        System.setProperty(PROVIDER_GOOGLE_MAPS_LANGUAGE, "pt-BR");

        SystemSettingsService service = createService();

        assertEquals("pt-BR", service.getString("geocoding.googlemaps.language"));
    }

    @Test
    void shouldTreatMissingGeoapifyApiKeyAsEmpty() {
        SystemSettingsService service = createService();

        assertEquals("", service.getString("geocoding.geoapify.api-key"));
    }

    @Test
    void shouldTreatMissingChibiGeoApiKeyAsEmpty() {
        SystemSettingsService service = createService();

        assertEquals("", service.getString("geocoding.chibigeo.api-key"));
    }

    @Test
    void shouldUseOngoingWeatherDefaultsWithoutHistoricalBackfill() {
        SystemSettingsService service = createService();

        assertTrue(service.getBoolean("weather.enabled"));
        assertTrue(service.getBoolean("weather.ongoing.enabled"));
        assertFalse(service.getBoolean("weather.backfill.enabled"));
    }

    @Test
    void shouldResolveMapMatchingSettingsFromGeopulseEnvVariableStyle() {
        Map<String, String> overrides = new LinkedHashMap<>();
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_ENABLED", "true");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_AUTOMATIC_ENABLED", "true");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_BACKFILL_ENABLED", "true");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_AUTOMATIC_QUIET_PERIOD_MINUTES", "20");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_PROVIDER", "valhalla");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_VALHALLA_BASE_URL", "http://valhalla:8002");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_CONNECT_TIMEOUT_SECONDS", "4");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_READ_TIMEOUT_SECONDS", "25");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_MAX_INPUT_POINTS", "120");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_MAX_TRIP_DURATION_HOURS", "36");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_WORKER_BATCH_SIZE", "8");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_MAX_ATTEMPTS", "5");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MIN_RAW_DISTANCE_METERS", "600");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MIN_DISTANCE_COVERAGE_PERCENT", "45");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MAX_DISCONTINUITY_PERCENT", "12");
        overrides.put("GEOPULSE_TIMELINE_MAP_MATCHING_QUALITY_MAX_SHORT_DISCONTINUITY_METERS", "150");
        overrides.forEach(System::setProperty);

        SystemSettingsService service = createService();

        assertTrue(service.getBoolean("map-matching.enabled"));
        assertTrue(service.getBoolean("map-matching.automatic.enabled"));
        assertTrue(service.getBoolean("map-matching.backfill.enabled"));
        assertEquals(20, service.getInteger("map-matching.automatic.quiet-period-minutes"));
        assertEquals("valhalla", service.getString("map-matching.provider"));
        assertEquals("http://valhalla:8002", service.getString("map-matching.valhalla.base-url"));
        assertEquals(4, service.getInteger("map-matching.valhalla.connect-timeout-seconds"));
        assertEquals(25, service.getInteger("map-matching.valhalla.read-timeout-seconds"));
        assertEquals(120, service.getInteger("map-matching.max-input-points"));
        assertEquals(36, service.getInteger("map-matching.max-trip-duration-hours"));
        assertEquals(8, service.getInteger("map-matching.worker.batch-size"));
        assertEquals(5, service.getInteger("map-matching.max-attempts"));
        assertEquals(600, service.getInteger("map-matching.quality.min-raw-distance-meters"));
        assertEquals(45, service.getInteger("map-matching.quality.min-distance-coverage-percent"));
        assertEquals(12, service.getInteger("map-matching.quality.max-discontinuity-percent"));
        assertEquals(150, service.getInteger("map-matching.quality.max-short-discontinuity-meters"));
    }

    @Test
    void shouldReturnEmptyValuesForMissingEncryptedGeoapifyAndChibiGeoCredentials() {
        SystemSettingsService service = createService();

        List<SettingInfo> geocodingSettings = service.getSettingsByCategory("geocoding");
        SettingInfo geoapifyApiKey = findSetting(geocodingSettings, "geocoding.geoapify.api-key");
        SettingInfo chibiGeoApiKey = findSetting(geocodingSettings, "geocoding.chibigeo.api-key");

        assertEquals("", geoapifyApiKey.value());
        assertEquals("", geoapifyApiKey.defaultValue());
        assertEquals("", chibiGeoApiKey.value());
        assertEquals("", chibiGeoApiKey.defaultValue());
    }

    @Test
    void shouldMaskRealGeoapifyAndChibiGeoEnvCredentials() {
        System.setProperty(GEOPULSE_GEOAPIFY_API_KEY, "geoapify-key");
        System.setProperty(GEOPULSE_CHIBIGEO_API_KEY, "chibigeo-key");

        SystemSettingsService service = createService();

        assertEquals("geoapify-key", service.getString("geocoding.geoapify.api-key"));
        assertEquals("chibigeo-key", service.getString("geocoding.chibigeo.api-key"));

        List<SettingInfo> geocodingSettings = service.getSettingsByCategory("geocoding");
        assertEquals("********", findSetting(geocodingSettings, "geocoding.geoapify.api-key").value());
        assertEquals("********", findSetting(geocodingSettings, "geocoding.chibigeo.api-key").value());
    }

    @Test
    void shouldUseDefaultUnits() {
        SystemSettingsService service = createService();

        assertEquals(DistanceUnit.KILOMETERS, service.getDefaultDistanceUnit());
        assertEquals(TemperatureUnit.CELSIUS, service.getDefaultTemperatureUnit());
        assertEquals("KILOMETERS", service.getString("system.user.default-distance-unit"));
        assertEquals("CELSIUS", service.getString("system.user.default-temperature-unit"));
    }

    @Test
    void shouldResolveDefaultUnitsFromGeopulseEnvVariableStyle() {
        System.setProperty(GEOPULSE_USER_DEFAULT_DISTANCE_UNIT, "MILES");
        System.setProperty(GEOPULSE_USER_DEFAULT_TEMPERATURE_UNIT, "FAHRENHEIT");

        SystemSettingsService service = createService();

        assertEquals(DistanceUnit.MILES, service.getDefaultDistanceUnit());
        assertEquals(TemperatureUnit.FAHRENHEIT, service.getDefaultTemperatureUnit());
    }

    @Test
    void shouldResolveDefaultUnitsFromPropertyStyle() {
        System.setProperty(USER_DEFAULT_DISTANCE_UNIT, "MILES");
        System.setProperty(USER_DEFAULT_TEMPERATURE_UNIT, "FAHRENHEIT");

        SystemSettingsService service = createService();

        assertEquals(DistanceUnit.MILES, service.getDefaultDistanceUnit());
        assertEquals(TemperatureUnit.FAHRENHEIT, service.getDefaultTemperatureUnit());
    }

    @Test
    void shouldFallbackToDefaultsWhenUnitEnvValuesAreInvalid() {
        System.setProperty(GEOPULSE_USER_DEFAULT_DISTANCE_UNIT, "yards");
        System.setProperty(GEOPULSE_USER_DEFAULT_TEMPERATURE_UNIT, "kelvin");

        SystemSettingsService service = createService();

        assertEquals(DistanceUnit.KILOMETERS, service.getDefaultDistanceUnit());
        assertEquals(TemperatureUnit.CELSIUS, service.getDefaultTemperatureUnit());
        assertEquals("KILOMETERS", service.getString("system.user.default-distance-unit"));
        assertEquals("CELSIUS", service.getString("system.user.default-temperature-unit"));
    }

    @Test
    void shouldRejectInvalidDefaultUnitAdminValues() {
        SystemSettingsService service = createService();

        assertThrows(IllegalArgumentException.class,
                () -> service.setValue("system.user.default-distance-unit", "yards", null));
        assertThrows(IllegalArgumentException.class,
                () -> service.setValue("system.user.default-temperature-unit", "kelvin", null));
    }

    @Test
    void shouldFireWeatherSettingsChangedEventWhenWeatherSettingIsUpdated() {
        SystemSettingsRepository repository = Mockito.mock(SystemSettingsRepository.class);
        AIEncryptionService encryptionService = Mockito.mock(AIEncryptionService.class);
        @SuppressWarnings("unchecked")
        Event<WeatherSettingsChangedEvent> weatherSettingsChangedEvent = Mockito.mock(Event.class);
        when(repository.findByKey(anyString())).thenReturn(Optional.empty());
        SystemSettingsService service = new SystemSettingsService(repository, encryptionService, weatherSettingsChangedEvent);

        service.setValue("weather.enabled", "true", null);

        verify(weatherSettingsChangedEvent).fire(new WeatherSettingsChangedEvent("weather.enabled"));
    }

    @Test
    void shouldNotFireWeatherSettingsChangedEventWhenNonWeatherSettingIsUpdated() {
        SystemSettingsRepository repository = Mockito.mock(SystemSettingsRepository.class);
        AIEncryptionService encryptionService = Mockito.mock(AIEncryptionService.class);
        @SuppressWarnings("unchecked")
        Event<WeatherSettingsChangedEvent> weatherSettingsChangedEvent = Mockito.mock(Event.class);
        when(repository.findByKey(anyString())).thenReturn(Optional.empty());
        SystemSettingsService service = new SystemSettingsService(repository, encryptionService, weatherSettingsChangedEvent);

        service.setValue("auth.registration.enabled", "true", null);

        verify(weatherSettingsChangedEvent, never()).fire(any());
    }

    private SystemSettingsService createService() {
        SystemSettingsRepository repository = Mockito.mock(SystemSettingsRepository.class);
        AIEncryptionService encryptionService = Mockito.mock(AIEncryptionService.class);
        @SuppressWarnings("unchecked")
        Event<WeatherSettingsChangedEvent> weatherSettingsChangedEvent = Mockito.mock(Event.class);
        when(repository.findByKey(anyString())).thenReturn(Optional.empty());
        return new SystemSettingsService(repository, encryptionService, weatherSettingsChangedEvent);
    }

    private SettingInfo findSetting(List<SettingInfo> settings, String key) {
        return settings.stream()
                .filter(setting -> setting.key().equals(key))
                .findFirst()
                .orElseThrow();
    }
}
