package org.github.tess1o.geopulse.exportimport;
import org.github.tess1o.geopulse.testsupport.TestIds;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.github.tess1o.geopulse.db.PostgisTestResource;
import org.github.tess1o.geopulse.export.model.ExportDateRange;
import org.github.tess1o.geopulse.export.model.ExportJob;
import org.github.tess1o.geopulse.export.service.ExportDataGenerator;
import org.github.tess1o.geopulse.favorites.model.FavoriteLocationType;
import org.github.tess1o.geopulse.favorites.model.FavoritesEntity;
import org.github.tess1o.geopulse.favorites.repository.FavoritesRepository;
import org.github.tess1o.geopulse.geocoding.model.ReverseGeocodingLocationEntity;
import org.github.tess1o.geopulse.geocoding.repository.ReverseGeocodingLocationRepository;
import org.github.tess1o.geopulse.importdata.model.ImportJob;
import org.github.tess1o.geopulse.importdata.model.ImportOptions;
import org.github.tess1o.geopulse.importdata.service.ImportDataService;
import org.github.tess1o.geopulse.shared.exportimport.ExportImportConstants;
import org.github.tess1o.geopulse.shared.geo.GeoUtils;
import org.github.tess1o.geopulse.streaming.model.domain.LocationSource;
import org.github.tess1o.geopulse.streaming.model.entity.TimelineStayEntity;
import org.github.tess1o.geopulse.streaming.repository.TimelineStayRepository;
import org.github.tess1o.geopulse.testsupport.SerializedDatabaseTest;
import org.github.tess1o.geopulse.user.model.UserEntity;
import org.github.tess1o.geopulse.user.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@QuarkusTest
@QuarkusTestResource(value = PostgisTestResource.class)
@Slf4j
@SerializedDatabaseTest
class SequenceResetTest {
    @Inject
    ExportDataGenerator exportDataGenerator;
    @Inject
    ImportDataService importDataService;
    @Inject
    UserRepository userRepository;
    @Inject
    TimelineStayRepository timelineStayRepository;
    @Inject
    FavoritesRepository favoritesRepository;
    @Inject
    ReverseGeocodingLocationRepository reverseGeocodingLocationRepository;
    @Inject
    org.github.tess1o.geopulse.shared.exportimport.SequenceResetService sequenceResetService;
    private UserEntity testUser;
    @BeforeEach
    @Transactional
    void setUp() {
        // Create test user
        testUser = new UserEntity();
        testUser.setEmail(TestIds.uniqueEmail("it-user"));
        testUser.setFullName("Sequence Test User");
        testUser.setPasswordHash("test-hash");
        testUser.setCreatedAt(Instant.now());
        userRepository.persist(testUser);
    }
    @AfterEach
    @Transactional
    void tearDown() {
    }
    @Transactional
    void cleanupTestData() {
        reverseGeocodingLocationRepository.delete("providerName = ?1", "sequence-test-provider");
        // Reset sequences to ensure clean state for test
        sequenceResetService.resetAllSequences();
    }
    @Test
    @org.junit.jupiter.api.condition.EnabledIfSystemProperty(named = "test.sequence.isolation", matches = "true", disabledReason = "This test is disabled by default because it's sensitive to Hibernate's sequence allocation strategy when run with other tests. "
            +
            "The sequence reset functionality works correctly in production. " +
            "To run this test in isolation, use: mvn test -Dtest=SequenceResetTest -Dtest.sequence.isolation=true")
    void testSequenceResetAfterImport() throws Exception {
        performStepInTransaction(() -> {
            log.info("=== Testing Sequence Reset After Import ===");
            // Step 1: Create entities with high IDs using native SQL (simulating import)
            log.info("Step 1: Creating entities with high IDs using native SQL");
            var entityManager = reverseGeocodingLocationRepository.getEntityManager();
            // Create reverse geocoding location with high ID using native SQL
            entityManager.createNativeQuery("""
                    INSERT INTO reverse_geocoding_location
                    (id, request_coordinates, result_coordinates, display_name, provider_name,
                     created_at, last_accessed_at, city, country)
                    VALUES (9999, ST_GeomFromText('POINT(-122.4194 37.7749)', 4326),
                            ST_GeomFromText('POINT(-122.4194 37.7749)', 4326),
                            'Test Location', 'sequence-test-provider', NOW(), NOW(), 'Test City', 'Test Country')
                    """).executeUpdate();
            // Create favorite with high ID using native SQL
            entityManager.createNativeQuery("""
                    INSERT INTO favorite_locations
                    (id, user_id, name, city, country, type, geometry)
                    VALUES (8888, ?, 'Test Favorite', 'Test City', 'Test Country', 'POINT',
                            ST_GeomFromText('POINT(-122.4194 37.7749)', 4326))
                    """).setParameter(1, testUser.getId()).executeUpdate();
            // Create timeline stay with high ID using native SQL
            entityManager.createNativeQuery("""
                    INSERT INTO timeline_stays
                    (id, user_id, timestamp, latitude, longitude, stay_duration, location_name,
                     location_source, favorite_id, geocoding_id, created_at, timeline_version,
                     last_updated, is_stale)
                    VALUES (7777, ?, NOW(), 37.7749, -122.4194, 60, 'Test Stay', 'HISTORICAL',
                            8888, 9999, NOW(), 'test', NOW(), false)
                    """).setParameter(1, testUser.getId()).executeUpdate();
        });
        // Step 2: Export the data
        byte[] exportedData = performStepInTransaction(() -> {
            log.info("Step 2: Exporting data with high IDs");
            ExportDateRange dateRange = new ExportDateRange();
            dateRange.setStartDate(Instant.now().minus(1, ChronoUnit.HOURS));
            dateRange.setEndDate(Instant.now().plus(1, ChronoUnit.HOURS));
            ExportJob exportJob = new ExportJob(
                    testUser.getId(),
                    List.of(ExportImportConstants.DataTypes.TIMELINE, ExportImportConstants.DataTypes.FAVORITES,
                            ExportImportConstants.DataTypes.REVERSE_GEOCODING_LOCATION),
                    dateRange,
                    ExportImportConstants.Formats.JSON);
            exportDataGenerator.generateGeoPulseNativeExport(exportJob);
            byte[] data = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(exportJob.getTempFilePath()));
            log.info("Export completed with {} bytes", data.length);
            return data;
        });
        // Step 3: Delete the original data
        performStepInTransaction(() -> {
            log.info("Step 3: Deleting original data");
            timelineStayRepository.deleteById(7777L);
            favoritesRepository.deleteById(8888L);
            reverseGeocodingLocationRepository.deleteById(9999L);
        });
        // Step 4: Import the data (this should reset sequences)
        log.info("Step 4: Importing data (should reset sequences)");
        ImportOptions importOptions = new ImportOptions();
        importOptions.setDataTypes(
                List.of(ExportImportConstants.DataTypes.TIMELINE, ExportImportConstants.DataTypes.FAVORITES,
                        ExportImportConstants.DataTypes.REVERSE_GEOCODING_LOCATION));
        importOptions.setImportFormat(ExportImportConstants.Formats.GEOPULSE);
        ImportJob importJob = new ImportJob(testUser.getId(), importOptions, "test-export.zip", exportedData);
        importDataService.processImportData(importJob);
        log.info("Import completed");
        // Step 5: Verify imported data exists with original high IDs
        performStepInTransaction(() -> {
            log.info("Step 5: Verifying imported data has original high IDs");
            var importedGeocoding = reverseGeocodingLocationRepository.findById(9999L);
            assertNotNull(importedGeocoding, "Imported geocoding location should exist with original ID 9999");
            var importedFavorite = favoritesRepository.findById(8888L);
            assertNotNull(importedFavorite, "Imported favorite should exist with original ID 8888");
            var importedStay = timelineStayRepository.findById(7777L);
            assertNotNull(importedStay, "Imported stay should exist with original ID 7777");
        });
        // Step 6: Create new entities and verify they get IDs higher than imported ones
        performStepInTransaction(() -> {
            log.info("Step 6: Creating new entities to test sequence reset");
            // Create new reverse geocoding location
            var newGeocoding = new ReverseGeocodingLocationEntity();
            newGeocoding.setRequestCoordinates(GeoUtils.createPoint(-122.4094, 37.7849));
            newGeocoding.setResultCoordinates(GeoUtils.createPoint(-122.4094, 37.7849));
            newGeocoding.setDisplayName("New Test Location");
            newGeocoding.setProviderName("sequence-test-provider");
            newGeocoding.setCreatedAt(Instant.now());
            newGeocoding.setLastAccessedAt(Instant.now());
            newGeocoding.setCity("New Test City");
            newGeocoding.setCountry("New Test Country");
            reverseGeocodingLocationRepository.persist(newGeocoding);
            // Create new favorite
            var newFavorite = new FavoritesEntity();
            newFavorite.setUser(testUser);
            newFavorite.setName("New Test Favorite");
            newFavorite.setCity("New Test City");
            newFavorite.setCountry("New Test Country");
            newFavorite.setType(FavoriteLocationType.POINT);
            newFavorite.setGeometry(GeoUtils.createPoint(-122.4094, 37.7849));
            favoritesRepository.persist(newFavorite);
            // Create new timeline stay
            var newStay = TimelineStayEntity.builder()
                    .user(testUser)
                    .timestamp(Instant.now())
                    .location(GeoUtils.createPoint(-122.4094, 37.7849))
                    .stayDuration(45)
                    .locationName("New Test Stay")
                    .locationSource(LocationSource.HISTORICAL)
                    .favoriteLocation(newFavorite)
                    .geocodingLocation(newGeocoding)
                    .createdAt(Instant.now())
                    .lastUpdated(Instant.now())
                    .build();
            timelineStayRepository.persist(newStay);
            // Step 7: Verify new entities have IDs higher than the imported ones
            // This is the core requirement - avoid ID conflicts with imported data
            log.info("Step 7: Verifying new entities have proper IDs");
            assertNotNull(newGeocoding.getId(), "New geocoding location should have an ID");
            assertTrue(newGeocoding.getId() > 9999L,
                    "New geocoding location ID should be > 9999 (imported ID), was: " + newGeocoding.getId());
            assertNotNull(newFavorite.getId(), "New favorite should have an ID");
            assertTrue(newFavorite.getId() > 8888L,
                    "New favorite ID should be > 8888 (imported ID), was: " + newFavorite.getId());
            assertNotNull(newStay.getId(), "New stay should have an ID");
            assertTrue(newStay.getId() > 7777L,
                    "New stay ID should be > 7777 (imported ID), was: " + newStay.getId());
            log.info("Sequence reset verification successful - no ID conflicts:");
            log.info("  - Original geocoding ID: 9999, New geocoding ID: {}", newGeocoding.getId());
            log.info("  - Original favorite ID: 8888, New favorite ID: {}", newFavorite.getId());
            log.info("  - Original stay ID: 7777, New stay ID: {}", newStay.getId());
            log.info("=== Sequence Reset Test Completed Successfully ===");
        });
    }
    @FunctionalInterface
    private interface TransactionalOperation<T> {
        T execute() throws Exception;
    }
    @FunctionalInterface
    private interface TransactionalAction {
        void execute() throws Exception;
    }
    @Transactional
    public <T> T performStepInTransaction(TransactionalOperation<T> operation) {
        try {
            return operation.execute();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    @Transactional
    public void performStepInTransaction(TransactionalAction action) {
        try {
            action.execute();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
