/**
 * @description Brainy 8.0 IdSpace contract: the JS fallback
 * `EntityIdMapper` caps at `u32::MAX` to match the metadata index's
 * Roaring32 bitmap width. When `nextId` would exceed the ceiling,
 * `getOrAssign` throws `EntityIdSpaceExceeded` with a message pointing
 * at the cortex 3.0 binary mapper's `idSpace: 'u64'` mode as the
 * migration path.
 *
 * This is the lockstep counterpart to cortex 3.0's Piece 10 / Step 15
 * (TS wrapper + napi BigInt siblings + Roaring32-or-Treemap
 * `PostingList` enum). Without this guard, a JS-only brainy install
 * past 4.29 B entities would silently truncate entity ids into the
 * low-32-bit range, zeroing query results and corrupting any
 * persisted int-keyed structure that consumed the mapper.
 */

import { describe, it, expect } from 'vitest'
import {
  EntityIdMapper,
  EntityIdSpaceExceeded,
  U32_ENTITY_ID_MAX,
} from '../../../src/utils/entityIdMapper.js'

/**
 * Construct a fresh in-memory mapper without going through brainy's
 * full storage adapter — we only exercise the in-RAM ceiling guard
 * here, so a minimal `getMetadata`-returning shim is enough.
 */
function makeMapper(): EntityIdMapper {
  const storage = {
    getMetadata: async () => null,
    setMetadata: async () => {},
    getNouns: async () => ({ items: [], totalCount: 0 }),
  } as any
  return new EntityIdMapper({ storage })
}

describe('EntityIdMapper U32 IdSpace ceiling (Brainy 8.0)', () => {
  it('U32_ENTITY_ID_MAX equals 0xFFFF_FFFF (u32 ceiling)', () => {
    expect(U32_ENTITY_ID_MAX).toBe(0xffff_ffff)
  })

  it('EntityIdSpaceExceeded is an Error subclass with attempted + ceiling', () => {
    const err = new EntityIdSpaceExceeded(0xffff_ffff + 1)
    expect(err).toBeInstanceOf(Error)
    expect(err.name).toBe('EntityIdSpaceExceeded')
    expect(err.ceiling).toBe(0xffff_ffff)
    expect(err.attempted).toBe(0x1_0000_0000)
    expect(err.message).toMatch(/u32::MAX/)
    expect(err.message).toMatch(/@soulcraft\/cor/)
    expect(err.message).toMatch(/idSpace: 'u64'/)
  })

  it('normal allocations under the ceiling succeed', async () => {
    const m = makeMapper()
    await m.init()
    const a = m.getOrAssign('uuid-a')
    const b = m.getOrAssign('uuid-b')
    expect(a).toBe(1)
    expect(b).toBe(2)
    expect(a).not.toBe(b)
  })

  it('throws EntityIdSpaceExceeded when nextId would exceed u32::MAX', async () => {
    const m = makeMapper()
    await m.init()
    // Reach into the mapper to bump `nextId` past the ceiling without
    // actually allocating 4.29 B entities at test time. This mirrors
    // the runtime invariant the guard protects.
    ;(m as any).nextId = U32_ENTITY_ID_MAX + 1
    expect(() => m.getOrAssign('uuid-overflow')).toThrow(EntityIdSpaceExceeded)
  })

  it('the last representable u32 int IS allocatable (boundary case)', async () => {
    const m = makeMapper()
    await m.init()
    ;(m as any).nextId = U32_ENTITY_ID_MAX
    // `nextId === U32_ENTITY_ID_MAX` is still within range — the
    // guard checks `> U32_ENTITY_ID_MAX`, so this last allocation
    // succeeds.
    const id = m.getOrAssign('uuid-at-ceiling')
    expect(id).toBe(U32_ENTITY_ID_MAX)
    // The NEXT allocation overflows.
    expect(() => m.getOrAssign('uuid-after-ceiling')).toThrow(
      EntityIdSpaceExceeded,
    )
  })

  it('existing uuid lookup never overflows even when nextId is past ceiling', async () => {
    const m = makeMapper()
    await m.init()
    const assigned = m.getOrAssign('uuid-existing')
    ;(m as any).nextId = U32_ENTITY_ID_MAX + 1
    // Looking up an already-assigned UUID does NOT allocate; the
    // guard is on the assign path only.
    expect(m.getOrAssign('uuid-existing')).toBe(assigned)
  })
})

describe('EntityIdMapper.entityIntsToUuids (bigint batch resolver — graph engine)', () => {
  it('reverse-resolves a BigInt64Array of assigned ints to UUIDs, in order', async () => {
    const m = makeMapper()
    await m.init()
    const a = m.getOrAssign('uuid-a')
    const b = m.getOrAssign('uuid-b')
    const c = m.getOrAssign('uuid-c')
    // Out-of-assignment order, to prove it's positional (not sorted).
    const ints = BigInt64Array.from([BigInt(c), BigInt(a), BigInt(b)])
    expect(m.entityIntsToUuids(ints)).toEqual(['uuid-c', 'uuid-a', 'uuid-b'])
  })

  it('yields "" for a never-assigned int (order-preserving, no drop)', async () => {
    const m = makeMapper()
    await m.init()
    const a = m.getOrAssign('uuid-a')
    const ints = BigInt64Array.from([BigInt(a), 999999n])
    // Position is preserved — the unknown int does not collapse the array.
    expect(m.entityIntsToUuids(ints)).toEqual(['uuid-a', ''])
  })

  it('returns an empty array for empty input', async () => {
    const m = makeMapper()
    await m.init()
    expect(m.entityIntsToUuids(new BigInt64Array(0))).toEqual([])
  })
})
