/**
 * GraphAdjacencyIndex Pagination Tests (v5.8.0, updated for the 8.0 u64 contract)
 *
 * Tests for pagination support in GraphIndex methods:
 * - getNeighbors() with limit/offset
 * - getVerbIdsBySource() with limit/offset
 * - getVerbIdsByTarget() with limit/offset
 *
 * 8.0 BigInt boundary: entity ints in (resolved via the metadata index's
 * idMapper), entity/verb ints out (`bigint[]`). Entity ints map back to UUIDs
 * via `idMapper.getUuid(Number(int))`; verb ints via `verbIntsToIds()`.
 */

import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'

describe('GraphAdjacencyIndex Pagination', () => {
  let brain: Brainy
  let centralId: string
  let neighborIds: string[]

  /** The brain's graph index (BigInt provider contract). */
  const graphIndex = () => (brain as any).graphIndex

  /** The shared UUID ↔ int resolver. */
  const idMapper = () => (brain as any).metadataIndex.getIdMapper()

  /** Resolve a UUID to its entity int (must already be mapped). */
  const entityInt = (uuid: string): bigint => {
    const intId = idMapper().getInt(uuid)
    expect(intId).toBeDefined()
    return BigInt(intId!)
  }

  /** Map returned entity ints back to UUIDs. */
  const intsToUuids = (ints: bigint[]): string[] =>
    ints
      .map((i) => idMapper().getUuid(Number(i)))
      .filter((u: string | undefined): u is string => u !== undefined)

  beforeEach(async () => {
    brain = new Brainy({ requireSubtype: false })
    await brain.init()

    // Create central entity
    centralId = await brain.add({
      data: { name: 'Central Hub' },
      type: NounType.Thing
    })

    // Create 50 neighbor entities with relationships
    neighborIds = []
    for (let i = 0; i < 50; i++) {
      const neighborId = await brain.add({
        data: { name: `Neighbor ${i}`, index: i },
        type: NounType.Thing
      })
      neighborIds.push(neighborId)

      // Create outgoing relationship from central to neighbor
      await brain.relate({
        from: centralId,
        to: neighborId,
        type: VerbType.RelatesTo
      })
    }
  })

  describe('getNeighbors() Pagination', () => {
    it('should return all neighbors without pagination', async () => {
      const neighborInts = await graphIndex().getNeighbors(entityInt(centralId))
      const neighbors = intsToUuids(neighborInts)

      // Should return all 50 neighbors
      expect(neighbors).toHaveLength(50)
      expect(neighbors.every((id: string) => neighborIds.includes(id))).toBe(true)
    })

    it('should return all outgoing neighbors with direction option', async () => {
      const neighbors = await graphIndex().getNeighbors(entityInt(centralId), { direction: 'out' })

      expect(neighbors).toHaveLength(50)
    })

    it('should paginate with limit only', async () => {
      const page1Ints = await graphIndex().getNeighbors(entityInt(centralId), { limit: 10 })
      const page1 = intsToUuids(page1Ints)

      expect(page1).toHaveLength(10)
      expect(page1.every((id: string) => neighborIds.includes(id))).toBe(true)
    })

    it('should paginate with offset only', async () => {
      const all = await graphIndex().getNeighbors(entityInt(centralId))
      const offsetResults = await graphIndex().getNeighbors(entityInt(centralId), { offset: 10 })

      // Offset 10 should return all after first 10
      expect(offsetResults).toHaveLength(all.length - 10)
    })

    it('should paginate with both limit and offset', async () => {
      const central = entityInt(centralId)
      const page1: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
      const page2: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
      const page3: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })

      // Each page should have 10 results
      expect(page1).toHaveLength(10)
      expect(page2).toHaveLength(10)
      expect(page3).toHaveLength(10)

      // Pages should not overlap
      const page2Set = new Set(page2)
      const page3Set = new Set(page3)

      const overlap12 = page1.filter((id) => page2Set.has(id))
      const overlap23 = page2.filter((id) => page3Set.has(id))

      expect(overlap12).toHaveLength(0)
      expect(overlap23).toHaveLength(0)
    })

    it('should handle offset beyond total count', async () => {
      const results = await graphIndex().getNeighbors(entityInt(centralId), { offset: 100 })

      // Offset 100 > 50 total → empty array
      expect(results).toHaveLength(0)
    })

    it('should handle limit = 0', async () => {
      const results = await graphIndex().getNeighbors(entityInt(centralId), { limit: 0 })

      expect(results).toHaveLength(0)
    })

    it('should paginate with direction filter', async () => {
      // Get first 10 outgoing neighbors
      const page1 = await graphIndex().getNeighbors(entityInt(centralId), {
        direction: 'out',
        limit: 10,
        offset: 0
      })

      expect(page1).toHaveLength(10)
    })

    it('should handle pagination with incoming direction', async () => {
      // Create some incoming relationships
      const sourceId = await brain.add({
        data: { name: 'Source' },
        type: NounType.Thing
      })

      await brain.relate({
        from: sourceId,
        to: centralId,
        type: VerbType.RelatesTo
      })

      // Get incoming neighbors with pagination
      const incoming = await graphIndex().getNeighbors(entityInt(centralId), {
        direction: 'in',
        limit: 10
      })

      expect(incoming.length).toBeGreaterThan(0)
    })
  })

  describe('getVerbIdsBySource() Pagination', () => {
    it('should return all verb ints without pagination and resolve them back to ids', async () => {
      const verbInts: bigint[] = await graphIndex().getVerbIdsBySource(entityInt(centralId))

      // Should return all 50 verb ints
      expect(verbInts).toHaveLength(50)

      // Every int must resolve back to a verb-id string
      const verbIds: (string | null)[] = await graphIndex().verbIntsToIds(verbInts)
      expect(verbIds).toHaveLength(50)
      expect(verbIds.every((id) => typeof id === 'string')).toBe(true)
    })

    it('should paginate verb ints with limit', async () => {
      const page1 = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 10 })

      expect(page1).toHaveLength(10)
    })

    it('should paginate verb ints with offset', async () => {
      const all = await graphIndex().getVerbIdsBySource(entityInt(centralId))
      const offsetResults = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 10 })

      expect(offsetResults).toHaveLength(all.length - 10)
    })

    it('should paginate verb ints with limit and offset', async () => {
      const central = entityInt(centralId)
      const page1: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 0 })
      const page2: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 10 })
      const page3: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 20 })

      // Each page should have 10 results
      expect(page1).toHaveLength(10)
      expect(page2).toHaveLength(10)
      expect(page3).toHaveLength(10)

      // Pages should not overlap
      const combined = [...page1, ...page2, ...page3]
      const unique = new Set(combined)
      expect(unique.size).toBe(30) // All unique
    })

    it('should handle pagination edge cases for verb ints', async () => {
      // Offset beyond total
      const beyondTotal = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 100 })
      expect(beyondTotal).toHaveLength(0)

      // Limit = 0
      const zeroLimit = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 0 })
      expect(zeroLimit).toHaveLength(0)
    })
  })

  describe('getVerbIdsByTarget() Pagination', () => {
    it('should return all verb ints targeting an entity', async () => {
      // Pick a neighbor that's a target of relationships
      const targetId = neighborIds[0]
      const verbInts = await graphIndex().getVerbIdsByTarget(entityInt(targetId))

      // Should have at least 1 (the relationship from central)
      expect(verbInts.length).toBeGreaterThanOrEqual(1)
    })

    it('should paginate verb ints by target with limit', async () => {
      // Create entity with many incoming relationships
      const popularTarget = await brain.add({
        data: { name: 'Popular Target' },
        type: NounType.Thing
      })

      // Create 30 relationships pointing to it
      for (let i = 0; i < 30; i++) {
        const sourceId = await brain.add({
          data: { name: `Source ${i}` },
          type: NounType.Thing
        })
        await brain.relate({
          from: sourceId,
          to: popularTarget,
          type: VerbType.RelatesTo
        })
      }

      // Paginate
      const target = entityInt(popularTarget)
      const page1: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10 })
      const page2: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10, offset: 10 })

      expect(page1).toHaveLength(10)
      expect(page2).toHaveLength(10)

      // No overlap
      const overlap = page1.filter((id) => page2.includes(id))
      expect(overlap).toHaveLength(0)
    })
  })

  describe('Performance with Pagination', () => {
    it('should maintain sub-5ms performance with pagination', async () => {
      const central = entityInt(centralId)

      // Multiple paginated queries should be fast
      const startTime = performance.now()

      await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
      await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
      await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })

      const elapsed = performance.now() - startTime

      // 3 paginated queries should complete in < 15ms (< 5ms each)
      expect(elapsed).toBeLessThan(15)
    })
  })

  describe('Real-World Use Cases', () => {
    it('should efficiently paginate through high-degree node', async () => {
      // Simulate popular entity with 100+ relationships
      const hub = await brain.add({
        data: { name: 'Popular Hub' },
        type: NounType.Thing
      })

      // Create 100 relationships
      const targetIds: string[] = []
      for (let i = 0; i < 100; i++) {
        const targetId = await brain.add({
          data: { name: `Target ${i}` },
          type: NounType.Thing
        })
        targetIds.push(targetId)
        await brain.relate({
          from: hub,
          to: targetId,
          type: VerbType.RelatesTo
        })
      }

      // Paginate in chunks of 25
      const hubInt = entityInt(hub)
      const chunks: bigint[][] = []
      for (let offset = 0; offset < 100; offset += 25) {
        const chunk = await graphIndex().getNeighbors(hubInt, {
          direction: 'out',
          limit: 25,
          offset
        })
        chunks.push(chunk)
      }

      // Should have 4 chunks
      expect(chunks).toHaveLength(4)

      // Each chunk should have 25 items
      chunks.forEach(chunk => {
        expect(chunk).toHaveLength(25)
      })

      // All chunks combined should equal total
      const allNeighbors = chunks.flat()
      expect(allNeighbors).toHaveLength(100)

      // No duplicates
      const uniqueNeighbors = new Set(allNeighbors)
      expect(uniqueNeighbors.size).toBe(100)
    })
  })
})
