/**
 * @module tests/unit/brainy/graph-native-routing
 * @description Graph engine — NATIVE seam coverage. brain.graph.subgraph/export
 * route to a registered GraphAccelerationProvider and hydrate its columnar
 * `Subgraph` (node ints -> ids, depth alignment, edge verb-ints -> Relations).
 * In production that provider is cor's native engine, cross-layer-tested on
 * bxl9000; brainy CI never registers one, so graphSubgraphNative /
 * graphExportNative / hydrateNativeSubgraph + the provider-resolution + routing
 * were previously UNEXERCISED — a return-shape or hydration-alignment drift would
 * pass CI silently. This registers a faithful MOCK provider (returning a columnar
 * Subgraph built from the brain's REAL ints, so hydration resolves to real
 * entities/relations) to lock those paths.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'

/** A stateful mock GraphAccelerationProvider; the test sets the columnar payloads. */
function makeMockAccel(opts: { isInitialized?: boolean } = {}) {
  const calls = { traverse: 0, cursorOpen: 0, cursorNext: 0, cursorClose: 0, rank: 0, communities: 0, path: 0 }
  const state: {
    calls: typeof calls
    traverseResult: any
    cursorChunks: any[]
    rankResult: any
    communitiesResult: any
    pathResult: any
    lastTraverseSeeds: any
  } = {
    calls,
    traverseResult: null,
    cursorChunks: [],
    rankResult: null,
    communitiesResult: null,
    pathResult: null,
    lastTraverseSeeds: undefined
  }
  const empty = { nodeInts: new BigInt64Array(0), scores: new Float64Array(0) }
  const emptyCommunities = {
    nodeInts: new BigInt64Array(0),
    communityIds: new Uint32Array(0),
    communityCount: 0
  }
  const provider = {
    isInitialized: opts.isInitialized ?? true,
    traverse: async (seeds: any) => {
      calls.traverse++
      state.lastTraverseSeeds = seeds
      return state.traverseResult
    },
    edgesForNode: async () => state.traverseResult,
    graphCursorOpen: async () => {
      calls.cursorOpen++
      return 'mock-handle'
    },
    graphCursorNext: async () => {
      calls.cursorNext++
      const subgraph = state.cursorChunks.shift()
      return { subgraph, done: state.cursorChunks.length === 0 }
    },
    graphCursorClose: async () => {
      calls.cursorClose++
    },
    rank: async () => {
      calls.rank++
      return state.rankResult ?? empty
    },
    communities: async () => {
      calls.communities++
      return state.communitiesResult ?? emptyCommunities
    },
    path: async () => {
      calls.path++
      return state.pathResult
    },
    sample: async () => state.traverseResult,
    mostConnected: async () => empty
  }
  return { provider, state }
}

describe('brain.graph.* native routing + columnar hydration (native seam)', () => {
  let brain: Brainy
  let mock: ReturnType<typeof makeMockAccel>
  let a: string, b: string, c: string

  // Build a faithful columnar Subgraph from the brain's REAL ints, so brainy's
  // hydration resolves the node ints to ids and the verb ints to Relations.
  async function realSubgraph(withUnresolvable = false) {
    const gei = (id: string): bigint => (brain as any).graphEntityInt(id)
    const gi = (brain as any).graphIndex
    const intA = gei(a), intB = gei(b), intC = gei(c)
    const vAB = (await gi.getVerbIdsBySource(intA))[0] as bigint // verb int a->b
    const vBC = (await gi.getVerbIdsBySource(intB))[0] as bigint // verb int b->c
    const nodeInts = [intA, intB, intC]
    const depths = [0, 1, 2]
    if (withUnresolvable) {
      nodeInts.push(999_999_999n) // a never-assigned int (simulates a deleted/unknown node)
      depths.push(3)
    }
    return {
      nodes: BigInt64Array.from(nodeInts),
      nodeDepth: Uint8Array.from(depths),
      edgeSources: BigInt64Array.from([intA, intB]),
      edgeTargets: BigInt64Array.from([intB, intC]),
      edgeVerbInts: BigInt64Array.from([vAB, vBC]),
      edgeTypes: Uint16Array.from([0, 0]),
      truncated: false
    }
  }

  beforeEach(async () => {
    mock = makeMockAccel()
    brain = new Brainy(createTestConfig())
    brain.use({
      name: 'mock-graph-accel',
      activate: async (ctx: any) => {
        ctx.registerProvider('graphAcceleration', mock.provider)
        return true
      }
    } as any)
    await brain.init()
    a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
    b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
    c = await brain.add({ type: NounType.Project, subtype: 'milestone', data: 'C' })
    await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' })
    await brain.relate({ from: b, to: c, type: VerbType.ParticipatesIn, subtype: 'assignment' })
  })

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

  it('subgraph() routes to the native provider and hydrates the columnar result', async () => {
    mock.state.traverseResult = await realSubgraph()
    const view = await brain.graph.subgraph(a, { depth: 2 })

    expect(mock.state.calls.traverse).toBe(1) // native path, not the TS fallback
    const byId = new Map(view.nodes.map((n) => [n.id, n]))
    expect(new Set(byId.keys())).toEqual(new Set([a, b, c])) // node int -> id
    expect(byId.get(a)?.depth).toBe(0)
    expect(byId.get(c)?.depth).toBe(2) // depth column aligned to node column
    expect(byId.get(c)?.type).toBe(NounType.Project) // node type hydrated via batchGet
    const pairs = view.edges.map((e) => `${e.from}->${e.to}`).sort()
    expect(pairs).toEqual([`${a}->${b}`, `${b}->${c}`].sort()) // verb int -> Relation
  })

  it('keeps node<->depth alignment when a node int does not resolve (deleted/unknown)', async () => {
    mock.state.traverseResult = await realSubgraph(true) // appends an unresolvable int at depth 3
    const view = await brain.graph.subgraph(a, { depth: 3 })

    const byId = new Map(view.nodes.map((n) => [n.id, n]))
    // The 3 real nodes keep their CORRECT depths — the unresolvable int is dropped,
    // not collapsed into the array (which would shift every later depth).
    expect(byId.get(a)?.depth).toBe(0)
    expect(byId.get(b)?.depth).toBe(1)
    expect(byId.get(c)?.depth).toBe(2)
    expect(view.nodes.length).toBe(3)
  })

  it('export() routes to the native graph cursor, hydrates chunks, and always closes', async () => {
    mock.state.cursorChunks = [await realSubgraph()]
    const chunks: any[] = []
    for await (const v of brain.graph.export()) chunks.push(v)

    expect(mock.state.calls.cursorOpen).toBe(1)
    expect(mock.state.calls.cursorClose).toBe(1) // cursor released even on normal completion
    const nodes = new Set(chunks.flatMap((c) => c.nodes.map((n: any) => n.id)))
    expect(nodes).toEqual(new Set([a, b, c]))
    const edges = chunks.flatMap((c) => c.edges.map((e: any) => `${e.from}->${e.to}`)).sort()
    expect(edges).toEqual([`${a}->${b}`, `${b}->${c}`].sort())
  })

  it('resolves a provider registered as a FACTORY (storage) => provider, not just an instance', async () => {
    const m = makeMockAccel()
    const fb = new Brainy(createTestConfig())
    fb.use({
      name: 'mock-graph-accel-factory',
      activate: async (ctx: any) => {
        ctx.registerProvider('graphAcceleration', () => m.provider) // factory form
        return true
      }
    } as any)
    await fb.init()
    const x = await fb.add({ type: NounType.Person, subtype: 'employee', data: 'X' })
    const y = await fb.add({ type: NounType.Person, subtype: 'employee', data: 'Y' })
    await fb.relate({ from: x, to: y, type: VerbType.RelatedTo, subtype: 'colleague' })
    const gei = (id: string): bigint => (fb as any).graphEntityInt(id)
    const gi = (fb as any).graphIndex
    const ix = gei(x), iy = gei(y)
    const vxy = (await gi.getVerbIdsBySource(ix))[0] as bigint
    m.state.traverseResult = {
      nodes: BigInt64Array.from([ix, iy]),
      nodeDepth: Uint8Array.from([0, 1]),
      edgeSources: BigInt64Array.from([ix]),
      edgeTargets: BigInt64Array.from([iy]),
      edgeVerbInts: BigInt64Array.from([vxy]),
      edgeTypes: Uint16Array.from([0]),
      truncated: false
    }
    const view = await fb.graph.subgraph(x, { depth: 1 })
    expect(m.state.calls.traverse).toBe(1) // factory was invoked + provider routed
    expect(new Set(view.nodes.map((n) => n.id))).toEqual(new Set([x, y]))
    await fb.close()
  })

  it('rank() routes to the native provider and hydrates node ints -> ids, order preserved', async () => {
    const gei = (id: string): bigint => (brain as any).graphEntityInt(id)
    // Provider returns DESCENDING scores: c, then a, then b.
    mock.state.rankResult = {
      nodeInts: BigInt64Array.from([gei(c), gei(a), gei(b)]),
      scores: Float64Array.from([0.5, 0.3, 0.2])
    }
    const ranked = await brain.graph.rank()

    expect(mock.state.calls.rank).toBe(1) // native path, not the TS PageRank fallback
    expect(ranked.map((r) => r.id)).toEqual([c, a, b]) // int -> id, provider order kept
    expect(ranked[0].score).toBe(0.5)

    const top2 = await brain.graph.rank({ topK: 2 })
    expect(top2.map((r) => r.id)).toEqual([c, a])
  })

  it('communities() routes to the native provider and buckets ids by community label', async () => {
    const gei = (id: string): bigint => (brain as any).graphEntityInt(id)
    // a,b in community 0; c alone in community 1.
    mock.state.communitiesResult = {
      nodeInts: BigInt64Array.from([gei(a), gei(b), gei(c)]),
      communityIds: Uint32Array.from([0, 0, 1]),
      communityCount: 2
    }
    const { groups, count } = await brain.graph.communities()

    expect(mock.state.calls.communities).toBe(1) // native path, not the TS fallback
    expect(count).toBe(2)
    const asSets = groups.map((g) => new Set(g))
    expect(asSets).toContainEqual(new Set([a, b]))
    expect(asSets).toContainEqual(new Set([c]))
    expect(groups[0]).toHaveLength(2) // largest group first
  })

  it('path() routes to the native provider and hydrates node + verb ints', async () => {
    const gei = (id: string): bigint => (brain as any).graphEntityInt(id)
    const gi = (brain as any).graphIndex
    const vAB = (await gi.getVerbIdsBySource(gei(a)))[0] as bigint
    const vBC = (await gi.getVerbIdsBySource(gei(b)))[0] as bigint
    mock.state.pathResult = {
      nodeInts: BigInt64Array.from([gei(a), gei(b), gei(c)]),
      edgeVerbInts: BigInt64Array.from([vAB, vBC]),
      cost: 2
    }
    const route = await brain.graph.path(a, c)

    expect(mock.state.calls.path).toBe(1) // native path, not the TS BFS/Dijkstra fallback
    expect(route?.nodes).toEqual([a, b, c]) // node ints -> ids
    expect(route?.relationships).toHaveLength(2) // verb ints -> verb-id strings
    expect(route?.cost).toBe(2)
  })

  it('path() returns null when the native provider reports unreachable', async () => {
    mock.state.pathResult = null
    expect(await brain.graph.path(a, c)).toBeNull()
    expect(mock.state.calls.path).toBe(1)
  })

  it('subgraph(query) forwards the metadata universe to traverse as an OpaqueIdSet (query→expand #61)', async () => {
    // The native metadata index would return its roaring filter result as a Buffer;
    // stub that producer and assert it reaches traverse WITHOUT id materialization.
    const sentinel = new Uint8Array([0x01, 0x02, 0x03])
    ;(brain as any).metadataIndex.getIdSetForFilter = async () => sentinel
    mock.state.traverseResult = await realSubgraph()

    await brain.graph.subgraph({ type: NounType.Person }, { depth: 1 })

    expect(mock.state.calls.traverse).toBe(1)
    // The opaque Buffer is the traverse seed argument — passed straight through.
    expect(mock.state.lastTraverseSeeds).toBe(sentinel)
  })

  it('subgraph(query) materializes seeds via find() when no opaque producer exists', async () => {
    // No getIdSetForFilter on the (real) metadata index → general path: find() runs,
    // its matched ids resolve to entity ints, and those seed the native traverse.
    mock.state.traverseResult = await realSubgraph()

    await brain.graph.subgraph({ type: NounType.Person }, { depth: 1 })

    expect(mock.state.calls.traverse).toBe(1)
    expect(Array.isArray(mock.state.lastTraverseSeeds)).toBe(true) // bigint[], not a Buffer
    expect(typeof (mock.state.lastTraverseSeeds as unknown[])[0]).toBe('bigint')
  })

  // Readiness gate (cor boundary-audit item #1): the native engine reports
  // isInitialized=false during its cold-start/rebuild window. Brainy must route to the
  // pure-TS path then — NOT call the not-ready provider (which would throw) — across the
  // whole brain.graph.* surface, and re-engage the native path once it flips true.
  it('routes to the TS fallback (never calls the provider) while accel.isInitialized is false', async () => {
    const notReady = makeMockAccel({ isInitialized: false })
    const b = new Brainy(createTestConfig())
    b.use({
      name: 'mock-graph-accel-not-ready',
      activate: async (ctx: any) => {
        ctx.registerProvider('graphAcceleration', notReady.provider)
        return true
      }
    } as any)
    await b.init()
    const p = await b.add({ type: NounType.Person, subtype: 'employee', data: 'P' })
    const q = await b.add({ type: NounType.Person, subtype: 'employee', data: 'Q' })
    await b.relate({ from: p, to: q, type: VerbType.RelatedTo })

    // Every brain.graph.* route + the query→expand fusion must NOT throw and must serve
    // from the TS fallback while the engine is cold.
    const sub = await b.graph.subgraph(p, { depth: 1 })
    const ranked = await b.graph.rank()
    const comm = await b.graph.communities()
    const route = await b.graph.path(p, q)
    const fused = await b.graph.subgraph({ type: NounType.Person }, { depth: 1 })

    // The provider was never touched — all served by the JS fallback.
    expect(notReady.state.calls.traverse).toBe(0)
    expect(notReady.state.calls.rank).toBe(0)
    expect(notReady.state.calls.communities).toBe(0)
    expect(notReady.state.calls.path).toBe(0)
    // And the TS fallback returned real answers (not throws / empties from a cold engine).
    expect(sub.nodes.some((n) => n.id === p)).toBe(true)
    expect(ranked.length).toBeGreaterThan(0)
    expect(comm.count).toBeGreaterThan(0)
    expect(route?.nodes).toEqual([p, q])
    expect(fused.nodes.some((n) => n.id === p)).toBe(true)
    await b.close()
  })
})
