/**
 * @module tests/unit/brainy/related-node-bidirectional
 * @description Graph-perf #4 (8.0): `related({ node })` — the one-call,
 * both-direction "all edges on a noun" query.
 *
 * `related({ from })` / `related({ to })` each return one direction; `related({ node })`
 * unions both (edges where the node is source OR target), deduped, in a single
 * O(degree) call — the indexed counterpart of merging two `related()` calls by hand.
 * It composes with `type` / `subtype` / visibility filters and is mutually exclusive
 * with `from` / `to`.
 */

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'

describe('related({ node }) both-direction incident edges (graph-perf #4)', () => {
  let brain: Brainy
  let a: string, b: string, c: string, d: string

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    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.Person, subtype: 'employee', data: 'C' })
    d = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'D' })
    // a's incident edges: a→b (out), c→a (in), a→d (out, ReportsTo), c→a internal (in, hidden)
    await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague' })
    await brain.relate({ from: c, to: a, type: VerbType.RelatedTo, subtype: 'colleague' })
    await brain.relate({ from: a, to: d, type: VerbType.ReportsTo, subtype: 'direct' })
  })

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

  it('returns edges incident in BOTH directions (source OR target)', async () => {
    const edges = await brain.related({ node: a })
    expect(edges).toHaveLength(3)
    // Both out-edges (a is `from`) and the in-edge (a is `to`) are present.
    const pairs = edges.map((e) => `${e.from}->${e.to}`).sort()
    expect(pairs).toEqual([`${a}->${b}`, `${a}->${d}`, `${c}->${a}`].sort())
  })

  it('matches the union of related({from}) + related({to})', async () => {
    const out = await brain.related({ from: a })
    const inc = await brain.related({ to: a })
    const expected = new Set([...out, ...inc].map((e) => e.id))
    const node = await brain.related({ node: a })
    expect(new Set(node.map((e) => e.id))).toEqual(expected)
  })

  it('dedupes a self-loop (an edge where the node is both source and target)', async () => {
    await brain.relate({ from: a, to: a, type: VerbType.RelatedTo, subtype: 'self' })
    const edges = await brain.related({ node: a })
    const selfLoops = edges.filter((e) => e.from === a && e.to === a)
    expect(selfLoops).toHaveLength(1) // appears once, not twice
  })

  it('composes with a type filter (both directions)', async () => {
    const related = await brain.related({ node: a, type: VerbType.RelatedTo })
    // a→b and c→a are RelatedTo; a→d is ReportsTo (excluded).
    expect(related.map((e) => `${e.from}->${e.to}`).sort()).toEqual(
      [`${a}->${b}`, `${c}->${a}`].sort()
    )
  })

  it('excludes internal edges by default, includes them with includeInternal', async () => {
    const e = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'E' })
    await brain.relate({ from: e, to: a, type: VerbType.RelatedTo, subtype: 'colleague', visibility: 'internal' })

    const visible = await brain.related({ node: a })
    expect(visible.some((r) => r.from === e)).toBe(false)

    const all = await brain.related({ node: a, includeInternal: true })
    const internalEdge = all.find((r) => r.from === e)
    expect(internalEdge?.visibility).toBe('internal')
  })

  it("throws when 'node' is combined with 'from' or 'to'", async () => {
    await expect(brain.related({ node: a, from: b })).rejects.toThrow(/mutually exclusive/)
    await expect(brain.related({ node: a, to: b })).rejects.toThrow(/mutually exclusive/)
  })
})
