package com.livingworld.regions;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Set;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests for {@link RegionLifecycleController}.
 */
class RegionLifecycleControllerTest {

    private static final Set<String> EXPECTED_TRANSITIONS = Set.of(
            "UNLOADED->LOADING",
            "LOADING->ACTIVE",
            "LOADING->FAILED",
            "ACTIVE->DIRTY",
            "DIRTY->SAVING",
            "SAVING->ACTIVE",
            "SAVING->FAILED",
            "ACTIVE->UNLOADING",
            "UNLOADING->UNLOADED",
            "FAILED->LOADING");

    // ------------------------------------------------------------------
    // Allowed transitions
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("Allowed transitions")
    class AllowedTransitions {

        @Test
        @DisplayName("complete transition matrix matches the lifecycle design")
        void completeTransitionMatrixMatchesDesign() {
            for (RegionLifecycleState from : RegionLifecycleState.values()) {
                for (RegionLifecycleState to : RegionLifecycleState.values()) {
                    String transition = from.name() + "->" + to.name();
                    assertEquals(
                            EXPECTED_TRANSITIONS.contains(transition),
                            RegionLifecycleController.canTransition(from, to),
                            transition);
                }
            }
        }

        @Test
        @DisplayName("UNLOADED -> LOADING is allowed")
        void unloadedToLoading() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.UNLOADED, RegionLifecycleState.LOADING));
        }

        @Test
        @DisplayName("LOADING -> ACTIVE is allowed")
        void loadingToActive() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.LOADING, RegionLifecycleState.ACTIVE));
        }

        @Test
        @DisplayName("LOADING -> FAILED is allowed")
        void loadingToFailed() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.LOADING, RegionLifecycleState.FAILED));
        }

        @Test
        @DisplayName("ACTIVE -> DIRTY is allowed")
        void activeToDirty() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.ACTIVE, RegionLifecycleState.DIRTY));
        }

        @Test
        @DisplayName("DIRTY -> SAVING is allowed")
        void dirtyToSaving() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.DIRTY, RegionLifecycleState.SAVING));
        }

        @Test
        @DisplayName("SAVING -> ACTIVE is allowed")
        void savingToActive() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.SAVING, RegionLifecycleState.ACTIVE));
        }

        @Test
        @DisplayName("SAVING -> FAILED is allowed")
        void savingToFailed() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.SAVING, RegionLifecycleState.FAILED));
        }

        @Test
        @DisplayName("ACTIVE -> UNLOADING is allowed")
        void activeToUnloading() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.ACTIVE, RegionLifecycleState.UNLOADING));
        }

        @Test
        @DisplayName("UNLOADING -> UNLOADED is allowed")
        void unloadingToUnloaded() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.UNLOADING, RegionLifecycleState.UNLOADED));
        }

        @Test
        @DisplayName("FAILED -> LOADING is allowed")
        void failedToLoading() {
            assertTrue(RegionLifecycleController.canTransition(
                RegionLifecycleState.FAILED, RegionLifecycleState.LOADING));
        }
    }

    // ------------------------------------------------------------------
    // Disallowed transitions
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("Disallowed transitions")
    class DisallowedTransitions {

        private static final String[] STATES = {
            "UNLOADED", "LOADING", "ACTIVE", "DIRTY", "SAVING", "FAILED", "UNLOADING"
        };

        @Test
        @DisplayName("Same state is not allowed (no self-transitions)")
        void sameStateNotAllowed() {
            for (String state : STATES) {
                assertFalse(RegionLifecycleController.canTransition(
                    RegionLifecycleState.valueOf(state), RegionLifecycleState.valueOf(state)));
            }
        }

        @Test
        @DisplayName("UNLOADED -> ACTIVE is not allowed")
        void unloadedToActive() {
            assertFalse(RegionLifecycleController.canTransition(
                RegionLifecycleState.UNLOADED, RegionLifecycleState.ACTIVE));
        }

        @Test
        @DisplayName("ACTIVE -> LOADING is not allowed")
        void activeToLoading() {
            assertFalse(RegionLifecycleController.canTransition(
                RegionLifecycleState.ACTIVE, RegionLifecycleState.LOADING));
        }

        @Test
        @DisplayName("FAILED -> UNLOADED is not allowed")
        void failedToUnloaded() {
            assertFalse(RegionLifecycleController.canTransition(
                RegionLifecycleState.FAILED, RegionLifecycleState.UNLOADED));
        }

        @Test
        @DisplayName("ACTIVE -> SAVING is not allowed")
        void activeToSaving() {
            assertFalse(RegionLifecycleController.canTransition(
                RegionLifecycleState.ACTIVE, RegionLifecycleState.SAVING));
        }

        @Test
        @DisplayName("All invalid transitions throw IllegalStateException on transition()")
        void invalidTransitionsThrowException() {
            Region region1 = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.ACTIVE, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            // ACTIVE -> LOADING (invalid)
            assertThrows(IllegalStateException.class, () ->
                RegionLifecycleController.transition(region1, RegionLifecycleState.LOADING));

            // ACTIVE -> SAVING (invalid)
            Region region2 = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.ACTIVE, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            assertThrows(IllegalStateException.class, () ->
                RegionLifecycleController.transition(region2, RegionLifecycleState.SAVING));
        }
    }

    // ------------------------------------------------------------------
    // Null handling
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("Null handling")
    class NullHandling {

        @Test
        @DisplayName("canTransition throws IllegalArgumentException for null from")
        void canTransitionNullFrom() {
            assertThrows(IllegalArgumentException.class, () ->
                RegionLifecycleController.canTransition(null, RegionLifecycleState.ACTIVE));
        }

        @Test
        @DisplayName("canTransition throws IllegalArgumentException for null to")
        void canTransitionNullTo() {
            assertThrows(IllegalArgumentException.class, () ->
                RegionLifecycleController.canTransition(RegionLifecycleState.UNLOADED, null));
        }

        @Test
        @DisplayName("transition throws IllegalArgumentException for null region")
        void transitionNullRegion() {
            assertThrows(IllegalArgumentException.class, () ->
                RegionLifecycleController.transition(null, RegionLifecycleState.LOADING));
        }

        @Test
        @DisplayName("transition throws IllegalArgumentException for null target")
        void transitionNullTarget() {
            Region region = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.UNLOADED, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            assertThrows(IllegalArgumentException.class, () ->
                RegionLifecycleController.transition(region, null));
        }
    }

    // ------------------------------------------------------------------
    // transition() side effects
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("transition() side effects")
    class TransitionSideEffects {

        @Test
        @DisplayName("transition() updates lifecycle state and marks dirty")
        void transitionUpdatesStateAndMarksDirty() {
            Region region = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.UNLOADED, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            assertFalse(region.isDirty());
            assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());

            RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);

            assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());
            assertTrue(region.isDirty());
        }

        @Test
        @DisplayName("transition() updates state through full load-save-unload cycle")
        void fullCycleTransition() {
            Region region = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.UNLOADED, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            // UNLOADED -> LOADING
            RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);
            assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());

            // LOADING -> ACTIVE
            RegionLifecycleController.transition(region, RegionLifecycleState.ACTIVE);
            assertEquals(RegionLifecycleState.ACTIVE, region.getLifecycleState());

            // ACTIVE -> DIRTY
            RegionLifecycleController.transition(region, RegionLifecycleState.DIRTY);
            assertEquals(RegionLifecycleState.DIRTY, region.getLifecycleState());

            // DIRTY -> SAVING
            RegionLifecycleController.transition(region, RegionLifecycleState.SAVING);
            assertEquals(RegionLifecycleState.SAVING, region.getLifecycleState());

            // SAVING -> ACTIVE (successful save)
            RegionLifecycleController.transition(region, RegionLifecycleState.ACTIVE);
            assertEquals(RegionLifecycleState.ACTIVE, region.getLifecycleState());

            // ACTIVE -> UNLOADING
            RegionLifecycleController.transition(region, RegionLifecycleState.UNLOADING);
            assertEquals(RegionLifecycleState.UNLOADING, region.getLifecycleState());

            // UNLOADING -> UNLOADED
            RegionLifecycleController.transition(region, RegionLifecycleState.UNLOADED);
            assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());
        }

        @Test
        @DisplayName("transition() allows FAILED -> LOADING recovery")
        void failedToLoadingRecovery() {
            Region region = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.FAILED, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);
            assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());
        }

        @Test
        @DisplayName("transition() allows SAVING -> FAILED error path")
        void savingToFailedErrorPath() {
            Region region = new Region(
                java.util.UUID.randomUUID(),
                new RegionCoordinate("minecraft:overworld", 0, 0),
                RegionLifecycleState.ACTIVE, 0, 0, false,
                new RegionFlags(), new RegionMetrics(), new RegionModuleData());

            // ACTIVE -> DIRTY
            RegionLifecycleController.transition(region, RegionLifecycleState.DIRTY);
            assertEquals(RegionLifecycleState.DIRTY, region.getLifecycleState());

            // DIRTY -> SAVING
            RegionLifecycleController.transition(region, RegionLifecycleState.SAVING);
            assertEquals(RegionLifecycleState.SAVING, region.getLifecycleState());

            // SAVING -> FAILED (save error)
            RegionLifecycleController.transition(region, RegionLifecycleState.FAILED);
            assertEquals(RegionLifecycleState.FAILED, region.getLifecycleState());
        }
    }
}
