package com.livingworld.modules;

import com.livingworld.modules.ecosystem.EcosystemModule;
import com.livingworld.modules.ecosystem.EcosystemRegionData;
import com.livingworld.modules.pollution.PollutionModule;
import com.livingworld.modules.pollution.PollutionRegionData;
import com.livingworld.modules.recovery.RecoveryModule;
import com.livingworld.modules.recovery.RecoveryRegionData;
import com.livingworld.modules.recovery.SuccessionStage;
import com.livingworld.modules.resources.ResourceDepletionModule;
import com.livingworld.modules.resources.ResourceRegionData;
import com.livingworld.modules.soil.SoilModule;
import com.livingworld.modules.soil.SoilRegionData;
import com.livingworld.modules.vegetation.VegetationModule;
import com.livingworld.modules.vegetation.VegetationRegionData;
import com.livingworld.modules.water.WaterModule;
import com.livingworld.modules.worldeffects.WorldEffectRequest;
import com.livingworld.modules.worldeffects.WorldEffectType;
import com.livingworld.modules.worldeffects.WorldEffectsModule;
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 java.util.ArrayList;
import java.util.List;

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

/**
 * Full Volume 2 integration test: runs all eight ecosystem modules in pipeline
 * order (Pollution → Soil → Water → Vegetation → ResourceDepletion → Recovery
 * → Ecosystem → WorldEffects) and verifies ecological cause-and-effect across
 * multiple simulation ticks.
 */
@DisplayName("Volume 2 Full Pipeline Integration")
class Volume2IntegrationTest {

    private RegionFactory factory;
    private WorldEffectsModule worldEffects;
    private List<WorldEffectRequest> effectsCapture;

    private List<SimulationModule> pipeline;

    @BeforeEach
    void setUp() {
        factory = new RegionFactory();
        worldEffects = new WorldEffectsModule();
        effectsCapture = new ArrayList<>();
        worldEffects.registerConsumer(effectsCapture::add);

        pipeline = List.of(
                new PollutionModule(),
                new SoilModule(),
                new WaterModule(),
                new VegetationModule(),
                new ResourceDepletionModule(),
                new RecoveryModule(),
                new EcosystemModule(),
                worldEffects);
    }

    // ------------------------------------------------------------------
    // helpers
    // ------------------------------------------------------------------

    private Region freshRegion() {
        return factory.createNewRegion(new RegionCoordinate("overworld", 0, 0), 0L);
    }

    private void tick(Region region) {
        effectsCapture.clear();
        RegionUpdateContext ctx = new RegionUpdateContext(region);
        for (SimulationModule module : pipeline) {
            module.createDefaultRegionData(region);
            module.updateRegion(ctx);
        }
    }

    private void tick(Region region, int ticks) {
        for (int i = 0; i < ticks; i++) {
            tick(region);
        }
    }

    // ------------------------------------------------------------------
    // Structural tests
    // ------------------------------------------------------------------

    @Test
    @DisplayName("all modules initialize without error")
    void allModulesInitialize() {
        Region region = freshRegion();
        for (SimulationModule module : pipeline) {
            assertDoesNotThrow(() -> module.createDefaultRegionData(region));
        }
    }

    @Test
    @DisplayName("pipeline runs 100 ticks on a clean region without exception")
    void cleanRegionStableOver100Ticks() {
        Region region = freshRegion();
        assertDoesNotThrow(() -> tick(region, 100));
    }

    @Test
    @DisplayName("all metrics stay in [0, 100] over 1000 ticks — pristine region")
    void metricsInRangePristine() {
        Region region = freshRegion();
        tick(region, 1000);
        assertMetricsInRange(region.getMetrics());
    }

    @Test
    @DisplayName("all metrics stay in [0, 100] over 500 ticks — heavily polluted region")
    void metricsInRangeHeavilyPolluted() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);

        tick(region, 500);
        assertMetricsInRange(region.getMetrics());
    }

    // ------------------------------------------------------------------
    // Causal chain tests
    // ------------------------------------------------------------------

    @Test
    @DisplayName("heavy pollution degrades soil quality over 30 ticks")
    void heavyPollutionDegradesSoil() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        double initialSoil = region.getMetrics().getSoilQuality();

        tick(region, 30);

        assertTrue(region.getMetrics().getSoilQuality() < initialSoil,
                "Soil quality should degrade under heavy pollution");
    }

    @Test
    @DisplayName("heavy pollution reduces vegetation pressure over 50 ticks")
    void heavyPollutionReducesVegetation() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        double initialVeg = region.getMetrics().getVegetationPressure();

        tick(region, 50);

        assertTrue(region.getMetrics().getVegetationPressure() < initialVeg,
                "Vegetation pressure should fall under heavy pollution");
    }

    @Test
    @DisplayName("heavy pollution lowers water quality over 20 ticks")
    void heavyPollutionLowersWater() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        double initialWater = region.getMetrics().getWaterQuality();

        tick(region, 20);

        assertTrue(region.getMetrics().getWaterQuality() < initialWater,
                "Water quality should fall under heavy pollution");
    }

    @Test
    @DisplayName("heavy pollution increases ecosystem stress over 30 ticks")
    void heavyPollutionIncreasesStress() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);

        tick(region, 30);

        EcosystemRegionData eco = region.getModuleData()
                .get(EcosystemModule.MODULE_ID, EcosystemRegionData.class).orElseThrow();
        assertTrue(eco.getStress() > 20.0,
                "Ecosystem stress should be elevated under heavy pollution");
    }

    @Test
    @DisplayName("bare ground (low soil + zero pollution) grows grass over 50 ticks")
    void bareGroundGrowsGrass() {
        Region region = freshRegion();
        // Start with bare-ish ground: good soil, no pollution, low veg
        SoilRegionData soil = new SoilRegionData(70.0, 50.0, 0.0, 10.0, 0.0);
        region.getModuleData().put(SoilModule.MODULE_ID, soil);
        VegetationRegionData veg = new VegetationRegionData(5.0, 5.0, 0.0, 0.0, 0.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, veg);

        tick(region, 50);

        VegetationRegionData after = region.getModuleData()
                .get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElseThrow();
        assertTrue(after.getGrassPressure() > 5.0,
                "Grass should grow on bare ground with good soil, was: " + after.getGrassPressure());
    }

    @Test
    @DisplayName("logging depletion reduces tree and shrub pressure over 20 ticks")
    void loggingReducesTreeShrubPressure() {
        Region region = freshRegion();
        ResourceRegionData resources = ResourceRegionData.defaults();
        resources.recordLogging(80.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, resources);
        VegetationRegionData vegData = new VegetationRegionData(50.0, 30.0, 50.0, 60.0, 5.0);
        vegData.reduceFromLogging(80.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, vegData);

        tick(region, 20);

        VegetationRegionData after = region.getModuleData()
                .get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElseThrow();
        assertTrue(after.getTreePressure() < 60.0,
                "Tree pressure should be reduced after logging");
    }

    @Test
    @DisplayName("high vegetation purifies water quality over 30 ticks (pollution-free region)")
    void vegetationPurifiesWater() {
        Region region = freshRegion();
        // Start with high veg, good soil, slightly low water quality
        VegetationRegionData vegData = new VegetationRegionData(80.0, 50.0, 60.0, 40.0, 5.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, vegData);
        region.getMetrics().setWaterQuality(40.0);

        tick(region, 30);

        assertTrue(region.getMetrics().getWaterQuality() > 40.0,
                "Water quality should improve when vegetation is healthy");
    }

    @Test
    @DisplayName("ecosystem health improves after pollution is removed (over 50 ticks)")
    void healthImprovesAfterPollutionRemoved() {
        Region region = freshRegion();
        // First: run 30 ticks with heavy pollution
        PollutionRegionData heavyPollution = new PollutionRegionData(80.0, 80.0, 80.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, heavyPollution);
        tick(region, 30);
        double healthAfterPollution = region.getMetrics().getEcosystemHealth();

        // Now remove pollution and run 50 more ticks
        region.getModuleData().put(PollutionModule.MODULE_ID, PollutionRegionData.defaults());
        tick(region, 50);

        assertTrue(region.getMetrics().getEcosystemHealth() > healthAfterPollution,
                "Ecosystem health should improve once pollution is removed");
    }

    @Test
    @DisplayName("succession advances from BARREN toward GRASSLAND under good conditions")
    void successionAdvancesUnderGoodConditions() {
        Region region = freshRegion();
        // Set good conditions exceeding all early stage thresholds
        region.getMetrics().setSoilQuality(70.0);
        region.getMetrics().setPollutionScore(0.0);
        region.getMetrics().setVegetationPressure(40.0);
        RecoveryRegionData recovery = new RecoveryRegionData(SuccessionStage.BARREN, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);

        tick(region, 300);

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

    @Test
    @DisplayName("succession regresses under severe pollution over 30 ticks")
    void successionRegressesUnderSeverePollution() {
        Region region = freshRegion();
        // Start at SCRUBLAND
        RecoveryRegionData recovery = new RecoveryRegionData(SuccessionStage.SCRUBLAND, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
        // Force very bad conditions
        PollutionRegionData pollution = new PollutionRegionData(95.0, 95.0, 95.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        SoilRegionData soil = new SoilRegionData(5.0, 0.0, 50.0, 30.0, 20.0);
        region.getModuleData().put(SoilModule.MODULE_ID, soil);

        tick(region, 30);

        RecoveryRegionData result = region.getModuleData()
                .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElseThrow();
        // Either damage accumulated or regression happened
        assertTrue(result.getDamageAccumulation() > 0.0
                        || result.getSuccessionStage().ordinal() < SuccessionStage.SCRUBLAND.ordinal(),
                "Succession should regress or take damage under severe pollution");
    }

    // ------------------------------------------------------------------
    // World effects tests
    // ------------------------------------------------------------------

    @Test
    @DisplayName("GRASS_DEGRADES_TO_DIRT emitted when pollution is high and soil is poor")
    void worldEffectGrassDegrades() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        SoilRegionData soil = new SoilRegionData(10.0, 0.0, 50.0, 30.0, 20.0);
        region.getModuleData().put(SoilModule.MODULE_ID, soil);

        // Prime the pipeline so metrics are set by pollution/soil before worldeffects runs
        tick(region, 5);
        effectsCapture.clear();
        tick(region);

        boolean found = effectsCapture.stream()
                .anyMatch(r -> r.type() == WorldEffectType.GRASS_DEGRADES_TO_DIRT);
        assertTrue(found,
                "GRASS_DEGRADES_TO_DIRT should be emitted when soil is poor and pollution is high");
    }

    @Test
    @DisplayName("VEGETATION_SPREADS emitted when vegetation and soil are healthy")
    void worldEffectVegetationSpreads() {
        Region region = freshRegion();
        // Force high vegetation and good soil
        VegetationRegionData veg = new VegetationRegionData(90.0, 50.0, 70.0, 60.0, 0.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, veg);
        SoilRegionData soil = new SoilRegionData(80.0, 60.0, 0.0, 5.0, 0.0);
        region.getModuleData().put(SoilModule.MODULE_ID, soil);

        tick(region, 3);
        effectsCapture.clear();
        tick(region);

        boolean found = effectsCapture.stream()
                .anyMatch(r -> r.type() == WorldEffectType.VEGETATION_SPREADS);
        assertTrue(found,
                "VEGETATION_SPREADS should be emitted with high vegetation and good soil");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_SLOWED emitted when logging depletion is high")
    void worldEffectSaplingSlowed() {
        Region region = freshRegion();
        ResourceRegionData resources = ResourceRegionData.defaults();
        resources.recordLogging(80.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, resources);

        effectsCapture.clear();
        tick(region);

        boolean found = effectsCapture.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_SLOWED);
        assertTrue(found, "SAPLING_GROWTH_SLOWED should be emitted with heavy logging depletion");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_BOOSTED emitted when region reaches YOUNG_WOODLAND")
    void worldEffectSaplingBoosted() {
        Region region = freshRegion();
        RecoveryRegionData recovery = new RecoveryRegionData(
                SuccessionStage.YOUNG_WOODLAND, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);

        effectsCapture.clear();
        tick(region);

        boolean found = effectsCapture.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_BOOSTED);
        assertTrue(found, "SAPLING_GROWTH_BOOSTED should be emitted at YOUNG_WOODLAND stage");
    }

    @Test
    @DisplayName("emitted WorldEffectRequests all have intensity in [0, 1]")
    void worldEffectIntensitiesInRange() {
        Region region = freshRegion();
        PollutionRegionData pollution = new PollutionRegionData(90.0, 90.0, 90.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
        SoilRegionData soil = new SoilRegionData(10.0, 0.0, 50.0, 30.0, 20.0);
        region.getModuleData().put(SoilModule.MODULE_ID, soil);
        tick(region, 5);
        effectsCapture.clear();
        tick(region);

        for (WorldEffectRequest req : effectsCapture) {
            assertTrue(req.intensity() >= 0.0 && req.intensity() <= 1.0,
                    "Intensity out of range: " + req.intensity() + " for " + req.type());
        }
    }

    // ------------------------------------------------------------------
    // Pipeline ordering test
    // ------------------------------------------------------------------

    @Test
    @DisplayName("pollution metric written in same tick is read by soil and water modules")
    void pipelineOrderVerified() {
        Region region = freshRegion();
        // High ground pollution → pollutionScore computed by PollutionModule
        // → SoilModule reads pollutionScore → contamination raised
        // → WaterModule reads soilQuality → leach applied
        PollutionRegionData pollution = new PollutionRegionData(0.0, 90.0, 0.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollution);

        // Single tick — everything computed in sequence
        tick(region);

        // pollutionScore = ground * 0.35 = 31.5 > threshold → soil contamination increases
        SoilRegionData soilAfter = region.getModuleData()
                .get(SoilModule.MODULE_ID, SoilRegionData.class).orElseThrow();
        assertTrue(soilAfter.getContamination() > 0.0,
                "SoilModule should have read the pollution metric set by PollutionModule in the same tick");
    }

    // ------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------

    private void assertMetricsInRange(RegionMetrics m) {
        assertTrue(m.getEcosystemHealth()    >= 0 && m.getEcosystemHealth()    <= 100, "ecosystemHealth out of range: " + m.getEcosystemHealth());
        assertTrue(m.getPollutionScore()     >= 0 && m.getPollutionScore()     <= 100, "pollutionScore out of range: " + m.getPollutionScore());
        assertTrue(m.getSoilQuality()        >= 0 && m.getSoilQuality()        <= 100, "soilQuality out of range: " + m.getSoilQuality());
        assertTrue(m.getWaterQuality()       >= 0 && m.getWaterQuality()       <= 100, "waterQuality out of range: " + m.getWaterQuality());
        assertTrue(m.getVegetationPressure() >= 0 && m.getVegetationPressure() <= 100, "vegetationPressure out of range: " + m.getVegetationPressure());
        assertTrue(m.getResourceDepletion()  >= 0 && m.getResourceDepletion()  <= 100, "resourceDepletion out of range: " + m.getResourceDepletion());
        assertTrue(m.getRecoveryPressure()   >= 0 && m.getRecoveryPressure()   <= 100, "recoveryPressure out of range: " + m.getRecoveryPressure());
    }
}
