package com.livingworld.testing;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.livingworld.config.SimulationConfig;
import com.livingworld.core.services.PersistenceService;
import com.livingworld.core.services.ServiceRegistry;
import com.livingworld.core.simulation.DefaultTimeService;
import com.livingworld.core.simulation.RegionManager;
import com.livingworld.core.simulation.RegionUpdateJob;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.core.simulation.SimulationScheduler;
import com.livingworld.core.simulation.UpdateReason;
import com.livingworld.debug.SimulationProfileSnapshot;
import com.livingworld.debug.SimulationProfiler;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleContext;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionLifecycleState;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;

class LongRunSimulationTest {

    private static final int REGION_COUNT = 1_000;
    private static final int SIMULATION_CYCLES = 10_000;
    private static final int JOBS_PER_CYCLE = 50;
    private static final int SAVE_INTERVAL_CYCLES = 100;

    @Test
    void simulationRemainsStableUnderSustainedLoad() {
        org.junit.jupiter.api.Assertions.assertTimeout(
                Duration.ofSeconds(30),
                this::runSustainedSimulation);
    }

    private void runSustainedSimulation() {
        SimulationConfig config = new SimulationConfig();
        config.setMaxRegionsPerCycle(JOBS_PER_CYCLE);
        config.setMaxMillisecondsPerCycle(1_000);
        config.setEmergencyStopMilliseconds(1_000);

        SimulationScheduler scheduler = new SimulationScheduler(config);
        InMemoryRegionManager regionManager = new InMemoryRegionManager();
        createRegions(regionManager);

        TestSimulationModule testModule = new TestSimulationModule();
        ModuleRegistry moduleRegistry = new ModuleRegistry();
        moduleRegistry.register(testModule);
        moduleRegistry.initializeAll(new ModuleContext(new ServiceRegistry()));

        DefaultTimeService timeService = new DefaultTimeService();
        SimulationProfiler profiler = new SimulationProfiler();
        InMemoryPersistenceService persistence =
                new InMemoryPersistenceService(regionManager.allRegions());
        SimulationManager simulationManager = new SimulationManager(
                scheduler,
                regionManager,
                moduleRegistry,
                new LivingWorldEventBus(),
                timeService,
                persistence,
                profiler);

        for (int cycle = 0; cycle < SIMULATION_CYCLES; cycle++) {
            queueCycleJobs(scheduler, regionManager, cycle);
            simulationManager.runSimulationCycle();

            if ((cycle + 1) % SAVE_INTERVAL_CYCLES == 0) {
                persistence.saveDirtyRegions();
            }
        }
        persistence.saveDirtyRegions();

        SimulationProfileSnapshot snapshot = profiler.createSnapshot();
        assertTrue(testModule.isInitialized());
        assertEquals(SIMULATION_CYCLES * JOBS_PER_CYCLE, testModule.getUpdateCount());
        assertEquals(SIMULATION_CYCLES, timeService.getSimulationTick());
        assertEquals(SIMULATION_CYCLES, scheduler.getSimulationTickCounter());
        assertEquals(0, scheduler.getQueuedJobCount());
        assertTrue(persistence.getSaveCount() > 0);
        assertTrue(persistence.getMaxDirtyObserved() <= REGION_COUNT);
        assertEquals(0, persistence.countDirtyRegions());
        assertTrue(snapshot.totalCycleNanos() > 0);
        assertTrue(snapshot.moduleTimings().containsKey(TestSimulationModule.MODULE_ID));
        assertEquals(JOBS_PER_CYCLE, snapshot.regionsUpdated());
        assertFalse(snapshot.budgetExceeded());
        assertTrue(regionManager.allRegions().stream()
                .allMatch(region -> region.getLifecycleState() == RegionLifecycleState.ACTIVE));
    }

    private static void createRegions(InMemoryRegionManager regionManager) {
        RegionFactory factory = new RegionFactory();
        for (int index = 0; index < REGION_COUNT; index++) {
            Region region = factory.createNewRegion(
                    new RegionCoordinate("minecraft:overworld", index, 0),
                    0);
            region.clearDirty();
            regionManager.add(region);
        }
    }

    private static void queueCycleJobs(
            SimulationScheduler scheduler,
            InMemoryRegionManager regionManager,
            int cycle) {
        int firstRegion = cycle * JOBS_PER_CYCLE;
        for (int offset = 0; offset < JOBS_PER_CYCLE; offset++) {
            Region region = regionManager.get((firstRegion + offset) % REGION_COUNT);
            scheduler.queueRegion(new RegionUpdateJob(
                    region.getCoordinate(),
                    1,
                    cycle,
                    Set.of(TestSimulationModule.MODULE_ID),
                    UpdateReason.NORMAL_ROLLING_UPDATE));
        }
    }

    private static final class InMemoryRegionManager implements RegionManager {
        private final Map<RegionCoordinate, Region> regions = new LinkedHashMap<>();
        private final List<Region> orderedRegions = new ArrayList<>();

        private void add(Region region) {
            regions.put(region.getCoordinate(), region);
            orderedRegions.add(region);
        }

        private Region get(int index) {
            return orderedRegions.get(index);
        }

        private Collection<Region> allRegions() {
            return orderedRegions;
        }

        @Override
        public Optional<Region> resolve(RegionCoordinate coordinate) {
            return Optional.ofNullable(regions.get(coordinate));
        }

        @Override
        public List<Region> resolveAll(List<RegionCoordinate> coordinates) {
            return coordinates.stream()
                    .map(regions::get)
                    .filter(java.util.Objects::nonNull)
                    .toList();
        }

        @Override
        public void markDirty(Region region) {
            region.markDirty();
        }
    }

    private static final class InMemoryPersistenceService implements PersistenceService {
        private final Collection<Region> regions;
        private final Map<RegionCoordinate, Region> savedRegions = new LinkedHashMap<>();
        private int saveCount;
        private int maxDirtyObserved;

        private InMemoryPersistenceService(Collection<Region> regions) {
            this.regions = regions;
        }

        @Override
        public void saveDirtyRegions() {
            maxDirtyObserved = Math.max(maxDirtyObserved, countDirtyRegions());
            regions.stream()
                    .filter(Region::isDirty)
                    .forEach(this::saveRegion);
        }

        private int countDirtyRegions() {
            return (int) regions.stream().filter(Region::isDirty).count();
        }

        private int getSaveCount() {
            return saveCount;
        }

        private int getMaxDirtyObserved() {
            return maxDirtyObserved;
        }

        @Override
        public void markRegionDirty(Region region) {
            region.markDirty();
        }

        @Override
        public void saveRegion(Region region) {
            savedRegions.put(region.getCoordinate(), region);
            region.clearDirty();
            saveCount++;
        }

        @Override
        public Optional<Region> loadRegion(RegionCoordinate coordinate) {
            return Optional.ofNullable(savedRegions.get(coordinate));
        }

        @Override
        public void flushAll() {
            saveDirtyRegions();
        }
    }
}
