package com.livingworld.regions;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Unit tests for {@link RegionCoordinate}.
 */
class RegionCoordinateTest {

    // ------------------------------------------------------------------
    // Validation – dimensionId
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("dimensionId validation")
    class DimensionIdValidation {

        @Test
        void nullDimensionIdThrows() {
            IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
                    () -> new RegionCoordinate(null, 0, 0));
            assertTrue(ex.getMessage().contains("dimensionId"));
        }

        @Test
        void blankDimensionIdThrows() {
            IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
                    () -> new RegionCoordinate("", 0, 0));
            assertTrue(ex.getMessage().contains("dimensionId"));
        }

        @Test
        void whitespaceOnlyDimensionIdThrows() {
            IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
                    () -> new RegionCoordinate("   ", 0, 0));
            assertTrue(ex.getMessage().contains("dimensionId"));
        }

        @Test
        void validDimensionIdAccepted() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertEquals("overworld", rc.dimensionId());
        }
    }

    // ------------------------------------------------------------------
    // Validation – regionSizeChunks
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("regionSizeChunks validation")
    class RegionSizeValidation {

        @Test
        void zeroRegionSizeThrows() {
            assertThrows(IllegalArgumentException.class,
                    () -> RegionCoordinate.fromChunk("overworld", 0, 0, 0));
        }

        @Test
        void negativeRegionSizeThrows() {
            assertThrows(IllegalArgumentException.class,
                    () -> RegionCoordinate.fromChunk("overworld", 0, 0, -1));
        }

        @Test
        void oneRegionSizeAccepted() {
            RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 5, 5, 1);
            assertEquals(5, rc.x());
            assertEquals(5, rc.z());
        }
    }

    // ------------------------------------------------------------------
    // fromBlock – specified test cases
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("fromBlock coordinate mapping")
    class FromBlockMapping {

        /** Block 0,0 maps to region 0,0 */
        @Test
        void blockZeroZeroMapsToRegionZeroZero() {
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 0, 0, 8);
            assertEquals(0, rc.x());
            assertEquals(0, rc.z());
        }

        /** Block 127,127 maps to region 0,0 with region size 8 chunks */
        @Test
        void block127_127MapsToRegionZeroZero() {
            // chunk = floorDiv(block, 16) → floorDiv(127,16) = 7
            // region = floorDiv(chunk, 8) → floorDiv(7,8) = 0
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 127, 127, 8);
            assertEquals(0, rc.x());
            assertEquals(0, rc.z());
        }

        /** Block 128,0 maps to region 1,0 */
        @Test
        void block128ZeroMapsToRegionOneZero() {
            // chunk = floorDiv(128,16) = 8
            // region = floorDiv(8,8) = 1
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 128, 0, 8);
            assertEquals(1, rc.x());
            assertEquals(0, rc.z());
        }

        /** Block -1,0 maps to region -1,0 */
        @Test
        void blockMinusOneZeroMapsToRegionMinusOneZero() {
            // chunk = floorDiv(-1,16) = -1
            // region = floorDiv(-1,8) = -1
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -1, 0, 8);
            assertEquals(-1, rc.x());
            assertEquals(0, rc.z());
        }

        /** Block -128,0 maps to region -1,0 */
        @Test
        void blockMinus128ZeroMapsToRegionMinusOneZero() {
            // chunk = floorDiv(-128,16) = -8
            // region = floorDiv(-8,8) = -1
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -128, 0, 8);
            assertEquals(-1, rc.x());
            assertEquals(0, rc.z());
        }

        /** Block -129,0 maps to region -2,0 */
        @Test
        void blockMinus129ZeroMapsToRegionMinusTwoZero() {
            // chunk = floorDiv(-129,16) = -9
            // region = floorDiv(-9,8) = -2
            RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -129, 0, 8);
            assertEquals(-2, rc.x());
            assertEquals(0, rc.z());
        }

        @Test
        void negativeZBoundaryUsesFloorDivision() {
            assertEquals(
                    new RegionCoordinate("overworld", 0, -1),
                    RegionCoordinate.fromBlock("overworld", 0, -128, 8));
            assertEquals(
                    new RegionCoordinate("overworld", 0, -2),
                    RegionCoordinate.fromBlock("overworld", 0, -129, 8));
        }
    }

    // ------------------------------------------------------------------
    // fromChunk – basic checks
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("fromChunk coordinate mapping")
    class FromChunkMapping {

        @Test
        void chunkZeroZeroMapsToRegionZeroZero() {
            RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 0, 0, 8);
            assertEquals(0, rc.x());
            assertEquals(0, rc.z());
        }

        @Test
        void chunkSevenSevenMapsToRegionZeroZero() {
            RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 7, 7, 8);
            assertEquals(0, rc.x());
            assertEquals(0, rc.z());
        }

        @Test
        void chunkEightEightMapsToRegionOneOne() {
            RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 8, 8, 8);
            assertEquals(1, rc.x());
            assertEquals(1, rc.z());
        }

        @Test
        void chunkMinusOneMinusOneMapsToRegionMinusOneMinusOne() {
            RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", -1, -1, 8);
            assertEquals(-1, rc.x());
            assertEquals(-1, rc.z());
        }
    }

    // ------------------------------------------------------------------
    // Boundary methods – min/max chunk coordinates
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("minChunkX / minChunkZ / maxChunkX / maxChunkZ")
    class ChunkBoundaryMethods {

        @Test
        void regionZeroBoundariesWithSizeEight() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertEquals(0, rc.minChunkX(8));
            assertEquals(0, rc.minChunkZ(8));
            assertEquals(7, rc.maxChunkX(8));
            assertEquals(7, rc.maxChunkZ(8));
        }

        @Test
        void regionOneBoundariesWithSizeEight() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 1, 1);
            assertEquals(8, rc.minChunkX(8));
            assertEquals(8, rc.minChunkZ(8));
            assertEquals(15, rc.maxChunkX(8));
            assertEquals(15, rc.maxChunkZ(8));
        }

        @Test
        void regionMinusOneBoundariesWithSizeEight() {
            RegionCoordinate rc = new RegionCoordinate("overworld", -1, -1);
            assertEquals(-8, rc.minChunkX(8));
            assertEquals(-8, rc.minChunkZ(8));
            assertEquals(-1, rc.maxChunkX(8));
            assertEquals(-1, rc.maxChunkZ(8));
        }

        @Test
        void invalidRegionSizeThrowsInBoundaryMethods() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertThrows(IllegalArgumentException.class, () -> rc.minChunkX(0));
            assertThrows(IllegalArgumentException.class, () -> rc.minChunkZ(-1));
            assertThrows(IllegalArgumentException.class, () -> rc.maxChunkX(0));
            assertThrows(IllegalArgumentException.class, () -> rc.maxChunkZ(-5));
        }
    }

    // ------------------------------------------------------------------
    // centerBlock methods
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("centerBlockX / centerBlockZ")
    class CenterBlockMethods {

        /** Region 0 with size 8: chunks 0-7, blocks 0-127 → centre = floorDiv(0+127,2) = 63 */
        @Test
        void centerOfRegionZero() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertEquals(63, rc.centerBlockX(8));
            assertEquals(63, rc.centerBlockZ(8));
        }

        /** Region 1 with size 8: chunks 8-15, blocks 128-255 → centre = floorDiv(128+255,2) = 191 */
        @Test
        void centerOfRegionOne() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 1, 1);
            assertEquals(191, rc.centerBlockX(8));
            assertEquals(191, rc.centerBlockZ(8));
        }

        /** Region -1 with size 8: chunks -8 to -1, blocks -128 to -1 → centre = floorDiv(-128+-1,2) = -65 */
        @Test
        void centerOfRegionMinusOne() {
            RegionCoordinate rc = new RegionCoordinate("overworld", -1, -1);
            assertEquals(-65, rc.centerBlockX(8));
            assertEquals(-65, rc.centerBlockZ(8));
        }

        @Test
        void invalidRegionSizeThrowsInCenterMethods() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertThrows(IllegalArgumentException.class, () -> rc.centerBlockX(0));
            assertThrows(IllegalArgumentException.class, () -> rc.centerBlockZ(-1));
        }
    }

    // ------------------------------------------------------------------
    // stableId
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("stableId")
    class StableIdTests {

        @Test
        void stableIdIsStable() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 1, -2);
            String id1 = rc.stableId();
            String id2 = rc.stableId();
            assertEquals(id1, id2);
        }

        @Test
        void stableIdFormat() {
            RegionCoordinate rc = new RegionCoordinate("the_nether", 3, 5);
            assertEquals("the_nether:3:5", rc.stableId());
        }

        @Test
        void negativeCoordinatesInStableId() {
            RegionCoordinate rc = new RegionCoordinate("overworld", -1, -2);
            assertEquals("overworld:-1:-2", rc.stableId());
        }
    }

    // ------------------------------------------------------------------
    // HashMap key usage
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("HashMap key compatibility")
    class HashMapKeyTests {

        @Test
        void worksAsHashMapKey() {
            Map<RegionCoordinate, String> map = new HashMap<>();
            RegionCoordinate key1 = new RegionCoordinate("overworld", 0, 0);
            RegionCoordinate key2 = new RegionCoordinate("overworld", 0, 0);
            RegionCoordinate key3 = new RegionCoordinate("overworld", 1, 0);

            map.put(key1, "value1");
            assertEquals("value1", map.get(key2)); // same logical key
            assertNotEquals("value1", map.get(key3)); // different key
        }

        @Test
        void equalsAndHashCodeContract() {
            RegionCoordinate rc1 = new RegionCoordinate("overworld", 0, 0);
            RegionCoordinate rc2 = new RegionCoordinate("overworld", 0, 0);

            assertEquals(rc1, rc2);
            assertEquals(rc1.hashCode(), rc2.hashCode());
        }

        @Test
        void notEqualToDifferentDimension() {
            RegionCoordinate rc1 = new RegionCoordinate("overworld", 0, 0);
            RegionCoordinate rc2 = new RegionCoordinate("nether", 0, 0);

            assertFalse(rc1.equals(rc2));
        }

        @Test
        void notEqualToDifferentType() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertFalse(rc.equals("not a region coordinate"));
        }

        @Test
        void nullSafeEquals() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
            assertFalse(rc.equals(null));
        }
    }

    // ------------------------------------------------------------------
    // toString
    // ------------------------------------------------------------------

    @Nested
    @DisplayName("toString")
    class ToStringTests {

        @Test
        void toStringContainsAllFields() {
            RegionCoordinate rc = new RegionCoordinate("overworld", 1, -3);
            String s = rc.toString();
            assertTrue(s.contains("RegionCoordinate"));
            assertTrue(s.contains("overworld"));
            assertTrue(s.contains("x=1"));
            assertTrue(s.contains("z=-3"));
        }

        @Test
        void toStringShouldNotThrow() {
            RegionCoordinate rc = new RegionCoordinate("the_end", 0, 0);
            assertDoesNotThrow(() -> rc.toString());
        }
    }
}
