package com.livingworld.regions;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

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

import org.junit.jupiter.api.Test;

import com.livingworld.config.SimulationConfig;
import com.livingworld.core.services.PersistenceService;
import com.livingworld.regions.cache.RegionCache;
import com.livingworld.regions.query.RegionQueryEngine;

class RegionManagerTest {

    @Test
    void checksCacheBeforeStorageAndPreventsDuplicateInstances() {
        Fixture fixture = new Fixture();
        RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 0, 0);

        Region first = fixture.manager.getOrCreateRegion(coordinate);
        Region second = fixture.manager.getOrCreateRegion(coordinate);

        assertSame(first, second);
        assertEquals(1, fixture.persistence.loadCount);
    }

    @Test
    void loadsFromStorageBeforeCreatingRegion() {
        Fixture fixture = new Fixture();
        RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 2, 3);
        Region stored = fixture.factory.createNewRegion(coordinate, 12);
        stored.clearDirty();
        fixture.persistence.stored.put(coordinate, stored);

        Region loaded = fixture.manager.getOrCreateRegion(coordinate);

        assertSame(stored, loaded);
        assertEquals(1, fixture.cache.size());
    }

    @Test
    void createsMissingRegionAndConvertsBlockCoordinates() {
        Fixture fixture = new Fixture();

        Region created = fixture.manager.getOrCreateRegionAtBlock(
                "minecraft:overworld", -129, 128);

        assertEquals(new RegionCoordinate("minecraft:overworld", -2, 1), created.getCoordinate());
        assertTrue(created.isDirty());
    }

    @Test
    void unloadSavesDirtyRegionAndRemovesItFromCache() {
        Fixture fixture = new Fixture();
        Region region = fixture.manager.getOrCreateRegion(
                new RegionCoordinate("minecraft:overworld", 0, 0));

        fixture.manager.unloadRegion(region.getCoordinate());

        assertEquals(1, fixture.persistence.saveCount);
        assertTrue(fixture.manager.findRegion(region.getCoordinate()).isEmpty());
        assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());
    }

    @Test
    void rejectsDifferentInstanceForCachedCoordinate() {
        Fixture fixture = new Fixture();
        RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 0, 0);
        Region managed = fixture.manager.getOrCreateRegion(coordinate);
        Region duplicate = fixture.factory.createNewRegion(coordinate, 0);

        assertNotSame(managed, duplicate);
        assertThrows(IllegalArgumentException.class, () -> fixture.manager.markDirty(duplicate));
    }

    private static final class Fixture {
        private final RegionFactory factory = new RegionFactory();
        private final TestPersistenceService persistence = new TestPersistenceService();
        private final RegionCache cache = new RegionCache();
        private final RegionManager manager = new RegionManager(
                factory,
                new RegionStorage(persistence),
                cache,
                new RegionQueryEngine(cache),
                new RegionLifecycleController(),
                new SimulationConfig());
    }

    private static final class TestPersistenceService implements PersistenceService {
        private final Map<RegionCoordinate, Region> stored = new LinkedHashMap<>();
        private int loadCount;
        private int saveCount;

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

        @Override
        public void saveDirtyRegions() {
        }

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

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

        @Override
        public void flushAll() {
        }
    }
}
