/**
 * @module bigint-contract.test
 * @description 8.0 u64 provider contract — BigInt boundary of the JS
 * GraphAdjacencyIndex and the coordinator that drives it.
 *
 * Covers:
 * - bigint round-trip through the JS graph index: `addVerb` returns a stable
 *   verb int, `getVerbIdsBySource`/`getVerbIdsByTarget` return that int, and
 *   `verbIntsToIds` maps it back to the verb-id string (null for unknown ints)
 * - `getNeighbors` speaks entity ints both ways via the shared idMapper
 * - unmapped entity ints / UUIDs produce empty reads, never errors
 * - a missing idMapper fails loudly (wiring bug, not a silent fallback)
 * - coordinator-level behavior: relate → related/neighbors → unrelate
 *   works end-to-end over the BigInt boundary (public API unchanged)
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy.js'
import { GraphAdjacencyIndex } from '../../../src/graph/graphAdjacencyIndex.js'
import { EntityIdMapper } from '../../../src/utils/entityIdMapper.js'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
import type { GraphVerb } from '../../../src/coreTypes.js'

// Verb ids are UUID-shaped by contract in 8.0 (storage enforces the shape on
// every save; cortex keys verb-int interning on the raw UUID bytes).
const VERB_UUID_1 = '11111111-1111-4111-8111-111111111111'
const VERB_UUID_2 = '22222222-2222-4222-8222-222222222222'
const VERB_UUID_3 = '33333333-3333-4333-8333-333333333333'

/** Build a minimal GraphVerb for direct index-level tests. */
function makeVerb(id: string, sourceId: string, targetId: string): GraphVerb {
  return {
    id,
    sourceId,
    targetId,
    vector: [],
    type: VerbType.RelatedTo,
    verb: VerbType.RelatedTo
  }
}

describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
  let storage: MemoryStorage
  let idMapper: EntityIdMapper
  let index: GraphAdjacencyIndex

  beforeEach(async () => {
    storage = new MemoryStorage()
    await storage.init()
    idMapper = new EntityIdMapper({ storage, storageKey: 'test:graph:idMapper' })
    await idMapper.init()
    index = new GraphAdjacencyIndex(storage, {}, idMapper)
  })

  afterEach(async () => {
    await index.close()
  })

  it('addVerb returns a stable verb int; reads return it; verbIntsToIds maps back', async () => {
    const sourceInt = BigInt(idMapper.getOrAssign('uuid-source'))
    const targetInt = BigInt(idMapper.getOrAssign('uuid-target'))
    const verb = makeVerb(VERB_UUID_1, 'uuid-source', 'uuid-target')
    verb.sourceInt = sourceInt
    verb.targetInt = targetInt

    // The 4th arg is the commit generation — contract parity with native
    // providers; the JS index ignores it (no per-generation edge chain).
    const verbInt = await index.addVerb(verb, sourceInt, targetInt, 1n)
    expect(typeof verbInt).toBe('bigint')

    // Re-adding the same verb id returns the SAME interned int (stable).
    const verbIntAgain = await index.addVerb(verb, sourceInt, targetInt, 1n)
    expect(verbIntAgain).toBe(verbInt)

    // Both directed verb-int reads surface the int from addVerb.
    const bySource = await index.getVerbIdsBySource(sourceInt)
    expect(bySource).toContain(verbInt)

    const byTarget = await index.getVerbIdsByTarget(targetInt)
    expect(byTarget).toContain(verbInt)

    // Batch reverse resolution is order-preserving with null for unknowns.
    const resolved = await index.verbIntsToIds([verbInt, 999_999n])
    expect(resolved).toEqual([VERB_UUID_1, null])
  })

  it('getNeighbors speaks entity ints in both directions', async () => {
    const aInt = BigInt(idMapper.getOrAssign('uuid-a'))
    const bInt = BigInt(idMapper.getOrAssign('uuid-b'))
    const verb = makeVerb(VERB_UUID_2, 'uuid-a', 'uuid-b')

    await index.addVerb(verb, aInt, bInt, 1n)

    const out = await index.getNeighbors(aInt, { direction: 'out' })
    expect(out).toEqual([bInt])

    const incoming = await index.getNeighbors(bInt, { direction: 'in' })
    expect(incoming).toEqual([aInt])

    const both = await index.getNeighbors(aInt)
    expect(both).toContain(bInt)
  })

  it('reads for unmapped entity ints return empty without error', async () => {
    // 424242 was never assigned by the mapper — no UUID behind it.
    const ghost = 424_242n
    expect(await index.getNeighbors(ghost)).toEqual([])
    expect(await index.getVerbIdsBySource(ghost)).toEqual([])
    expect(await index.getVerbIdsByTarget(ghost)).toEqual([])
  })

  it('verb ints stay resolvable after removeVerb (interning is append-only)', async () => {
    const sInt = BigInt(idMapper.getOrAssign('uuid-s'))
    const tInt = BigInt(idMapper.getOrAssign('uuid-t'))
    const verb = makeVerb(VERB_UUID_3, 'uuid-s', 'uuid-t')

    const verbInt = await index.addVerb(verb, sInt, tInt, 1n)
    // Persist the verb (vector + metadata — getVerb requires both) so
    // removeVerb can resolve its type for count updates.
    await storage.saveVerb({
      id: verb.id,
      vector: [],
      connections: new Map<number, Set<string>>(),
      verb: VerbType.RelatedTo,
      sourceId: verb.sourceId,
      targetId: verb.targetId
    })
    await storage.saveVerbMetadata(verb.id, {
      verb: VerbType.RelatedTo,
      createdAt: Date.now()
    })

    await index.removeVerb(verb.id, 2n)

    // Removed verb no longer appears in reads…
    expect(await index.getVerbIdsBySource(sInt)).toEqual([])
    // …but its int still reverse-resolves (append-only interning).
    expect(await index.verbIntsToIds([verbInt])).toEqual([VERB_UUID_3])
  })

  it('fails loudly when the entityIdMapper is not wired', async () => {
    const bare = new GraphAdjacencyIndex(storage)
    await expect(bare.getNeighbors(1n)).rejects.toThrow(/entityIdMapper not wired/)
    await bare.close()
  })
})

describe('Coordinator — relate/related end-to-end over the BigInt boundary', () => {
  let brain: Brainy

  beforeEach(async () => {
    brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
    await brain.init()
  })

  afterEach(async () => {
    await brain.close()
  })

  it('relate → related → neighbors round-trips (public API unchanged)', async () => {
    const personId = await brain.add({
      data: { name: 'Ada' },
      type: NounType.Person
    })
    const projectId = await brain.add({
      data: { name: 'Analytical Engine' },
      type: NounType.Thing
    })

    const relId = await brain.relate({
      from: personId,
      to: projectId,
      type: VerbType.WorksWith
    })

    // related resolves verb ints back to verb-id strings internally.
    const relations = await brain.related({ from: personId })
    expect(relations).toHaveLength(1)
    expect(relations[0].id).toBe(relId)
    expect(relations[0].to).toBe(projectId)

    // neighbors() routes through getNeighbors(bigint) + int → UUID mapping.
    const out = await brain.neighbors(personId, { direction: 'outgoing' })
    expect(out).toContain(projectId)

    const incoming = await brain.neighbors(projectId, { direction: 'incoming' })
    expect(incoming).toContain(personId)
  })

  it('relate feeds the verb-int warm cache; duplicate detection still works', async () => {
    const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
    const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })

    const first = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })

    // The AddToGraphIndexOperation callback recorded the verb int → id pair.
    const warmCache: Map<bigint, string> = (brain as any).verbIntWarmCache
    expect([...warmCache.values()]).toContain(first)

    // Duplicate check runs through getVerbIdsBySource(bigint) + resolution.
    const duplicate = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
    expect(duplicate).toBe(first)
  })

  it('unrelate removes the edge from BigInt-boundary reads', async () => {
    const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
    const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })

    const relId = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
    await brain.unrelate(relId)

    const relations = await brain.related({ from: a })
    expect(relations).toHaveLength(0)
  })

  it('neighbors() of an unknown UUID returns empty without error', async () => {
    const result = await brain.neighbors('00000000-0000-4000-8000-000000000000')
    expect(result).toEqual([])
  })

  it('relate() rejects a caller-supplied verb id with a teaching error (8.0 contract)', async () => {
    const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
    const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })

    // RelateParams has no `id` field — untyped callers passing one used to be
    // silently ignored. 8.0 teaches: verb ids are brainy-generated UUIDs.
    await expect(
      brain.relate({ from: a, to: b, type: VerbType.RelatedTo, id: 'my-custom-id' } as any)
    ).rejects.toThrow(/Verb ids are UUIDs by contract in 8\.0/)
  })
})
