package com.livingworld.core.simulation;

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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.ArrayList;
import java.util.Collections;
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;

import com.livingworld.config.SimulationConfig;
import com.livingworld.core.services.PersistenceService;
import com.livingworld.core.services.TimeService;
import com.livingworld.data.serialization.PersistenceReader;
import com.livingworld.data.serialization.PersistenceWriter;
import com.livingworld.events.BaseLivingWorldEvent;
import com.livingworld.events.LivingWorldEvent;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleContext;
import com.livingworld.modules.ModuleMetadata;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.modules.ModuleUpdateResult;
import com.livingworld.modules.RegionUpdateContext;
import com.livingworld.modules.ServerContext;
import com.livingworld.modules.SimulationModule;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;

class SimulationManagerTest {

    @Test
    void runsRequestedModulesPublishesEventsAndMarksChangedRegionDirty() {
        Fixture fixture = new Fixture(25);
        TestModule skipped = new TestModule("skipped", false, 0);
        TestModule ecology = new TestModule("ecology", true, 0);
        fixture.modules.register(skipped);
        fixture.modules.register(ecology);

        List<String> receivedEvents = new ArrayList<>();
        fixture.eventBus.register("region_changed", event -> receivedEvents.add(event.sourceModuleId()));
        fixture.queue(Set.of("ecology"));

        fixture.manager.runSimulationCycle();

        assertEquals(0, skipped.updateCount);
        assertEquals(1, ecology.updateCount);
        assertEquals(List.of("ecology"), receivedEvents);
        assertTrue(fixture.region.isDirty());
        assertEquals(1, fixture.regionManager.markDirtyCount);
        assertEquals(1, fixture.timeService.getSimulationTick());
        assertEquals(1, fixture.scheduler.getSimulationTickCounter());
        assertEquals(1, fixture.profiler.startCount);
        assertEquals(1, fixture.profiler.endCount);
    }

    @Test
    void noChangeResultDoesNotMarkRegionDirty() {
        Fixture fixture = new Fixture(25);
        fixture.modules.register(new TestModule("ecology", false, 0));
        fixture.queue(Set.of());

        fixture.manager.runSimulationCycle();

        assertFalse(fixture.region.isDirty());
        assertEquals(0, fixture.regionManager.markDirtyCount);
    }

    @Test
    void forcedTicksRunCyclesWithoutWaitingForMinecraftInterval() {
        Fixture fixture = new Fixture(25);
        TestModule module = new TestModule("ecology", false, 0);
        fixture.modules.register(module);
        fixture.queue(Set.of());

        fixture.manager.runForcedSimulationTicks(3);

        assertEquals(1, module.updateCount);
        assertEquals(3, fixture.timeService.getSimulationTick());
        assertEquals(3, fixture.scheduler.getSimulationTickCounter());
        assertEquals(0, fixture.scheduler.getMinecraftTickCounter());
    }

    @Test
    void forcedTicksAcceptSafeMaximumAndRejectValuesOutsideRange() {
        Fixture fixture = new Fixture(25);

        assertDoesNotThrow(() -> fixture.manager.runForcedSimulationTicks(
                SimulationManager.MAX_FORCED_SIMULATION_TICKS));
        assertEquals(
                SimulationManager.MAX_FORCED_SIMULATION_TICKS,
                fixture.timeService.getSimulationTick());
        assertThrows(IllegalArgumentException.class,
                () -> fixture.manager.runForcedSimulationTicks(0));
        assertThrows(IllegalArgumentException.class,
                () -> fixture.manager.runForcedSimulationTicks(
                        SimulationManager.MAX_FORCED_SIMULATION_TICKS + 1));
    }

    @Test
    void timeBudgetRequeuesJobsNotYetProcessed() {
        Fixture fixture = new Fixture(1);
        fixture.modules.register(new TestModule("slow", false, 10));
        fixture.queueAt(new RegionCoordinate("minecraft:overworld", 0, 0));
        Region secondRegion = new RegionFactory().createNewRegion(
                new RegionCoordinate("minecraft:overworld", 1, 0), 0);
        secondRegion.clearDirty();
        fixture.regionManager.regions.put(secondRegion.getCoordinate(), secondRegion);
        fixture.queueAt(secondRegion.getCoordinate());

        fixture.manager.runSimulationCycle();

        assertEquals(1, fixture.scheduler.getQueuedJobCount());
    }

    private static final class Fixture {
        private final SimulationScheduler scheduler;
        private final TestRegionManager regionManager = new TestRegionManager();
        private final ModuleRegistry modules = new ModuleRegistry();
        private final LivingWorldEventBus eventBus = new LivingWorldEventBus();
        private final TestTimeService timeService = new TestTimeService();
        private final TestProfiler profiler = new TestProfiler();
        private final Region region;
        private final SimulationManager manager;

        private Fixture(int maxMillisecondsPerCycle) {
            SimulationConfig config = new SimulationConfig();
            config.setMaxMillisecondsPerCycle(maxMillisecondsPerCycle);
            config.setEmergencyStopMilliseconds(Math.max(40, maxMillisecondsPerCycle));
            this.scheduler = new SimulationScheduler(config);
            this.region = new RegionFactory().createNewRegion(
                    new RegionCoordinate("minecraft:overworld", 0, 0), 0);
            this.region.clearDirty();
            this.regionManager.regions.put(region.getCoordinate(), region);
            this.manager = new SimulationManager(
                    scheduler,
                    regionManager,
                    modules,
                    eventBus,
                    timeService,
                    new TestPersistenceService(),
                    profiler);
        }

        private void queue(Set<String> requestedModules) {
            scheduler.queueRegion(new RegionUpdateJob(
                    region.getCoordinate(),
                    5,
                    timeService.getSimulationTick(),
                    requestedModules,
                    UpdateReason.NORMAL_ROLLING_UPDATE));
        }

        private void queueAt(RegionCoordinate coordinate) {
            scheduler.queueRegion(new RegionUpdateJob(
                    coordinate,
                    5,
                    timeService.getSimulationTick(),
                    Set.of(),
                    UpdateReason.NORMAL_ROLLING_UPDATE));
        }
    }

    private static final class TestModule implements SimulationModule {
        private final String id;
        private final boolean changesRegion;
        private final long sleepMilliseconds;
        private int updateCount;

        private TestModule(String id, boolean changesRegion, long sleepMilliseconds) {
            this.id = id;
            this.changesRegion = changesRegion;
            this.sleepMilliseconds = sleepMilliseconds;
        }

        @Override
        public String getModuleId() {
            return id;
        }

        @Override
        public ModuleMetadata getMetadata() {
            return new ModuleMetadata(
                    id, id, "1.0.0", "", "1",
                    List.of(), List.of(), true, true, false);
        }

        @Override
        public ModuleUpdateResult updateRegion(RegionUpdateContext context) {
            updateCount++;
            if (sleepMilliseconds > 0) {
                try {
                    Thread.sleep(sleepMilliseconds);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new IllegalStateException(e);
                }
            }
            List<LivingWorldEvent> events = changesRegion
                    ? List.of(new BaseLivingWorldEvent("region_changed", 0, id))
                    : List.of();
            return new ModuleUpdateResult(changesRegion, events, List.of(), 0, List.of());
        }

        @Override public void initialize(ModuleContext context) {}
        @Override public void onServerStarted(ServerContext context) {}
        @Override public void createDefaultRegionData(Region region) {}
        @Override public void onLivingWorldEvent(LivingWorldEvent event) {}
        @Override public void saveModuleData(PersistenceWriter writer) {}
        @Override public void loadModuleData(PersistenceReader reader) {}
        @Override public void shutdown() {}
    }

    private static final class TestRegionManager implements RegionManager {
        private final Map<RegionCoordinate, Region> regions = new LinkedHashMap<>();
        private int markDirtyCount;

        @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) {
            markDirtyCount++;
            region.markDirty();
        }
    }

    private static final class TestPersistenceService implements PersistenceService {
        @Override public void markRegionDirty(Region region) { region.markDirty(); }
        @Override public void saveDirtyRegions() {}
        @Override public void saveRegion(Region region) {}
        @Override public Optional<Region> loadRegion(RegionCoordinate coordinate) {
            return Optional.empty();
        }
        @Override public void flushAll() {}
    }

    private static final class TestProfiler implements SimulationProfiler {
        private int startCount;
        private int endCount;

        @Override public void startCycle(long simulationTick) { startCount++; }
        @Override public void endCycle(long durationMs) { endCount++; }
    }

    private static final class TestTimeService implements TimeService {
        private final DefaultTimeService delegate = new DefaultTimeService();

        @Override public long getSimulationTick() { return delegate.getSimulationTick(); }
        @Override public LivingWorldCalendar getCalendar() { return delegate.getCalendar(); }
        @Override public void advanceSimulationTick() { delegate.advanceSimulationTick(); }
        @Override public void advanceSimulationTicks(long ticks) { delegate.advanceSimulationTicks(ticks); }
    }
}
