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.soil.SoilModule;
import com.livingworld.modules.soil.SoilRegionData;
import com.livingworld.modules.vegetation.VegetationModule;
import com.livingworld.modules.vegetation.VegetationRegionData;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionLifecycleState;
import com.livingworld.regions.RegionMetrics;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

/**
 * Integration tests that run all four ecosystem modules in pipeline order against
 * real Region objects and verify ecological cause-and-effect across multiple ticks.
 */
class EcosystemModuleIntegrationTest {

    private static final List<SimulationModule> MODULES = List.of(
            new PollutionModule(),
            new SoilModule(),
            new VegetationModule(),
            new EcosystemModule());

    private RegionFactory factory;

    @BeforeEach
    void setUp() {
        factory = new RegionFactory();
    }

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

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

    /** Runs all modules once against the region, in pipeline order. */
    private void tick(Region region) {
        RegionUpdateContext ctx = new RegionUpdateContext(region);
        for (SimulationModule module : MODULES) {
            module.createDefaultRegionData(region);
            module.updateRegion(ctx);
        }
    }

    /** Runs N ticks against the region. */
    private void tick(Region region, int ticks) {
        for (int i = 0; i < ticks; i++) {
            tick(region);
        }
    }

    // ------------------------------------------------------------------
    // clean region stays stable
    // ------------------------------------------------------------------

    @Test
    void cleanRegionKeepsHighEcosystemHealthOverManyTicks() {
        Region region = freshRegion();
        // Start with pristine defaults – no pollution.
        tick(region, 50);
        // Ecosystem health should remain high with no external stressors.
        assertTrue(region.getMetrics().getEcosystemHealth() >= 50.0,
                "Clean region should maintain reasonable ecosystem health");
        assertTrue(region.getMetrics().getPollutionScore() < 5.0,
                "No pollution was added; score should stay near zero");
    }

    // ------------------------------------------------------------------
    // heavy pollution degrades soil and vegetation
    // ------------------------------------------------------------------

    @Test
    void heavyPollutionDegradesSoilQualityOverTime() {
        Region region = freshRegion();
        // Prime the region with severe pollution directly in module data.
        PollutionRegionData pollData = new PollutionRegionData(80.0, 80.0, 50.0, 20.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);

        double initialSoilQuality = region.getMetrics().getSoilQuality();

        tick(region, 30);

        double laterSoilQuality = region.getMetrics().getSoilQuality();
        assertTrue(laterSoilQuality < initialSoilQuality,
                "Sustained heavy pollution should degrade soil quality. Before="
                        + initialSoilQuality + " After=" + laterSoilQuality);
    }

    @Test
    void heavyPollutionReducesVegetationPressure() {
        Region region = freshRegion();
        PollutionRegionData pollData = new PollutionRegionData(90.0, 90.0, 70.0, 20.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);

        double initialVeg = region.getMetrics().getVegetationPressure();
        tick(region, 30);
        double laterVeg = region.getMetrics().getVegetationPressure();

        assertTrue(laterVeg < initialVeg,
                "Heavy pollution should reduce vegetation pressure. Before="
                        + initialVeg + " After=" + laterVeg);
    }

    @Test
    void heavyPollutionIncreasesEcosystemStress() {
        Region region = freshRegion();
        PollutionRegionData pollData = new PollutionRegionData(100.0, 100.0, 100.0, 50.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);

        tick(region, 20);

        EcosystemRegionData ecoData = region.getModuleData()
                .get(EcosystemModule.MODULE_ID, EcosystemRegionData.class)
                .orElseThrow();
        assertTrue(ecoData.getStress() > 20.0,
                "Severe pollution should elevate ecosystem stress");
    }

    // ------------------------------------------------------------------
    // vegetation succession
    // ------------------------------------------------------------------

    @Test
    void bareGroundWithGoodSoilGrowsGrassOverTime() {
        Region region = freshRegion();
        // Strip all vegetation.
        VegetationRegionData barren = new VegetationRegionData(5.0, 0.0, 0.0, 0.0, 0.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, barren);

        tick(region, 50);

        VegetationRegionData later = region.getModuleData()
                .get(VegetationModule.MODULE_ID, VegetationRegionData.class)
                .orElseThrow();
        assertTrue(later.getGrassPressure() > 5.0,
                "Good soil should allow grass to grow back from a barren start");
    }

    @Test
    void loggingReducesTreeAndShrubPressure() {
        Region region = freshRegion();
        VegetationRegionData preLog = VegetationRegionData.defaults();
        double treesBefore = preLog.getTreePressure();
        preLog.reduceFromLogging(30.0);
        region.getModuleData().put(VegetationModule.MODULE_ID, preLog);

        assertTrue(preLog.getTreePressure() < treesBefore,
                "Logging should immediately reduce tree pressure");
        assertTrue(preLog.getDeadVegetation() > 5.0,
                "Logging should produce dead vegetation");
    }

    // ------------------------------------------------------------------
    // water quality
    // ------------------------------------------------------------------

    @Test
    void waterPollutionDegradedWaterQuality() {
        Region region = freshRegion();
        // Force high water pollution.
        PollutionRegionData pollData = new PollutionRegionData(0.0, 0.0, 80.0, 20.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);
        // Set metrics to a known starting waterQuality.
        region.getMetrics().setWaterQuality(80.0);

        tick(region, 10);

        assertTrue(region.getMetrics().getWaterQuality() < 80.0,
                "Water pollution should degrade water quality metric");
    }

    // ------------------------------------------------------------------
    // recovery after pollution clears
    // ------------------------------------------------------------------

    @Test
    void ecosystemHealthImprovesDramaticallyAfterPollutionIsRemoved() {
        Region region = freshRegion();
        // Heavily pollute for 20 ticks.
        PollutionRegionData pollData = new PollutionRegionData(90.0, 90.0, 60.0, 20.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);
        tick(region, 20);
        double healthMidPollution = region.getMetrics().getEcosystemHealth();

        // Remove pollution and run for another 50 ticks.
        region.getModuleData().put(PollutionModule.MODULE_ID, PollutionRegionData.defaults());
        tick(region, 50);
        double healthAfterRecovery = region.getMetrics().getEcosystemHealth();

        assertTrue(healthAfterRecovery > healthMidPollution,
                "Ecosystem should recover after pollution is removed. MidPollution="
                        + healthMidPollution + " AfterRecovery=" + healthAfterRecovery);
    }

    // ------------------------------------------------------------------
    // module order matters
    // ------------------------------------------------------------------

    @Test
    void pollutionModuleUpdatesMetricsReadBySoilModuleInSameTick() {
        Region region = freshRegion();
        // groundPollution=90 → pollutionScore=90*0.35=31.5 > POLLUTION_CONTAMINATION_THRESHOLD(30).
        PollutionRegionData pollData = new PollutionRegionData(0.0, 90.0, 0.0, 0.0);
        region.getModuleData().put(PollutionModule.MODULE_ID, pollData);

        // Run exactly one tick.
        tick(region);

        // Pollution module should have set a non-zero pollution score.
        assertTrue(region.getMetrics().getPollutionScore() > 0.0,
                "PollutionModule should have written a non-zero pollutionScore to metrics");
        // Soil module (running after) should have begun accumulating contamination.
        SoilRegionData soilData = region.getModuleData()
                .get(SoilModule.MODULE_ID, SoilRegionData.class)
                .orElseThrow();
        assertTrue(soilData.getContamination() > 0.0,
                "SoilModule should have accumulated contamination from this tick's pollutionScore");
    }

    // ------------------------------------------------------------------
    // lifecycle: createDefaultRegionData is idempotent
    // ------------------------------------------------------------------

    @Test
    void createDefaultRegionDataIsIdempotent() {
        Region region = freshRegion();
        PollutionModule module = new PollutionModule();
        module.createDefaultRegionData(region);
        module.createDefaultRegionData(region); // second call must not overwrite

        // Data exists and is valid defaults.
        PollutionRegionData data = region.getModuleData()
                .get(PollutionModule.MODULE_ID, PollutionRegionData.class)
                .orElseThrow();
        assertEquals(0.0, data.getAirPollution(), 1e-9);
    }

    // ------------------------------------------------------------------
    // ModuleUpdateResult signals
    // ------------------------------------------------------------------

    @Test
    void pollutionModuleReturnsNoChangeWhenPollutionIsZero() {
        Region region = freshRegion();
        // Ensure pollution module data is initialised to all-zero.
        region.getModuleData().put(PollutionModule.MODULE_ID, PollutionRegionData.defaults());
        // Zero water quality impact needs zero starting waterQuality impact too.
        region.getMetrics().setWaterQuality(60.0);

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

        // Zero pollution decays to zero; pollutionScore stays 0; no meaningful change.
        assertFalse(result.changedRegion(),
                "PollutionModule with zero pollution should return noChange");
    }

    @Test
    void pollutionModuleReturnsChangedWhenPollutionIsPresent() {
        Region region = freshRegion();
        region.getModuleData().put(PollutionModule.MODULE_ID,
                new PollutionRegionData(50.0, 50.0, 50.0, 0.0));

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

        assertTrue(result.changedRegion(),
                "PollutionModule with non-zero pollution should return changed");
    }

    // ------------------------------------------------------------------
    // long-run stability
    // ------------------------------------------------------------------

    @Test
    void allMetricsRemainInValidRangeAfter1000Ticks() {
        Region region = freshRegion();
        // Add moderate pollution so the simulation isn't completely quiescent.
        region.getModuleData().put(PollutionModule.MODULE_ID,
                new PollutionRegionData(30.0, 20.0, 10.0, 25.0));

        tick(region, 1000);

        RegionMetrics m = region.getMetrics();
        assertInRange("ecosystemHealth",   m.getEcosystemHealth());
        assertInRange("pollutionScore",    m.getPollutionScore());
        assertInRange("soilQuality",       m.getSoilQuality());
        assertInRange("waterQuality",      m.getWaterQuality());
        assertInRange("vegetationPressure",m.getVegetationPressure());
        assertInRange("recoveryPressure",  m.getRecoveryPressure());
    }

    private static void assertInRange(String name, double value) {
        assertTrue(value >= 0.0 && value <= 100.0,
                name + " must be in [0, 100] but was " + value);
    }
}
