package peruncs.eclipsestore.core;

import org.eclipse.store.gigamap.types.*;
import org.junit.jupiter.api.Test;

import java.time.Instant;

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

/// Reproduces `ArrayIndexOutOfBoundsException` in Eclipse Store 4.0.1
/// `SubBitmapIndexHashing.index()`.
///
/// **Root Cause:** `AbstractCompositeBitmapIndex.internalHandleChanged()` calls
/// `ensureSubIndices(newKeys)` which may grow the sub-index array (e.g. from 1 to 6),
/// then iterates **all** sub-indices calling `subIndex.internalHandleChanged(oldKeys, ...)`.
/// Each sub-index calls `internalLookupEntry(oldKeys)` → `indexEntity(oldKeys)` →
/// `index(oldKeys)` → `oldKeys[this.position]`.
/// When `oldKeys` is the `NULL()` sentinel (length 1) but `this.position >= 1`,
/// it throws `ArrayIndexOutOfBoundsException`.
///
/// **Trigger scenario:** An entity is added with a `null` composite-indexed field
/// (e.g. `IndexerInstant` returns `NULL()` = `Object[1]`), then updated to set
/// a non-null value (composite key becomes `Object[6]`).
///
/// **Stack trace:**
/// ```
/// java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
///   at SubBitmapIndexHashing.index(SubBitmapIndexHashing.java:97)
///   at AbstractBitmapIndexHashing.indexEntity(AbstractBitmapIndexHashing.java:98)
///   at AbstractBitmapIndexHashing.internalEnsureEntry(AbstractBitmapIndexHashing.java:131)
///   at AbstractBitmapIndexHashing.internalHandleChanged(AbstractBitmapIndexHashing.java:349)
///   at SubBitmapIndexHashing.internalHandleChanged(SubBitmapIndexHashing.java:157)
///   at AbstractCompositeBitmapIndex.internalHandleChanged(AbstractCompositeBitmapIndex.java:534)
/// ```
///
/// **Buggy code** in `AbstractCompositeBitmapIndex.java` lines 528-537:
/// ```java
/// final void internalHandleChanged(final KS oldKeys, final long entityId, final KS newKeys) {
///     this.ensureSubIndices(newKeys);          // grows sub-indices to match newKeys.length
///     for (final Sub<E, KS, K> subIndex : this.subIndices) {
///         subIndex.internalHandleChanged(oldKeys, entityId, newKeys); // oldKeys too short!
///     }
/// }
/// ```
public class SubBitmapIndexHashingBugTest {

    /// Entity with an optional [Instant] field — mirrors Booking with nullable
    /// `expiresAt`, `billingReportedAt`, etc.
    static final class Event {
        final long id;
        Instant timestamp; // initially null, set on update

        Event(long id) {
            this.id = id;
            this.timestamp = null;
        }
    }

    static final BinaryIndexerLong<Event> PK = new BinaryIndexerLong.Abstract<>() {
        @Override
        protected Long getLong(Event e) {
            return e.id;
        }
    };

    /// `IndexerInstant` is a `HashingCompositeIndexer.AbstractSingleValueFixedSize`
    /// that decomposes an [Instant] into `Object[6]` (year, month, day, hour, min, sec).
    /// For `null` values it returns `NULL()` = `Object[1]`.
    static final IndexerInstant<Event> TIMESTAMP_INDEXER = new IndexerInstant.Abstract<>() {
        @Override
        protected Instant getInstant(Event e) {
            return e.timestamp;
        }
    };

    @Test
    void nullToNonNullCompositeKeyUpdateCausesArrayIndexOutOfBounds() {
        var map = GigaMap.<Event>Builder()
                .withBitmapIdentityIndex(PK)
                .withBitmapIndex(TIMESTAMP_INDEXER)
                .build();

        // Step 1: Add entity with null timestamp → composite key = NULL() = Object[1]
        var event = new Event(1L);
        map.add(event);

        // Step 2: Update entity to set non-null timestamp → composite key = Object[6]
        // This triggers the bug: oldKeys.length=1, newKeys.length=6,
        // sub-indices 1..5 try oldKeys[position] → ArrayIndexOutOfBoundsException
        var ex = assertThrows(ArrayIndexOutOfBoundsException.class, () ->
                map.update(event, e -> e.timestamp = Instant.parse("2025-06-15T10:30:00Z"))
        );

        assertTrue(ex.getMessage().contains("out of bounds for length 1"),
                "Expected access beyond NULL() sentinel array (length 1)");
    }
}
