package com.livingworld.modules.worldeffects;

import com.livingworld.modules.ModuleUpdateResult;
import com.livingworld.modules.RegionUpdateContext;
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.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.*;

@DisplayName("WorldEffectsModule")
class WorldEffectsModuleTest {

    private WorldEffectsModule module;
    private RegionFactory factory;
    private Region region;
    private RegionMetrics metrics;
    private List<WorldEffectRequest> captured;

    @BeforeEach
    void setUp() {
        module = new WorldEffectsModule();
        captured = new ArrayList<>();
        module.registerConsumer(captured::add);

        factory = new RegionFactory();
        region = factory.createNewRegion(new RegionCoordinate("overworld", 0, 0), 0L);
        metrics = region.getMetrics();
        module.createDefaultRegionData(region);
    }

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

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

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

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

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

    @Test
    @DisplayName("no consumer registered → noChange returned")
    void noConsumerNoChange() {
        WorldEffectsModule fresh = new WorldEffectsModule();
        metrics.setPollutionScore(80.0);
        ModuleUpdateResult result = fresh.updateRegion(new RegionUpdateContext(region));
        assertFalse(result.changedRegion());
    }

    @Test
    @DisplayName("GRASS_DEGRADES_TO_DIRT emitted when pollution > 60 and soil < 30")
    void grassDegrades() {
        metrics.setPollutionScore(80.0);
        metrics.setSoilQuality(15.0);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.GRASS_DEGRADES_TO_DIRT);
        assertTrue(found, "GRASS_DEGRADES_TO_DIRT should have been emitted");
    }

    @Test
    @DisplayName("GRASS_DEGRADES_TO_DIRT not emitted when pollution is low")
    void grassDegradeNotEmittedLowPollution() {
        metrics.setPollutionScore(10.0);
        metrics.setSoilQuality(15.0);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.GRASS_DEGRADES_TO_DIRT);
        assertFalse(found, "GRASS_DEGRADES_TO_DIRT should NOT be emitted with low pollution");
    }

    @Test
    @DisplayName("VEGETATION_SPREADS emitted when vegetationPressure > 60 and soilQuality > 50")
    void vegetationSpreads() {
        metrics.setVegetationPressure(80.0);
        metrics.setSoilQuality(70.0);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.VEGETATION_SPREADS);
        assertTrue(found, "VEGETATION_SPREADS should have been emitted");
    }

    @Test
    @DisplayName("VEGETATION_SPREADS not emitted when soil is poor")
    void vegetationSpreadsNotEmittedPoorSoil() {
        metrics.setVegetationPressure(80.0);
        metrics.setSoilQuality(30.0); // below threshold

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.VEGETATION_SPREADS);
        assertFalse(found, "VEGETATION_SPREADS should NOT be emitted with poor soil");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_SLOWED emitted when logging depletion > 50")
    void saplingGrowthSlowed() {
        ResourceRegionData resources = ResourceRegionData.defaults();
        resources.recordLogging(70.0);
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, resources);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_SLOWED);
        assertTrue(found, "SAPLING_GROWTH_SLOWED should have been emitted");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_SLOWED not emitted when logging depletion is low")
    void saplingSlowNotEmittedLowLogging() {
        ResourceRegionData resources = ResourceRegionData.defaults();
        resources.recordLogging(20.0); // below threshold
        region.getModuleData().put(ResourceDepletionModule.MODULE_ID, resources);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_SLOWED);
        assertFalse(found, "SAPLING_GROWTH_SLOWED should NOT be emitted with low logging");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_BOOSTED emitted when succession stage >= YOUNG_WOODLAND")
    void saplingGrowthBoostedAtYoungWoodland() {
        RecoveryRegionData recovery = new RecoveryRegionData(
                SuccessionStage.YOUNG_WOODLAND, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_BOOSTED);
        assertTrue(found, "SAPLING_GROWTH_BOOSTED should have been emitted at YOUNG_WOODLAND");
    }

    @Test
    @DisplayName("SAPLING_GROWTH_BOOSTED not emitted below YOUNG_WOODLAND")
    void saplingBoostNotEmittedBelowYoungWoodland() {
        RecoveryRegionData recovery = new RecoveryRegionData(
                SuccessionStage.SCRUBLAND, 0.0, 0.0);
        region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.SAPLING_GROWTH_BOOSTED);
        assertFalse(found, "SAPLING_GROWTH_BOOSTED should NOT be emitted below YOUNG_WOODLAND");
    }

    @Test
    @DisplayName("POLLUTION_VISUAL_INDICATOR emitted when pollutionScore > 70")
    void pollutionVisualIndicator() {
        metrics.setPollutionScore(85.0);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.POLLUTION_VISUAL_INDICATOR);
        assertTrue(found, "POLLUTION_VISUAL_INDICATOR should have been emitted");
    }

    @Test
    @DisplayName("POLLUTION_VISUAL_INDICATOR not emitted below threshold")
    void pollutionVisualNotEmitted() {
        metrics.setPollutionScore(5.0);

        module.updateRegion(new RegionUpdateContext(region));

        boolean found = captured.stream()
                .anyMatch(r -> r.type() == WorldEffectType.POLLUTION_VISUAL_INDICATOR);
        assertFalse(found, "POLLUTION_VISUAL_INDICATOR should NOT be emitted below threshold");
    }

    @Test
    @DisplayName("emitted request has intensity in [0, 1]")
    void requestIntensityInRange() {
        metrics.setPollutionScore(80.0);
        metrics.setSoilQuality(15.0);

        module.updateRegion(new RegionUpdateContext(region));

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

    @Test
    @DisplayName("emitted request carries the correct region coordinate")
    void requestHasCorrectCoordinate() {
        metrics.setPollutionScore(85.0);
        module.updateRegion(new RegionUpdateContext(region));

        assertFalse(captured.isEmpty(), "Should have emitted at least one request");
        assertEquals(region.getCoordinate(), captured.get(0).region());
    }

    @Test
    @DisplayName("returns noChange when no effects are triggered")
    void noChangeWhenNoEffects() {
        // Default metrics: pollution=0, veg=50, soil=60 — none of the five conditions met
        ModuleUpdateResult result = module.updateRegion(new RegionUpdateContext(region));
        assertFalse(result.changedRegion());
        assertTrue(captured.isEmpty());
    }

    @Test
    @DisplayName("returns changed when at least one effect is triggered")
    void changedWhenEffectTriggered() {
        metrics.setPollutionScore(85.0);
        ModuleUpdateResult result = module.updateRegion(new RegionUpdateContext(region));
        assertTrue(result.changedRegion());
    }

    @Test
    @DisplayName("multiple consumers all receive the same requests")
    void multipleConsumersAllReceive() {
        List<WorldEffectRequest> second = new ArrayList<>();
        module.registerConsumer(second::add);
        metrics.setPollutionScore(85.0);

        module.updateRegion(new RegionUpdateContext(region));

        assertFalse(captured.isEmpty());
        assertEquals(captured.size(), second.size(),
                "Both consumers should receive the same number of requests");
    }

    @Test
    @DisplayName("NO_OP consumer can be registered without error")
    void noOpConsumer() {
        WorldEffectsModule fresh = new WorldEffectsModule();
        assertDoesNotThrow(() -> fresh.registerConsumer(WorldEffectConsumer.NO_OP));
    }

    @Test
    @DisplayName("shutdown clears all registered consumers")
    void shutdownClearsConsumers() {
        module.shutdown();
        assertTrue(module.getConsumers().isEmpty(), "Consumers should be cleared after shutdown");
    }
}
