package com.livingworld.core.services;

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.modules.pollution.PollutionRegionData;
import com.livingworld.modules.recovery.RecoveryRegionData;
import com.livingworld.modules.recovery.SuccessionStage;
import com.livingworld.modules.soil.SoilRegionData;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionModuleData;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class FileRegionPersistenceServiceTest {

    private static final Clock CLOCK =
            Clock.fixed(Instant.ofEpochMilli(1_000L), ZoneOffset.UTC);

    @TempDir
    Path temporaryDirectory;

    @Test
    void roundTripsCoreRegionState() {
        FileRegionPersistenceService service = service();
        Region original = new RegionFactory().createNewRegion(
                new RegionCoordinate("minecraft:overworld", -2, 3),
                12);
        original.updateLastSimulatedTick(42);
        original.getFlags().setHasHighPollution(true);
        original.getFlags().setForceLoadedBySimulation(true);
        original.getMetrics().setEcosystemHealth(37.5);
        original.getMetrics().setWaterQuality(22.25);

        service.saveRegion(original);
        Optional<Region> loaded = service.loadRegion(original.getCoordinate());

        assertTrue(loaded.isPresent());
        Region restored = loaded.orElseThrow();
        assertEquals(original.getId(), restored.getId());
        assertEquals(original.getCoordinate(), restored.getCoordinate());
        assertEquals(original.getLifecycleState(), restored.getLifecycleState());
        assertEquals(12, restored.getCreatedAtSimulationTick());
        assertEquals(42, restored.getLastUpdatedSimulationTick());
        assertTrue(restored.getFlags().isHasHighPollution());
        assertTrue(restored.getFlags().isForceLoadedBySimulation());
        assertEquals(37.5, restored.getMetrics().getEcosystemHealth());
        assertEquals(22.25, restored.getMetrics().getWaterQuality());
        assertFalse(restored.isDirty());
    }

    @Test
    void dirtyQueueIsPersistedAndCleared() {
        FileRegionPersistenceService service = service();
        Region region = new RegionFactory().createNewRegion(
                new RegionCoordinate("minecraft:overworld", 0, 0),
                0);

        service.markRegionDirty(region);
        assertEquals(1, service.getDirtyRegionCount());

        service.saveDirtyRegions();

        assertEquals(0, service.getDirtyRegionCount());
        assertFalse(region.isDirty());
        assertTrue(service.loadRegion(region.getCoordinate()).isPresent());
    }

    @Test
    void corruptRegionIsQuarantinedInsteadOfCrashingLoad() throws Exception {
        FileRegionPersistenceService service = service();
        Region region = new RegionFactory().createNewRegion(
                new RegionCoordinate("minecraft:overworld", 1, 0),
                0);
        service.saveRegion(region);
        Path regionFile;
        try (var files = Files.list(temporaryDirectory.resolve("regions"))) {
            regionFile = files.findFirst().orElseThrow();
        }
        Files.writeString(regionFile, "not=a-valid-region");

        assertTrue(service.loadRegion(region.getCoordinate()).isEmpty());
        try (var files = Files.list(temporaryDirectory.resolve("regions"))) {
            assertTrue(files.anyMatch(path -> path.getFileName().toString().contains(".corrupt-")));
        }
    }

    @Test
    void metadataSurvivesServiceRestart() {
        service().flushAll();

        FileRegionPersistenceService restarted = service();

        assertTrue(Files.exists(temporaryDirectory.resolve("metadata.properties")));
        assertEquals(0, restarted.getDirtyRegionCount());
    }

    @Test
    void roundTripsModuleData() {
        FileRegionPersistenceService service = service();
        service.registerModuleCodec(
                "pollution",
                (data, w) -> {
                    PollutionRegionData d = data.get("pollution", PollutionRegionData.class)
                            .orElseGet(PollutionRegionData::defaults);
                    w.writeDouble("airPollution",    d.getAirPollution());
                    w.writeDouble("groundPollution", d.getGroundPollution());
                    w.writeDouble("waterPollution",  d.getWaterPollution());
                    w.writeDouble("decayResistance", d.getDecayResistance());
                },
                (r, data) -> data.put("pollution", new PollutionRegionData(
                        r.readDouble("airPollution",    0.0),
                        r.readDouble("groundPollution", 0.0),
                        r.readDouble("waterPollution",  0.0),
                        r.readDouble("decayResistance", 20.0))));
        service.registerModuleCodec(
                "soil",
                (data, w) -> {
                    SoilRegionData d = data.get("soil", SoilRegionData.class)
                            .orElseGet(SoilRegionData::defaults);
                    w.writeDouble("fertility",     d.getFertility());
                    w.writeDouble("contamination", d.getContamination());
                },
                (r, data) -> data.put("soil", new SoilRegionData(
                        r.readDouble("fertility",     60.0),
                        r.readDouble("moisture",      50.0),
                        r.readDouble("contamination",  0.0),
                        r.readDouble("compaction",    10.0),
                        r.readDouble("erosion",        0.0))));
        service.registerModuleCodec(
                "recovery",
                (data, w) -> {
                    RecoveryRegionData d = data.get("recovery", RecoveryRegionData.class)
                            .orElseGet(RecoveryRegionData::defaults);
                    w.writeString("successionStage",   d.getSuccessionStage().name());
                    w.writeDouble("damageAccumulation", d.getDamageAccumulation());
                },
                (r, data) -> data.put("recovery", new RecoveryRegionData(
                        SuccessionStage.valueOf(r.readString("successionStage", SuccessionStage.GRASSLAND.name())),
                        r.readDouble("recoveryProgress",   0.0),
                        r.readDouble("damageAccumulation", 0.0))));

        Region original = new RegionFactory().createNewRegion(
                new RegionCoordinate("minecraft:overworld", 0, 0), 0);
        RegionModuleData moduleData = original.getModuleData();
        moduleData.put("pollution", new PollutionRegionData(42.0, 15.0, 8.0, 25.0));
        moduleData.put("soil", new SoilRegionData(75.0, 60.0, 5.0, 20.0, 3.0));
        moduleData.put("recovery", new RecoveryRegionData(SuccessionStage.YOUNG_WOODLAND, 66.0, 12.5));

        service.saveRegion(original);
        Region restored = service.loadRegion(original.getCoordinate()).orElseThrow();
        RegionModuleData restoredData = restored.getModuleData();

        PollutionRegionData pollution = restoredData.get("pollution", PollutionRegionData.class).orElseThrow();
        assertEquals(42.0, pollution.getAirPollution());
        assertEquals(15.0, pollution.getGroundPollution());
        assertEquals(8.0,  pollution.getWaterPollution());
        assertEquals(25.0, pollution.getDecayResistance());

        SoilRegionData soil = restoredData.get("soil", SoilRegionData.class).orElseThrow();
        assertEquals(75.0, soil.getFertility());
        assertEquals(5.0,  soil.getContamination());

        RecoveryRegionData recovery = restoredData.get("recovery", RecoveryRegionData.class).orElseThrow();
        assertEquals(SuccessionStage.YOUNG_WOODLAND, recovery.getSuccessionStage());
        assertEquals(12.5, recovery.getDamageAccumulation());
    }

    private FileRegionPersistenceService service() {
        return new FileRegionPersistenceService(
                temporaryDirectory,
                "0.1.0",
                CLOCK);
    }
}
