package com.livingworld.modules.recovery;

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("RecoveryModule")
class RecoveryModuleTest {

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

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

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

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

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

    @Test
    @DisplayName("createDefaultRegionData populates module data at GRASSLAND stage")
    void createDefaultPopulates() {
        Region fresh = factory.createNewRegion(new RegionCoordinate("overworld", 1, 0), 0L);
        module.createDefaultRegionData(fresh);
        RecoveryRegionData data = fresh.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow();
        assertEquals(SuccessionStage.GRASSLAND, data.getSuccessionStage());
    }

    @Test
    @DisplayName("createDefaultRegionData is idempotent")
    void createDefaultIdempotent() {
        SuccessionStage before = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow()
                .getSuccessionStage();
        module.createDefaultRegionData(region);
        SuccessionStage after = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow()
                .getSuccessionStage();
        assertEquals(before, after);
    }

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

    @Test
    @DisplayName("good conditions advance recovery progress")
    void goodConditionsAdvanceProgress() {
        // GRASSLAND thresholds: minSoil=25, maxPollution=70, minVeg=20
        metrics.setSoilQuality(70.0);
        metrics.setPollutionScore(10.0);
        metrics.setVegetationPressure(50.0);
        metrics.setEcosystemHealth(50.0);

        module.updateRegion(new RegionUpdateContext(region));

        RecoveryRegionData data = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow();
        assertTrue(data.getRecoveryProgress() > 0.0,
                "Recovery progress should advance under good conditions");
    }

    @Test
    @DisplayName("bad conditions accumulate damage over several ticks")
    void badConditionsAccumulateDamage() {
        // Put region at SCRUBLAND so it CAN regress.
        // Force conditions way below SCRUBLAND minimums to trigger regression checks.
        RecoveryRegionData startData = new RecoveryRegionData(
                SuccessionStage.SCRUBLAND, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, startData);

        metrics.setSoilQuality(5.0);    // far below minSoil=40 (SCRUBLAND)
        metrics.setPollutionScore(95.0); // far above maxPollution=60 (SCRUBLAND)
        metrics.setVegetationPressure(2.0); // far below minVeg=35 (SCRUBLAND)

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

        RecoveryRegionData result = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow();
        // Either damage accumulated or regression already happened
        assertTrue(result.getDamageAccumulation() > 0.0
                        || result.getSuccessionStage().ordinal() < SuccessionStage.SCRUBLAND.ordinal(),
                "Bad conditions should accumulate damage or cause regression");
    }

    @Test
    @DisplayName("enough good ticks cause succession stage advancement")
    void enoughGoodTicksAdvanceStage() {
        // Start at BARREN — advancement thresholds: minSoil=10, maxPollution=80, minVeg=10
        RecoveryRegionData data = new RecoveryRegionData(SuccessionStage.BARREN, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, data);
        metrics.setSoilQuality(60.0);
        metrics.setPollutionScore(5.0);
        metrics.setVegetationPressure(30.0);
        metrics.setEcosystemHealth(80.0); // triggers health bonus

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

        RecoveryRegionData result = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow();
        assertTrue(result.getSuccessionStage().ordinal() > SuccessionStage.BARREN.ordinal(),
                "Stage should advance with enough good ticks");
    }

    @Test
    @DisplayName("recoveryPressure metric is written and stays in [0, 100]")
    void recoveryPressureMetricWritten() {
        metrics.setSoilQuality(70.0);
        metrics.setPollutionScore(0.0);
        metrics.setVegetationPressure(50.0);

        module.updateRegion(new RegionUpdateContext(region));

        double pressure = region.getMetrics().getRecoveryPressure();
        assertTrue(pressure >= 0.0 && pressure <= 100.0,
                "Recovery pressure must be in [0,100], was: " + pressure);
    }

    @Test
    @DisplayName("MATURE_FOREST stage has zero base recovery pressure")
    void matureForestZeroPressure() {
        RecoveryRegionData data = new RecoveryRegionData(
                SuccessionStage.MATURE_FOREST, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, data);
        metrics.setSoilQuality(70.0);
        metrics.setPollutionScore(0.0);
        metrics.setVegetationPressure(50.0);

        module.updateRegion(new RegionUpdateContext(region));

        // stagesFromPeak = 0 → base pressure = 0 + damage * 0.3 = 0
        assertEquals(0.0, region.getMetrics().getRecoveryPressure(), 1e-9);
    }

    @Test
    @DisplayName("returns changed when progress increases")
    void changedWhenProgressIncreases() {
        metrics.setSoilQuality(70.0);
        metrics.setPollutionScore(5.0);
        metrics.setVegetationPressure(50.0);

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

        assertTrue(result.changedRegion());
    }

    @Test
    @DisplayName("returns noChange when neither advancement nor regression conditions are met")
    void noChangeWhenNeutral() {
        // GRASSLAND advancement: minSoil=25, maxPollution=70, minVeg=20
        // GRASSLAND regression (50% severity): soil<12.5, pollution>84, veg<10
        // Put values between these two sets: soil=20 (below advance, above regress),
        //   pollution=10 (fine), veg=15 (below advance threshold 20, above regress threshold 10)
        metrics.setSoilQuality(20.0);
        metrics.setPollutionScore(10.0);
        metrics.setVegetationPressure(15.0);

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

        assertFalse(result.changedRegion());
    }

    @Test
    @DisplayName("recovery pressure stays in [0, 100] over 1000 ticks with bad conditions")
    void recoveryPressureBounded() {
        metrics.setSoilQuality(5.0);
        metrics.setPollutionScore(90.0);
        metrics.setVegetationPressure(5.0);

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

        double p = region.getMetrics().getRecoveryPressure();
        assertTrue(p >= 0.0 && p <= 100.0, "Recovery pressure out of range: " + p);
    }
}
