package com.livingworld.modules.resources;

import com.livingworld.modules.ModuleUpdateResult;
import com.livingworld.modules.RegionUpdateContext;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionMetrics;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("ResourceDepletionModule")
class ResourceDepletionModuleTest {

    private ResourceDepletionModule module;
    private RegionFactory factory;
    private Region region;
    private RegionMetrics metrics;

    @BeforeEach
    void setUp() {
        module = new ResourceDepletionModule();
        factory = new RegionFactory();
        region = factory.createNewRegion(new RegionCoordinate("overworld", 0, 0), 0L);
        metrics = region.getMetrics();
        module.createDefaultRegionData(region);
    }

    @Test
    @DisplayName("moduleId is 'resources'")
    void moduleId() {
        assertEquals("resources", module.getModuleId());
    }

    @Test
    @DisplayName("metadata is non-null with correct id")
    void metadata() {
        assertNotNull(module.getMetadata());
        assertEquals("resources", module.getMetadata().moduleId());
    }

    @Test
    @DisplayName("initialize throws on null context")
    void initializeNullThrows() {
        assertThrows(IllegalArgumentException.class, () -> module.initialize(null));
    }

    @Test
    @DisplayName("createDefaultRegionData populates module data")
    void createDefaultPopulates() {
        Region fresh = factory.createNewRegion(new RegionCoordinate("overworld", 1, 0), 0L);
        module.createDefaultRegionData(fresh);
        assertTrue(fresh.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).isPresent());
    }

    @Test
    @DisplayName("createDefaultRegionData is idempotent")
    void createDefaultIdempotent() {
        ResourceRegionData before = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        module.createDefaultRegionData(region);
        ResourceRegionData after = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        assertEquals(before.getMiningDepletion(), after.getMiningDepletion(), 1e-9);
    }

    @Test
    @DisplayName("updateRegion throws on null context")
    void updateNullThrows() {
        assertThrows(IllegalArgumentException.class, () -> module.updateRegion(null));
    }

    @Test
    @DisplayName("mining depletion regenerates very slowly (geological timescale)")
    void miningRegeneratesSlowly() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordMining(50.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);

        // After 1000 ticks, mining depletion should be very close to 50 (barely moved)
        for (int i = 0; i < 1000; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }
        ResourceRegionData after = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        assertTrue(after.getMiningDepletion() > 40.0,
                "Mining depletion should recover very slowly, was: " + after.getMiningDepletion());
    }

    @Test
    @DisplayName("logging depletion regenerates — drops from 80 over 100 ticks")
    void loggingRegenerates() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordLogging(80.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);
        metrics.setVegetationPressure(30.0); // below bonus threshold

        for (int i = 0; i < 100; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }
        ResourceRegionData after = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        assertTrue(after.getLoggingDepletion() < 80.0,
                "Logging depletion should decrease, was: " + after.getLoggingDepletion());
    }

    @Test
    @DisplayName("high vegetation pressure accelerates logging regeneration")
    void highVegetationAcceleratesLoggingRegen() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordLogging(80.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);
        metrics.setVegetationPressure(80.0); // above threshold

        for (int i = 0; i < 50; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }
        double withHighVeg = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow()
                .getLoggingDepletion();

        // Reset to same starting state with low vegetation
        Region region2 = factory.createNewRegion(new RegionCoordinate("overworld", 1, 0), 0L);
        module.createDefaultRegionData(region2);
        ResourceRegionData data2 = ResourceRegionData.defaults();
        data2.recordLogging(80.0);
        region2.getModuleData().put(ResourceDepletionModule.MODULE_ID, data2);
        region2.getMetrics().setVegetationPressure(0.0);

        for (int i = 0; i < 50; i++) {
            module.updateRegion(new RegionUpdateContext(region2));
        }
        double withLowVeg = region2.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow()
                .getLoggingDepletion();

        assertTrue(withHighVeg < withLowVeg,
                "Higher vegetation should accelerate logging regen");
    }

    @Test
    @DisplayName("farming depletion regenerates — drops from 60 over 100 ticks")
    void farmingRegenerates() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordFarming(60.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);
        metrics.setSoilQuality(30.0);

        for (int i = 0; i < 100; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }
        ResourceRegionData after = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        assertTrue(after.getFarmingDepletion() < 60.0,
                "Farming depletion should decrease, was: " + after.getFarmingDepletion());
    }

    @Test
    @DisplayName("high soil quality accelerates farming regeneration")
    void highSoilAcceleratesFarmingRegen() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordFarming(60.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);
        metrics.setSoilQuality(80.0);

        for (int i = 0; i < 50; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }
        double withHighSoil = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow()
                .getFarmingDepletion();

        Region region2 = factory.createNewRegion(new RegionCoordinate("overworld", 1, 0), 0L);
        module.createDefaultRegionData(region2);
        ResourceRegionData data2 = ResourceRegionData.defaults();
        data2.recordFarming(60.0);
        region2.getModuleData().put(ResourceDepletionModule.MODULE_ID, data2);
        region2.getMetrics().setSoilQuality(20.0);

        for (int i = 0; i < 50; i++) {
            module.updateRegion(new RegionUpdateContext(region2));
        }
        double withLowSoil = region2.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow()
                .getFarmingDepletion();

        assertTrue(withHighSoil < withLowSoil,
                "Higher soil quality should accelerate farming regen");
    }

    @Test
    @DisplayName("resourceDepletion metric is written after update")
    void metricWritten() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordMining(50.0);
        data.recordLogging(40.0);
        data.recordFarming(20.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);

        module.updateRegion(new RegionUpdateContext(region));

        assertTrue(region.getMetrics().getResourceDepletion() > 0.0,
                "Resource depletion metric should be set");
        assertTrue(region.getMetrics().getResourceDepletion() <= 100.0);
    }

    @Test
    @DisplayName("returns noChange for pristine (zero depletion) region")
    void noChangeWhenPristine() {
        // defaults are all-zero depletion; regen of zero is zero delta
        ModuleUpdateResult result = module.updateRegion(new RegionUpdateContext(region));
        assertFalse(result.changedRegion());
    }

    @Test
    @DisplayName("returns changed when depletion drops by more than threshold")
    void changedWhenDepletionDrops() {
        // High vegetation triggers bonus logging regen (0.255/tick × 30% weight = 0.077 delta > 0.01)
        metrics.setVegetationPressure(100.0);
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordLogging(100.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);

        ModuleUpdateResult result = module.updateRegion(new RegionUpdateContext(region));

        assertTrue(result.changedRegion());
    }

    @Test
    @DisplayName("all depletion values stay in [0, 100] under extreme conditions")
    void depletionsBounded() {
        ResourceRegionData data = ResourceRegionData.defaults();
        data.recordMining(100.0);
        data.recordLogging(100.0);
        data.recordFarming(100.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, data);

        for (int i = 0; i < 500; i++) {
            module.updateRegion(new RegionUpdateContext(region));
        }

        ResourceRegionData result = region.getModuleData()
                .get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElseThrow();
        assertTrue(result.getMiningDepletion()  >= 0.0 && result.getMiningDepletion()  <= 100.0);
        assertTrue(result.getLoggingDepletion() >= 0.0 && result.getLoggingDepletion() <= 100.0);
        assertTrue(result.getFarmingDepletion() >= 0.0 && result.getFarmingDepletion() <= 100.0);
    }
}
