/**
 * Generation threading through the graph-index transaction operations.
 *
 * The graph time-travel contract (AddVerb/RemoveVerb gain a `generation`) is
 * load-bearing for native providers: `db.asOf(g)` graph hops resolve historical
 * endpoints from a per-generation edge chain the provider stamps at write time.
 * Brainy's JS index ignores the value, so a behavioural test against it proves
 * nothing — these tests use a spy provider to assert the operation layer:
 *   1. passes the resolved generation to addVerb/removeVerb,
 *   2. resolves the generation at EXECUTE time, not construction (the batch
 *      generation is only assigned once the commit begins executing), and
 *   3. reuses one generation across an operation's forward + rollback halves
 *      (so an add and its undo reference the same watermark in the chain).
 */
import { describe, it, expect } from 'vitest'
import {
  AddToGraphIndexOperation,
  RemoveFromGraphIndexOperation
} from '../../../src/transaction/operations/IndexOperations.js'
import type { GraphIndexProvider } from '../../../src/plugin.js'
import type { GraphVerb } from '../../../src/coreTypes.js'
import { VerbType } from '../../../src/types/graphTypes.js'

/** A graph verb with just the fields the operations touch. */
function makeVerb(id: string): GraphVerb {
  return {
    id,
    sourceId: 'src-uuid',
    targetId: 'tgt-uuid',
    type: VerbType.RelatedTo,
    sourceInt: 10n,
    targetInt: 20n
  } as unknown as GraphVerb
}

/** Records every (method, generation) the operations push to the provider. */
function makeSpyProvider(): {
  provider: GraphIndexProvider
  calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }>
} {
  const calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }> = []
  const provider = {
    async addVerb(verb: GraphVerb, _s: bigint, _t: bigint, generation: bigint): Promise<bigint> {
      calls.push({ method: 'addVerb', generation, verbId: verb.id })
      return 99n // interned verb int
    },
    async removeVerb(verbId: string, generation: bigint): Promise<void> {
      calls.push({ method: 'removeVerb', generation, verbId })
    }
  } as unknown as GraphIndexProvider
  return { provider, calls }
}

describe('Graph index operations — generation threading', () => {
  it('AddToGraphIndexOperation stamps addVerb (and its rollback removeVerb) at the resolved generation', async () => {
    const { provider, calls } = makeSpyProvider()
    const verb = makeVerb('verb-1')
    const op = new AddToGraphIndexOperation(provider, verb, { sourceInt: 10n, targetInt: 20n }, () => 7n)

    const rollback = await op.execute()
    expect(calls).toEqual([{ method: 'addVerb', generation: 7n, verbId: 'verb-1' }])

    await rollback()
    expect(calls[1]).toEqual({ method: 'removeVerb', generation: 7n, verbId: 'verb-1' })
  })

  it('RemoveFromGraphIndexOperation stamps removeVerb (and its rollback addVerb) at the resolved generation', async () => {
    const { provider, calls } = makeSpyProvider()
    const verb = makeVerb('verb-2')
    const op = new RemoveFromGraphIndexOperation(provider, verb, { sourceInt: 10n, targetInt: 20n }, () => 12n)

    const rollback = await op.execute()
    expect(calls).toEqual([{ method: 'removeVerb', generation: 12n, verbId: 'verb-2' }])

    await rollback()
    expect(calls[1]).toEqual({ method: 'addVerb', generation: 12n, verbId: 'verb-2' })
  })

  it('resolves the generation at EXECUTE time, not at construction', async () => {
    // The generation store assigns the batch generation only once the commit
    // begins executing — after the operations are built. The thunk must be read
    // then, so a value that changes between construction and execute is observed
    // at its execute-time value.
    const { provider, calls } = makeSpyProvider()
    let current = 1n
    const op = new AddToGraphIndexOperation(provider, makeVerb('verb-3'), { sourceInt: 10n, targetInt: 20n }, () => current)

    current = 42n // store assigns the batch generation after the op is built
    await op.execute()

    expect(calls[0].generation).toBe(42n)
  })

  it('still surfaces the provider verb int through onVerbInt after the signature change', async () => {
    const { provider } = makeSpyProvider()
    let seen: bigint | undefined
    const op = new AddToGraphIndexOperation(
      provider,
      makeVerb('verb-4'),
      { sourceInt: 10n, targetInt: 20n },
      () => 5n,
      (verbInt) => {
        seen = verbInt
      }
    )

    await op.execute()
    expect(seen).toBe(99n)
  })
})
