/**
 * @module tests/integration/transact-forward-ref-graph
 * @description Regression for a consumer-reported native/JS parity bug:
 * `transact([{op:'add', id:X}, {op:'relate', to:X}])` — the platform's first
 * atomic add+relate consumer — threw in `EntityIdMapper.getOrAssign` at PLAN
 * time on the native id mapper (an entity added in the same batch does not
 * exist when the planner runs, so a strict mapper rightly refuses to assign;
 * the permissive JS mapper masked the bug — and silently leaked an int
 * assignment whenever the batch was later rejected at precommit).
 *
 * Fix: graph-index operations resolve endpoint ints at EXECUTE time (a lazy
 * thunk, mirroring the existing lazy `generationFn`), after the batch's add
 * operations have applied. These tests pin every forward-ref shape on the JS
 * path, including the JS-observable deferral proof: a REJECTED batch leaves
 * the id mapper untouched (pre-fix, plan-time resolution assigned ints for
 * entities that never came to exist). The native side is locked in by the
 * accelerator's own gate running this same shape.
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'

let seq = 0
const freshId = (): string =>
  `00000000-0000-4000-8000-${(++seq).toString(16).padStart(12, '0')}`

describe('transact() forward references into the graph index (native/JS parity)', () => {
  let dir: string
  let brain: any

  beforeEach(async () => {
    process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-fwd-ref-'))
    brain = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      dimensions: 384,
      silent: true
    })
    await brain.init()
  })

  afterEach(async () => {
    await brain.close()
    fs.rmSync(dir, { recursive: true, force: true })
  })

  it('CASE 1 (the exact repro): add + relate-to-that-add in ONE transact', async () => {
    const agent = await brain.add({ id: freshId(), data: 'agent probe', type: NounType.Person })
    const thread = freshId()

    const db = await brain.transact([
      { op: 'add', id: thread, data: 'thread PROBE-ONE', type: NounType.Thing },
      { op: 'relate', from: agent, to: thread, type: VerbType.RelatedTo }
    ])

    // The batch committed as one generation and the edge traverses.
    expect(db.generation).toBeGreaterThan(0)
    const related = await brain.related(agent)
    expect(related.map((r: any) => r.to)).toContain(thread)
  })

  it('both endpoints created in-batch: add A + add B + relate A→B', async () => {
    const a = freshId()
    const b = freshId()
    await brain.transact([
      { op: 'add', id: a, data: 'node A', type: NounType.Thing },
      { op: 'add', id: b, data: 'node B', type: NounType.Thing },
      { op: 'relate', from: a, to: b, type: VerbType.Contains }
    ])
    expect((await brain.related(a)).map((r: any) => r.to)).toContain(b)
  })

  it('bidirectional relate to an in-batch add: both edges traverse', async () => {
    const hub = await brain.add({ id: freshId(), data: 'hub', type: NounType.Thing })
    const spoke = freshId()
    await brain.transact([
      { op: 'add', id: spoke, data: 'spoke', type: NounType.Thing },
      { op: 'relate', from: hub, to: spoke, type: VerbType.RelatedTo, bidirectional: true }
    ])
    expect((await brain.related(hub)).map((r: any) => r.to)).toContain(spoke)
    expect((await brain.related(spoke)).map((r: any) => r.to)).toContain(hub)
  })

  it('add + relate + remove in ONE transact: the cascade covers the in-batch verb', async () => {
    const keeper = await brain.add({ id: freshId(), data: 'keeper', type: NounType.Thing })
    const doomed = freshId()
    await brain.transact([
      { op: 'add', id: doomed, data: 'doomed', type: NounType.Thing },
      { op: 'relate', from: keeper, to: doomed, type: VerbType.Contains },
      { op: 'remove', id: doomed }
    ])
    expect(await brain.get(doomed)).toBeNull()
    expect((await brain.related(keeper)).length).toBe(0)
  })

  it('CASE 2 control: the same two ops split across two transacts still work', async () => {
    const agent = await brain.add({ id: freshId(), data: 'agent two', type: NounType.Person })
    const thread = freshId()
    await brain.transact([{ op: 'add', id: thread, data: 'thread two', type: NounType.Thing }])
    await brain.transact([{ op: 'relate', from: agent, to: thread, type: VerbType.RelatedTo }])
    expect((await brain.related(agent)).map((r: any) => r.to)).toContain(thread)
  })

  it('a REJECTED forward-ref batch leaves the id mapper untouched (the deferral proof)', async () => {
    const existing = await brain.add({
      id: freshId(),
      data: 'cas anchor',
      type: NounType.Thing
    })
    const never = freshId()

    // Reject the batch via a stale per-op CAS — planning completes, the
    // commit precondition throws, nothing applies.
    await expect(
      brain.transact([
        { op: 'add', id: never, data: 'never exists', type: NounType.Thing },
        { op: 'relate', from: existing, to: never, type: VerbType.RelatedTo },
        { op: 'update', id: existing, metadata: { poke: 1 }, ifRev: 999 }
      ])
    ).rejects.toMatchObject({ name: 'RevisionConflictError' })

    // Nothing applied…
    expect(await brain.get(never)).toBeNull()
    // …and the id mapper was never asked to assign for the phantom entity —
    // pre-fix, plan-time resolution leaked an int here on every rejected batch.
    const idMapper = brain['metadataIndex'].getIdMapper()
    expect(idMapper.getInt(never)).toBeUndefined()

    // The same shape then succeeds cleanly on retry.
    await brain.transact([
      { op: 'add', id: never, data: 'exists now', type: NounType.Thing },
      { op: 'relate', from: existing, to: never, type: VerbType.RelatedTo }
    ])
    expect((await brain.related(existing)).map((r: any) => r.to)).toContain(never)
  })
})
