package org.github.tess1o.geopulse.service;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.github.tess1o.geopulse.admin.model.Role;
import org.github.tess1o.geopulse.db.PostgisTestResource;
import org.github.tess1o.geopulse.gps.integrations.owntracks.model.OwnTracksLocationMessage;
import org.github.tess1o.geopulse.gps.model.GpsPointEntity;
import org.github.tess1o.geopulse.gps.model.GpsPointFilterDTO;
import org.github.tess1o.geopulse.gps.model.GpsPointDTO;
import org.github.tess1o.geopulse.gps.model.GpsPointPathDTO;
import org.github.tess1o.geopulse.gps.model.GpsPointPathPointDTO;
import org.github.tess1o.geopulse.gps.model.GpsPointSummaryDTO;
import org.github.tess1o.geopulse.gps.model.RawGpsPointMapResponseDTO;
import org.github.tess1o.geopulse.gps.repository.GpsPointRepository;
import org.github.tess1o.geopulse.gps.service.GpsPointService;
import org.github.tess1o.geopulse.gpssource.model.GpsSourceConfigEntity;
import org.github.tess1o.geopulse.gpssource.model.GpsTelemetryMappingEntry;
import org.github.tess1o.geopulse.gpssource.repository.GpsSourceRepository;
import org.github.tess1o.geopulse.gpssource.repository.GpsSourceTypeTelemetryConfigRepository;
import org.github.tess1o.geopulse.gpssource.service.GpsSourceTypeTelemetryConfigService;
import org.github.tess1o.geopulse.shared.gps.GpsSourceType;
import org.github.tess1o.geopulse.testsupport.SerializedDatabaseTest;
import org.github.tess1o.geopulse.testsupport.TestIds;
import org.github.tess1o.geopulse.user.model.TimelinePreferences;
import org.github.tess1o.geopulse.user.model.UserEntity;
import org.github.tess1o.geopulse.user.repository.UserRepository;
import org.hibernate.Hibernate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
@QuarkusTestResource(value = PostgisTestResource.class)
@TestProfile(GpsPointServiceTest.DisableLocationTimeThresholdTestProfile.class)
@SerializedDatabaseTest
public class GpsPointServiceTest {

    private UUID userId;
    private GpsSourceConfigEntity testConfig;

    @Inject
    GpsPointService gpsPointService;

    @Inject
    GpsPointRepository gpsPointRepository;

    @Inject
    UserRepository userRepository;

    @Inject
    GpsSourceRepository gpsSourceRepository;

    @Inject
    GpsSourceTypeTelemetryConfigService telemetryConfigService;

    @Inject
    GpsSourceTypeTelemetryConfigRepository telemetryConfigRepository;

    @Inject
    EntityManager entityManager;
    @BeforeEach
    @Transactional
    public void setup() {
        // Clean up GPS points first (due to foreign key constraints)
        // Clean up GPS source configs
        // Clean up users
        // Create fresh test user
        UserEntity user = UserEntity.builder()
                .email(TestIds.uniqueEmail("gps-point-service"))
                .role(Role.USER)
                .passwordHash("pass")
                .build();
        userRepository.persist(user);
        userId = user.getId();
        // Create test GPS source config with filtering disabled for existing tests
        testConfig = GpsSourceConfigEntity.builder()
                .user(user)
                .sourceType(GpsSourceType.OWNTRACKS)
                .username(TestIds.uniqueValue("gps-point-source-user"))
                .active(true)
                .filterInaccurateData(false) // Filtering disabled for backward compatibility
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .build();
        gpsSourceRepository.persist(testConfig);
    }
    @Test
    @Transactional
    public void testSaveOwnTracksGpsPoint() {
        long tst = (int) Instant.now().plusSeconds(20000).toEpochMilli() / 1000;
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .acc(0.2)
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .vel(5.0)
                .build();
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
        GpsPointEntity savedGpsPoint = gpsPointRepository.find("user.id = ?1 ORDER BY createdAt DESC", userId).firstResult();
        assertEquals(userId, savedGpsPoint.getUser().getId());
        assertFalse(Hibernate.isInitialized(savedGpsPoint.getUser()));
        assertNull(savedGpsPoint.getAltitude());
        assertNull(savedGpsPoint.getBattery());
        assertEquals(0.2, savedGpsPoint.getAccuracy(), 0.000001);
        assertEquals(40.0, savedGpsPoint.getCoordinates().getY(), 0.000001);
        assertEquals(-74.0, savedGpsPoint.getCoordinates().getX(), 0.000001);
        assertEquals(5.0, savedGpsPoint.getVelocity(), 0.000001);
        assertEquals("test-device", savedGpsPoint.getDeviceId());
        assertEquals(tst, (int) savedGpsPoint.getTimestamp().getEpochSecond());
    }

    @Test
    @Transactional
    public void testSaveOwnTracksGpsPointDoesNotEnrichWaterEvidenceWhenBoatDisabled() {
        installTestWaterDataset();

        long tst = (int) Instant.now().plusSeconds(20000).toEpochMilli() / 1000;
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .acc(0.2)
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .vel(5.0)
                .build();

        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);

        assertEquals(0, countGpsPointEnvironmentRows(userId));
    }

    @Test
    @Transactional
    public void testSaveOwnTracksGpsPointEnrichesWaterEvidenceWhenBoatEnabledAndDatasetExists() {
        enableBoatForTestUser();
        installTestWaterDataset();

        long tst = (int) Instant.now().plusSeconds(20000).toEpochMilli() / 1000;
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .acc(0.2)
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .vel(5.0)
                .build();

        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);

        assertEquals(1, countGpsPointEnvironmentRows(userId));
        assertTrue(Boolean.TRUE.equals(findOnlyWaterEvidenceValue(userId)));
    }

    @Test
    @Transactional
    public void testSaveMobileAppGpsPointsEnrichesWaterEvidenceForSavedBatch() {
        enableBoatForTestUser();
        installTestWaterDataset();

        GpsSourceConfigEntity mobileAppConfig = GpsSourceConfigEntity.builder()
                .user(userRepository.findById(userId))
                .sourceType(GpsSourceType.MOBILE_APP)
                .username(TestIds.uniqueValue("gps-point-mobile-source-user"))
                .active(true)
                .filterInaccurateData(false)
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .enableDuplicateDetection(false)
                .build();

        GpsPointDTO waterPoint = new GpsPointDTO(
                0L,
                Instant.parse("2026-05-15T10:00:00Z"),
                new GpsPointDTO.CoordinatesDTO(40.0, -74.0),
                5.0,
                88.0,
                1.5,
                12.0,
                null
        );
        GpsPointDTO landPoint = new GpsPointDTO(
                0L,
                Instant.parse("2026-05-15T10:05:00Z"),
                new GpsPointDTO.CoordinatesDTO(10.0, 10.0),
                5.0,
                88.0,
                1.5,
                12.0,
                null
        );

        gpsPointService.saveMobileAppGpsPoints(List.of(waterPoint, landPoint), "pixel-9-pro",
                userId, GpsSourceType.MOBILE_APP, mobileAppConfig);

        assertEquals(2, countGpsPointEnvironmentRows(userId));
        assertEquals(1, countWaterEvidenceRows(userId, true));
        assertEquals(1, countWaterEvidenceRows(userId, false));
    }
    @Test
    @Transactional
    public void testSaveOwnTracksGpsPointDuplicate() {
        long tst = (int) Instant.now().plusSeconds(20000).toEpochMilli() / 1000;
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .acc(0.2)
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .vel(5.0)
                .build();
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
    }
    @Test
    @Transactional
    public void testSaveMobileAppGpsPointDuplicate_SkipsDuplicateWithoutThrowing() {
        Instant timestamp = Instant.parse("2026-05-15T10:00:00Z");
        GpsPointDTO request = new GpsPointDTO(
                0L,
                timestamp,
                new GpsPointDTO.CoordinatesDTO(40.0, -74.0),
                5.0,
                88.0,
                1.5,
                12.0,
                null
        );
        GpsSourceConfigEntity mobileAppConfig = GpsSourceConfigEntity.builder()
                .user(userRepository.findById(userId))
                .sourceType(GpsSourceType.MOBILE_APP)
                .username(TestIds.uniqueValue("gps-point-mobile-source-user"))
                .active(true)
                .filterInaccurateData(false)
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .enableDuplicateDetection(false)
                .build();

        gpsPointService.saveMobileAppGpsPoint(request, "pixel-9-pro", userId, GpsSourceType.MOBILE_APP, mobileAppConfig);

        assertDoesNotThrow(() -> gpsPointService.saveMobileAppGpsPoint(request, "pixel-9-pro", userId, GpsSourceType.MOBILE_APP, mobileAppConfig));

        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
    }

    @Test
    public void testSaveMobileAppGpsPointsDuplicate_SkipsDuplicateAndKeepsFirstPoint() {
        Instant timestamp = Instant.parse("2026-05-15T10:00:00Z");
        GpsPointDTO first = new GpsPointDTO(
                0L,
                timestamp,
                new GpsPointDTO.CoordinatesDTO(40.0, -74.0),
                5.0,
                88.0,
                1.5,
                12.0,
                null
        );
        GpsPointDTO duplicate = new GpsPointDTO(
                0L,
                timestamp,
                new GpsPointDTO.CoordinatesDTO(40.0, -74.0),
                4.0,
                87.0,
                1.0,
                13.0,
                null
        );
        GpsSourceConfigEntity mobileAppConfig = GpsSourceConfigEntity.builder()
                .user(userRepository.findById(userId))
                .sourceType(GpsSourceType.MOBILE_APP)
                .username(TestIds.uniqueValue("gps-point-mobile-source-user"))
                .active(true)
                .filterInaccurateData(false)
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .enableDuplicateDetection(false)
                .build();

        assertDoesNotThrow(() -> gpsPointService.saveMobileAppGpsPoints(List.of(first, duplicate), "pixel-9-pro", userId, GpsSourceType.MOBILE_APP, mobileAppConfig));

        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
    }

    @Test
    @Transactional
    public void testSaveMobileAppGpsPoints_FiltersMissingTimestampAndSavesInTimestampOrder() {
        Instant earlierTimestamp = Instant.parse("2026-05-15T09:00:00Z");
        Instant laterTimestamp = Instant.parse("2026-05-15T11:00:00Z");
        GpsPointDTO later = new GpsPointDTO(
                0L,
                laterTimestamp,
                new GpsPointDTO.CoordinatesDTO(41.0, -73.0),
                5.0,
                88.0,
                1.5,
                12.0,
                null
        );
        GpsPointDTO missingTimestamp = new GpsPointDTO(
                0L,
                null,
                new GpsPointDTO.CoordinatesDTO(42.0, -72.0),
                5.0,
                87.0,
                1.0,
                13.0,
                null
        );
        GpsPointDTO earlier = new GpsPointDTO(
                0L,
                earlierTimestamp,
                new GpsPointDTO.CoordinatesDTO(40.0, -74.0),
                4.0,
                86.0,
                0.5,
                14.0,
                null
        );
        GpsSourceConfigEntity mobileAppConfig = GpsSourceConfigEntity.builder()
                .user(userRepository.findById(userId))
                .sourceType(GpsSourceType.MOBILE_APP)
                .username(TestIds.uniqueValue("gps-point-mobile-source-user"))
                .active(true)
                .filterInaccurateData(false)
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .enableDuplicateDetection(false)
                .build();

        gpsPointService.saveMobileAppGpsPoints(List.of(later, missingTimestamp, earlier), "pixel-9-pro", userId, GpsSourceType.MOBILE_APP, mobileAppConfig);

        List<GpsPointEntity> savedPoints = gpsPointRepository.list("user.id = ?1 order by id asc", userId);
        assertEquals(2, savedPoints.size());
        assertEquals(earlierTimestamp, savedPoints.get(0).getTimestamp());
        assertEquals(laterTimestamp, savedPoints.get(1).getTimestamp());
    }

    @Test
    @Transactional
    public void testGetGpsPointPath_FiltersPointsAboveTimelineAccuracyThreshold() {
        setTimelineAccuracyPreferences(true, 60.0);
        savePathTestPoints();

        GpsPointPathDTO path = gpsPointService.getGpsPointPath(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:03:00Z")
        );

        List<Double> accuracies = path.getPoints().stream()
                .map(point -> ((GpsPointPathPointDTO) point).getAccuracy())
                .toList();

        assertEquals(List.of(11.0, 12.0), accuracies);
    }

    @Test
    @Transactional
    public void testGetGpsPointPath_KeepsHighAccuracyPointsWhenAccuracyValidationDisabled() {
        setTimelineAccuracyPreferences(false, 60.0);
        savePathTestPoints();

        GpsPointPathDTO path = gpsPointService.getGpsPointPath(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:03:00Z")
        );

        List<Double> accuracies = path.getPoints().stream()
                .map(point -> ((GpsPointPathPointDTO) point).getAccuracy())
                .toList();

        assertEquals(List.of(11.0, 700.0, 12.0), accuracies);
    }

    @Test
    @Transactional
    public void testGetRawGpsMapPoints_FiltersPointsAboveTimelineAccuracyThresholdAndCountsEligibleOnly() {
        setTimelineAccuracyPreferences(true, 60.0);
        savePathTestPoints();

        RawGpsPointMapResponseDTO limitedResponse = gpsPointService.getRawGpsMapPoints(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:03:00Z"),
                1
        );

        assertEquals(2, limitedResponse.getTotalCount());
        assertEquals(1, limitedResponse.getReturnedCount());
        assertEquals(1, limitedResponse.getLimit());
        assertTrue(limitedResponse.isLimited());
        assertEquals(List.of(11.0), limitedResponse.getPoints().stream()
                .map(point -> point.getAccuracy())
                .toList());

        RawGpsPointMapResponseDTO fullResponse = gpsPointService.getRawGpsMapPoints(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:03:00Z"),
                10
        );

        assertEquals(2, fullResponse.getTotalCount());
        assertEquals(2, fullResponse.getReturnedCount());
        assertFalse(fullResponse.isLimited());
        assertEquals(List.of(11.0, 12.0), fullResponse.getPoints().stream()
                .map(point -> point.getAccuracy())
                .toList());
    }

    @Test
    @Transactional
    public void testGetRawGpsMapPoints_KeepsHighAccuracyPointsWhenAccuracyValidationDisabled() {
        setTimelineAccuracyPreferences(false, 60.0);
        savePathTestPoints();

        RawGpsPointMapResponseDTO response = gpsPointService.getRawGpsMapPoints(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:03:00Z"),
                10
        );

        assertEquals(3, response.getTotalCount());
        assertEquals(3, response.getReturnedCount());
        assertFalse(response.isLimited());
        assertEquals(List.of(11.0, 700.0, 12.0), response.getPoints().stream()
                .map(point -> point.getAccuracy())
                .toList());
    }

    @Test
    @Transactional
    public void testGetGpsPointPath_KeepsNullAccuracyPoints() {
        setTimelineAccuracyPreferences(true, 60.0);
        GpsSourceConfigEntity mobileAppConfig = createMobileAppTestConfig();
        gpsPointService.saveMobileAppGpsPoint(
                createMobilePoint("2026-05-15T10:00:00Z", 40.0, -74.0, null),
                "pixel-9-pro",
                userId,
                GpsSourceType.MOBILE_APP,
                mobileAppConfig
        );

        GpsPointPathDTO path = gpsPointService.getGpsPointPath(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:01:00Z")
        );

        assertEquals(1, path.getPoints().size());
        assertNull(((GpsPointPathPointDTO) path.getPoints().get(0)).getAccuracy());

        RawGpsPointMapResponseDTO response = gpsPointService.getRawGpsMapPoints(
                userId,
                Instant.parse("2026-05-15T09:59:00Z"),
                Instant.parse("2026-05-15T10:01:00Z"),
                10
        );

        assertEquals(1, response.getTotalCount());
        assertEquals(1, response.getReturnedCount());
        assertNull(response.getPoints().get(0).getAccuracy());
    }

    @Test
    @Transactional
    public void testGetGpsPointSummary_BasicFunctionality() {
        Instant utcTodayNoon = Instant.now()
                .atZone(ZoneId.of("UTC"))
                .toLocalDate()
                .atTime(12, 0)
                .toInstant(ZoneOffset.UTC);
        Instant sevenDaysAgo = utcTodayNoon.minus(7, ChronoUnit.DAYS);
        Instant oneDayAgo = utcTodayNoon.minus(1, ChronoUnit.DAYS);
        // Create test GPS points with deterministic timestamps
        createTestGpsPoint(sevenDaysAgo);
        createTestGpsPoint(oneDayAgo);
        createTestGpsPoint(utcTodayNoon); // Should count as "today" in UTC
        GpsPointSummaryDTO summary = gpsPointService.getGpsPointSummary(userId);
        assertEquals(3, summary.getTotalPoints());
        assertEquals(1, summary.getPointsToday());
        assertEquals(sevenDaysAgo, summary.getFirstPointDate());
        assertEquals(utcTodayNoon, summary.getLastPointDate());
    }
    @Test
    @Transactional
    public void testGetGpsPointSummary_TimezoneIssue_GMT_Plus3_Early_Morning() {
        // Test timezone fix: Create points that are clearly from different days in user timezone
        ZoneId gmtPlus3 = ZoneId.of("Europe/Kyiv"); // GMT+3
        // Use "today" in GMT+3 timezone to avoid date skew under timezone matrix runs
        LocalDate today = LocalDate.now(gmtPlus3);
        LocalDate yesterday = today.minusDays(1);
        // Create test points:
        // 1. Point from "yesterday" in GMT+3 (should NOT count as today)
        ZonedDateTime yesterdayPoint = yesterday.atTime(20, 0).atZone(gmtPlus3);
        createTestGpsPoint(yesterdayPoint.toInstant());
        // 2. Point from "today" in GMT+3 (should count as today)
        ZonedDateTime todayPoint = today.atTime(1, 0).atZone(gmtPlus3);
        createTestGpsPoint(todayPoint.toInstant());
        // Test the FIXED implementation with correct timezone
        GpsPointSummaryDTO summaryFixed = gpsPointService.getGpsPointSummary(userId, gmtPlus3);
        assertEquals(2, summaryFixed.getTotalPoints());
        assertEquals(1, summaryFixed.getPointsToday(),
                "Expected 1 point for 'today' from user's GMT+3 perspective with the timezone fix. " +
                        "Got " + summaryFixed.getPointsToday() + " points.");
        // Also test that UTC-based calculation might give different result
        GpsPointSummaryDTO summaryUtc = gpsPointService.getGpsPointSummary(userId);
        // UTC might count differently due to timezone offset
        assertTrue(summaryUtc.getPointsToday() >= 0 && summaryUtc.getPointsToday() <= 2,
                "UTC-based count should be between 0-2, got: " + summaryUtc.getPointsToday());
    }
    @Test
    @Transactional
    public void testGetGpsPointSummary_TimezoneIssue_GMT_Minus8_Late_Evening() {
        // Test timezone fix: Create points that are clearly from different days in user timezone
        ZoneId gmtMinus8 = ZoneId.of("America/Los_Angeles"); // GMT-8 (Pacific Time)
        // Use "today" in Pacific Time zone (not server timezone) to ensure test works regardless of server location
        LocalDate todayPacific = LocalDate.now(gmtMinus8);
        LocalDate tomorrowPacific = todayPacific.plusDays(1);
        // Create test points:
        // 1. Point from "today" in GMT-8 (should count as today from Pacific perspective)
        ZonedDateTime todayPoint = todayPacific.atTime(10, 0).atZone(gmtMinus8);
        createTestGpsPoint(todayPoint.toInstant());
        // 2. Point from "tomorrow" in GMT-8 (should NOT count as today from Pacific perspective)
        ZonedDateTime tomorrowPoint = tomorrowPacific.atTime(1, 0).atZone(gmtMinus8);
        createTestGpsPoint(tomorrowPoint.toInstant());
        // Test the FIXED implementation with correct timezone
        GpsPointSummaryDTO summaryFixed = gpsPointService.getGpsPointSummary(userId, gmtMinus8);
        assertEquals(2, summaryFixed.getTotalPoints());
        assertEquals(1, summaryFixed.getPointsToday(),
                "Expected 1 point for 'today' from user's GMT-8 perspective with the timezone fix. " +
                        "Got " + summaryFixed.getPointsToday() + " points.");
        // Also test that UTC-based calculation might give different result
        GpsPointSummaryDTO summaryUtc = gpsPointService.getGpsPointSummary(userId);
        // UTC might count differently due to timezone offset
        assertTrue(summaryUtc.getPointsToday() >= 0 && summaryUtc.getPointsToday() <= 2,
                "UTC-based count should be between 0-2, got: " + summaryUtc.getPointsToday());
    }
    @Test
    @Transactional
    public void testGetGpsPointSummary_EmptyResult() {
        // Test with no GPS points
        GpsPointSummaryDTO summary = gpsPointService.getGpsPointSummary(userId);
        assertEquals(0, summary.getTotalPoints());
        assertEquals(0, summary.getPointsToday());
        assertNull(summary.getFirstPointDate());
        assertNull(summary.getLastPointDate());
    }
    @Test
    @Transactional
    public void testSaveOwnTracksGpsPoint_TelemetryPersistedWhenPresent() {
        long tst = Instant.now().plusSeconds(20000).getEpochSecond();
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .ext(Map.of(
                        "ignition", 1,
                        "batt_v", 12.6
                ))
                .build();
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
        assertEquals(1, gpsPointRepository.count("user.id = ?1", userId));
        GpsPointEntity savedPoint = gpsPointRepository.find("user.id = ?1 ORDER BY createdAt DESC", userId).firstResult();
        assertNotNull(savedPoint.getTelemetry());
        assertEquals(1, savedPoint.getTelemetry().get("ignition"));
        assertEquals(12.6, ((Number) savedPoint.getTelemetry().get("batt_v")).doubleValue(), 0.000001);
    }
    @Test
    @Transactional
    public void testTelemetryVisibilityRulesAppliedToGpsDataAndCurrentPopup() {
        long tst = Instant.now().plusSeconds(20000).getEpochSecond();
        List<GpsTelemetryMappingEntry> mapping = List.of(
                GpsTelemetryMappingEntry.builder()
                        .key("ignition")
                        .label("Ignition")
                        .type("boolean")
                        .enabled(true)
                        .order(10)
                        .showInGpsData(true)
                        .showInCurrentPopup(false)
                        .build(),
                GpsTelemetryMappingEntry.builder()
                        .key("batt_v")
                        .label("Battery")
                        .type("number")
                        .unit("V")
                        .enabled(true)
                        .order(20)
                        .showInGpsData(false)
                        .showInCurrentPopup(true)
                        .build(),
                GpsTelemetryMappingEntry.builder()
                        .key("geofence_lat")
                        .label("Geofence Lat")
                        .type("number")
                        .enabled(false)
                        .order(30)
                        .showInGpsData(true)
                        .showInCurrentPopup(true)
                        .build()
        );
        telemetryConfigService.upsertConfig(userId, GpsSourceType.OWNTRACKS, mapping);
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .lat(40.0)
                .lon(-74.0)
                .tst(tst)
                .ext(Map.of(
                        "ignition", 1,
                        "batt_v", 12.55,
                        "geofence_lat", 49.1
                ))
                .build();
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
        var page = gpsPointService.getGpsPointsPageWithFilters(
                userId,
                GpsPointFilterDTO.builder().build(),
                1,
                10,
                "timestamp",
                "desc"
        );
        assertEquals(1, page.getData().size());
        assertNotNull(page.getData().get(0).getTelemetryGpsData());
        assertEquals(1, page.getData().get(0).getTelemetryGpsData().size());
        assertEquals("ignition", page.getData().get(0).getTelemetryGpsData().get(0).getKey());
        GpsPointPathDTO path = gpsPointService.getGpsPointPath(userId, Instant.EPOCH, Instant.now().plusSeconds(30000));
        assertEquals(1, path.getPoints().size());
        GpsPointPathPointDTO pathPoint = (GpsPointPathPointDTO) path.getPoints().get(0);
        assertNotNull(pathPoint.getTelemetryCurrentPopup());
        assertEquals(1, pathPoint.getTelemetryCurrentPopup().size());
        assertEquals("batt_v", pathPoint.getTelemetryCurrentPopup().get(0).getKey());
        assertTrue(pathPoint.getTelemetryCurrentPopup().stream().noneMatch(item -> "geofence_lat".equals(item.getKey())));
    }
    private void enableBoatForTestUser() {
        UserEntity user = userRepository.findById(userId);
        user.setTimelinePreferences(TimelinePreferences.builder()
                .boatEnabled(true)
                .build());
        userRepository.persist(user);
    }

    private void installTestWaterDataset() {
        entityManager.createNativeQuery("""
                        INSERT INTO water_surface_polygons (source, source_id, name, water_type, geom)
                        VALUES (
                            'gps-point-service-test',
                            'test-water',
                            'Test Water',
                            'lake',
                            ST_Multi(ST_GeomFromText('POLYGON((-75 39, -75 41, -73 41, -73 39, -75 39))', 4326))
                        )
                        """)
                .executeUpdate();
        entityManager.createNativeQuery("""
                        INSERT INTO geo_dataset_metadata (
                            dataset_name, source_url, source_version, license, attribution, feature_count, imported_at
                        )
                        VALUES (
                            'water_surface_polygons:gps_point_service_test',
                            'test',
                            'test',
                            'test',
                            'test',
                            1,
                            NOW()
                        )
                        ON CONFLICT (dataset_name) DO UPDATE SET
                            feature_count = EXCLUDED.feature_count,
                            imported_at = EXCLUDED.imported_at
                        """)
                .executeUpdate();
    }

    private long countGpsPointEnvironmentRows(UUID targetUserId) {
        Number result = (Number) entityManager.createNativeQuery("""
                        SELECT COUNT(*)
                        FROM gps_point_environment env
                        JOIN gps_points gp ON gp.id = env.gps_point_id
                        WHERE gp.user_id = :userId
                        """)
                .setParameter("userId", targetUserId)
                .getSingleResult();
        return result.longValue();
    }

    private long countWaterEvidenceRows(UUID targetUserId, boolean onWater) {
        Number result = (Number) entityManager.createNativeQuery("""
                        SELECT COUNT(*)
                        FROM gps_point_environment env
                        JOIN gps_points gp ON gp.id = env.gps_point_id
                        WHERE gp.user_id = :userId
                          AND env.on_water = :onWater
                        """)
                .setParameter("userId", targetUserId)
                .setParameter("onWater", onWater)
                .getSingleResult();
        return result.longValue();
    }

    private Boolean findOnlyWaterEvidenceValue(UUID targetUserId) {
        return (Boolean) entityManager.createNativeQuery("""
                        SELECT env.on_water
                        FROM gps_point_environment env
                        JOIN gps_points gp ON gp.id = env.gps_point_id
                        WHERE gp.user_id = :userId
                        """)
                .setParameter("userId", targetUserId)
                .getSingleResult();
    }

    private void createTestGpsPoint(Instant timestamp) {
        OwnTracksLocationMessage message = OwnTracksLocationMessage.builder()
                .type("location")
                .acc(5.0)
                .lat(40.0)
                .lon(-74.0)
                .tst(timestamp.getEpochSecond())
                .vel(2.0)
                .build();
        gpsPointService.saveOwnTracksGpsPoint(message, userId, "test-device", GpsSourceType.OWNTRACKS, testConfig);
    }

    private void setTimelineAccuracyPreferences(boolean useVelocityAccuracy, double maxAccuracyThreshold) {
        UserEntity user = userRepository.findById(userId);
        user.setTimelinePreferences(TimelinePreferences.builder()
                .useVelocityAccuracy(useVelocityAccuracy)
                .staypointMaxAccuracyThreshold(maxAccuracyThreshold)
                .build());
        userRepository.persist(user);
    }

    private void savePathTestPoints() {
        GpsSourceConfigEntity mobileAppConfig = createMobileAppTestConfig();
        gpsPointService.saveMobileAppGpsPoints(List.of(
                        createMobilePoint("2026-05-15T10:00:00Z", 40.0, -74.0, 11.0),
                        createMobilePoint("2026-05-15T10:01:00Z", 40.1, -74.1, 700.0),
                        createMobilePoint("2026-05-15T10:02:00Z", 40.2, -74.2, 12.0)
                ),
                "pixel-9-pro",
                userId,
                GpsSourceType.MOBILE_APP,
                mobileAppConfig);
    }

    private GpsPointDTO createMobilePoint(String timestamp, double lat, double lng, Double accuracy) {
        return new GpsPointDTO(
                0L,
                Instant.parse(timestamp),
                new GpsPointDTO.CoordinatesDTO(lat, lng),
                accuracy,
                88.0,
                1.5,
                12.0,
                null
        );
    }

    private GpsSourceConfigEntity createMobileAppTestConfig() {
        return GpsSourceConfigEntity.builder()
                .user(userRepository.findById(userId))
                .sourceType(GpsSourceType.MOBILE_APP)
                .username(TestIds.uniqueValue("gps-point-mobile-source-user"))
                .active(true)
                .filterInaccurateData(false)
                .maxAllowedAccuracy(100)
                .maxAllowedSpeed(250)
                .enableDuplicateDetection(false)
                .build();
    }

    public static class DisableLocationTimeThresholdTestProfile implements QuarkusTestProfile {
        @Override
        public Map<String, String> getConfigOverrides() {
            return Map.of("geopulse.gps.duplicate-detection.location-time-threshold-minutes", "-1");
        }
    }
}
