/**
 * @module tests/integration/id-normalization
 * @description Correctness gate for the 8.0 transparent entity-id normalization
 * layer (#18). A caller may use a natural string key (e.g. `'user-1'`) anywhere
 * an id is accepted; it is mapped to a STABLE UUID (`v5('user-1')`) on creation
 * AND on every lookup, so it round-trips transparently while the engine only
 * ever sees a UUID. The caller's original string is preserved under
 * {@link ORIGINAL_ID_KEY} in the stored entity metadata. A real UUID passes
 * through untouched (no `_originalId`).
 *
 * The layer is ALL-OR-NOTHING: every creation AND lookup path must normalize
 * identically, or round-trips break. These tests prove the contract holds
 * end-to-end across `add` / `get` / `update` / `remove` / `relate` / `related`
 * / `find({ connected })` / `transact` / `addMany` / `relateMany`, plus
 * determinism (same key → same UUID, upsert not duplicate), UUID passthrough,
 * and the v7 defaults for no-id `add()` and `newId()`.
 *
 * All entities carry explicit 384-dim vectors so no test invokes the embedder.
 */

import { describe, it, expect } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'
import { v5, v7, isUUID } from '../../src/universal/uuid.js'
import { ORIGINAL_ID_KEY } from '../../src/utils/idNormalization.js'

/** Deterministic 384-dim vector so no test ever invokes the embedder. */
function vec(seed: number): number[] {
  return Array.from({ length: 384 }, (_, i) => ((seed * 31 + i * 7) % 100) / 100)
}

/** Fresh in-memory brain (no embedder, no subtype enforcement). */
async function makeBrain(): Promise<Brainy> {
  const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
  await brain.init()
  return brain
}

describe('id normalization — transparent string-key round-trips', () => {
  it('1. add() returns v5(key); get(key) and get(returnedId) both resolve; _originalId preserved', async () => {
    const brain = await makeBrain()

    const returnedId = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })

    // The returned id is the stable v5 mapping, a real UUID.
    expect(returnedId).toBe(v5('user-1'))
    expect(isUUID(returnedId)).toBe(true)

    // get() by the natural key AND by the canonical id both resolve.
    const byKey = await brain.get('user-1')
    const byId = await brain.get(returnedId)
    expect(byKey).not.toBeNull()
    expect(byId).not.toBeNull()
    expect(byKey!.id).toBe(returnedId)
    expect(byId!.id).toBe(returnedId)

    // The caller's original string is surfaced under ORIGINAL_ID_KEY.
    expect(byKey!.metadata[ORIGINAL_ID_KEY]).toBe('user-1')
  })

  it('2. relate() by string keys; related(key) and related({from:key}) return the edge to v5(toKey)', async () => {
    const brain = await makeBrain()

    await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
    await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })

    await brain.relate({ from: 'user-1', to: 'doc-1', type: VerbType.Creates })

    const expectedTo = v5('doc-1')

    const viaString = await brain.related('user-1')
    expect(viaString.length).toBe(1)
    expect(viaString[0].from).toBe(v5('user-1'))
    expect(viaString[0].to).toBe(expectedTo)

    const viaFrom = await brain.related({ from: 'user-1' })
    expect(viaFrom.length).toBe(1)
    expect(viaFrom[0].to).toBe(expectedTo)

    // And the inbound anchor resolves too.
    const viaTo = await brain.related({ to: 'doc-1' })
    expect(viaTo.length).toBe(1)
    expect(viaTo[0].from).toBe(v5('user-1'))
  })

  it('3. update() by string key reflects on get(key)', async () => {
    const brain = await makeBrain()

    await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { role: 'admin' } })
    await brain.update({ id: 'user-1', metadata: { role: 'owner' } })

    const e = await brain.get('user-1')
    expect(e).not.toBeNull()
    expect((e!.metadata as any).role).toBe('owner')
    // Original id still surfaced after update.
    expect(e!.metadata[ORIGINAL_ID_KEY]).toBe('user-1')
  })

  it('4. remove() by string key deletes; get(key) is null', async () => {
    const brain = await makeBrain()

    await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
    expect(await brain.get('user-1')).not.toBeNull()

    await brain.remove('user-1')
    expect(await brain.get('user-1')).toBeNull()
    // The canonical id is gone too.
    expect(await brain.get(v5('user-1'))).toBeNull()
  })

  it('5. find({ connected: { from: key } }) resolves the anchor key', async () => {
    const brain = await makeBrain()

    await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
    await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })
    await brain.relate({ from: 'user-1', to: 'doc-1', type: VerbType.Creates })

    const results = await brain.find({ connected: { from: 'user-1' } })
    const ids = results.map((r) => r.id)
    expect(ids).toContain(v5('doc-1'))
  })

  it('6. transact() add+relate by string keys round-trips with consistent canonical ids', async () => {
    const brain = await makeBrain()

    // Seed user-1 so the relate op has a target to point at.
    await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })

    const db = await brain.transact([
      { op: 'add', id: 'u-2', vector: vec(3), type: NounType.Person },
      { op: 'relate', from: 'u-2', to: 'user-1', type: VerbType.RelatedTo }
    ])

    // Receipt ids resolve to the canonical mappings.
    expect(db.receipt.ids[0]).toBe(v5('u-2'))

    // get('u-2') works and surfaces the original id.
    const u2 = await brain.get('u-2')
    expect(u2).not.toBeNull()
    expect(u2!.id).toBe(v5('u-2'))
    expect(u2!.metadata[ORIGINAL_ID_KEY]).toBe('u-2')

    // The relation links the right canonical ids.
    const rels = await brain.related('u-2')
    expect(rels.length).toBe(1)
    expect(rels[0].from).toBe(v5('u-2'))
    expect(rels[0].to).toBe(v5('user-1'))
  })

  it('7. addMany() + relateMany() with string ids round-trip', async () => {
    const brain = await makeBrain()

    const added = await brain.addMany({
      items: [
        { id: 'a-1', vector: vec(10), type: NounType.Person },
        { id: 'b-1', vector: vec(11), type: NounType.Document }
      ]
    })
    expect(added.successful).toContain(v5('a-1'))
    expect(added.successful).toContain(v5('b-1'))

    const relIds = await brain.relateMany({
      items: [{ from: 'a-1', to: 'b-1', type: VerbType.Creates }]
    })
    expect(relIds.length).toBe(1)

    const a1 = await brain.get('a-1')
    expect(a1).not.toBeNull()
    expect(a1!.metadata[ORIGINAL_ID_KEY]).toBe('a-1')

    const rels = await brain.related('a-1')
    expect(rels.length).toBe(1)
    expect(rels[0].to).toBe(v5('b-1'))
  })

  it('8. determinism: same key maps to same UUID — two adds upsert ONE entity, not two', async () => {
    const brain = await makeBrain()

    const id1 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 1 } })
    const id2 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 2 } })

    // Stable mapping across calls.
    expect(id1).toBe(id2)
    expect(id1).toBe(v5('user-1'))

    // Only ONE entity exists (the second add overwrote the first).
    const count = await brain.getNounCount()
    expect(count).toBe(1)

    const e = await brain.get('user-1')
    expect((e!.metadata as any).n).toBe(2)
  })

  it('9. valid-UUID passthrough: a real UUID is kept verbatim with NO _originalId', async () => {
    const brain = await makeBrain()

    const realUuid = v7()
    const returnedId = await brain.add({ id: realUuid, vector: vec(5), type: NounType.Thing })

    expect(returnedId).toBe(realUuid)

    const e = await brain.get(realUuid)
    expect(e).not.toBeNull()
    expect(e!.id).toBe(realUuid)
    expect(e!.metadata[ORIGINAL_ID_KEY]).toBeUndefined()
  })

  it('10. no-id add() mints a v7; newId() mints a v7', async () => {
    const brain = await makeBrain()

    const autoId = await brain.add({ vector: vec(6), type: NounType.Thing })
    expect(isUUID(autoId)).toBe(true)
    // v7 carries version nibble '7' at the canonical position.
    expect(autoId[14]).toBe('7')

    const minted = brain.newId()
    expect(isUUID(minted)).toBe(true)
    expect(minted[14]).toBe('7')

    // The auto-id entity round-trips by its returned canonical id.
    const e = await brain.get(autoId)
    expect(e).not.toBeNull()
    expect(e!.metadata[ORIGINAL_ID_KEY]).toBeUndefined()
  })
})
