package com.livingworld.modules.water;

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("WaterModule")
class WaterModuleTest {

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

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

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

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

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

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

    @Test
    @DisplayName("createDefaultRegionData is idempotent")
    void createDefaultRegionDataIdempotent() {
        WaterRegionData before = region.getModuleData()
                .get(WaterModule.MODULE_ID, WaterRegionData.class).orElseThrow();
        module.createDefaultRegionData(region);
        WaterRegionData after = region.getModuleData()
                .get(WaterModule.MODULE_ID, WaterRegionData.class).orElseThrow();
        assertEquals(before.getPurificationCapacity(), after.getPurificationCapacity(), 1e-9);
    }

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

    @Test
    @DisplayName("high vegetation pressure sets purification capacity")
    void highVegetationIncreasesPurification() {
        metrics.setVegetationPressure(80.0);
        metrics.setWaterQuality(50.0);
        metrics.setSoilQuality(70.0); // above leach threshold

        module.updateRegion(new RegionUpdateContext(region));

        WaterRegionData data = region.getModuleData()
                .get(WaterModule.MODULE_ID, WaterRegionData.class).orElseThrow();
        // purification = 80 * 0.50 = 40
        assertEquals(40.0, data.getPurificationCapacity(), 0.01);
    }

    @Test
    @DisplayName("vegetation purification raises water quality")
    void vegetationPurificationRaisesWaterQuality() {
        metrics.setVegetationPressure(80.0);
        metrics.setWaterQuality(50.0);
        metrics.setSoilQuality(70.0); // above leach threshold — no leaching

        module.updateRegion(new RegionUpdateContext(region));

        // Recovery = 40 * 0.01 = 0.4 gain
        assertTrue(region.getMetrics().getWaterQuality() > 50.0,
                "Water quality should rise with vegetation purification");
    }

    @Test
    @DisplayName("low soil quality leaches contamination into water")
    void lowSoilQualityLeachesWater() {
        metrics.setVegetationPressure(0.0); // no purification
        metrics.setWaterQuality(80.0);
        metrics.setSoilQuality(10.0); // well below threshold of 40

        module.updateRegion(new RegionUpdateContext(region));

        // leach = (40 - 10) * 0.005 = 0.15 reduction
        assertTrue(region.getMetrics().getWaterQuality() < 80.0,
                "Water quality should fall when soil is contaminated");
    }

    @Test
    @DisplayName("soil above threshold causes no leaching")
    void soilAboveThresholdNoLeach() {
        metrics.setVegetationPressure(0.0);
        metrics.setWaterQuality(60.0);
        metrics.setSoilQuality(50.0); // above threshold

        double prevWQ = metrics.getWaterQuality();
        module.updateRegion(new RegionUpdateContext(region));

        // No leach; no purification either (veg=0). Water quality unchanged.
        assertEquals(prevWQ, region.getMetrics().getWaterQuality(), 1e-9);
    }

    @Test
    @DisplayName("returns noChange when water quality is stable")
    void noChangeWhenStable() {
        metrics.setVegetationPressure(0.0);
        metrics.setSoilQuality(50.0);
        metrics.setWaterQuality(60.0);

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

        assertFalse(result.changedRegion());
    }

    @Test
    @DisplayName("returns changed when water quality shifts")
    void changedWhenWaterQualityShifts() {
        metrics.setVegetationPressure(80.0);
        metrics.setSoilQuality(70.0);
        metrics.setWaterQuality(50.0);

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

        assertTrue(result.changedRegion());
    }

    @Test
    @DisplayName("water quality stays in [0, 100] under extreme conditions")
    void waterQualityBounded() {
        metrics.setVegetationPressure(100.0);
        metrics.setSoilQuality(0.0);
        metrics.setWaterQuality(0.0);

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

        double wq = region.getMetrics().getWaterQuality();
        assertTrue(wq >= 0.0 && wq <= 100.0,
                "Water quality must stay in [0, 100], was: " + wq);
    }
}
