/**
 * @module tests/unit/brainy/graph-analytics
 * @description Graph analytics surface — `brain.graph.rank()` (PageRank importance),
 * `brain.graph.communities()` (connected-component grouping), and
 * `brain.graph.path()` (best route). These exercise the pure-TS fallbacks (no native
 * GraphAccelerationProvider is registered in CI); a native provider answers the same
 * INTENT (which nodes matter most / which things group / best route) and returns the
 * same public shapes, cross-layer-tested against the provider.
 */

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('brain.graph.communities()', () => {
  let brain: Brainy
  let a: string, b: string, c: string, d: string, e: string, f: string

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    await brain.init()
    // Two disjoint clusters + one isolated node:
    //   cluster 1:  a → b → c
    //   cluster 2:  d → e
    //   isolated:   f (no edges)
    a = await brain.add({ type: NounType.Person, data: 'A' })
    b = await brain.add({ type: NounType.Person, data: 'B' })
    c = await brain.add({ type: NounType.Person, data: 'C' })
    d = await brain.add({ type: NounType.Person, data: 'D' })
    e = await brain.add({ type: NounType.Person, data: 'E' })
    f = await brain.add({ type: NounType.Person, data: 'F' })
    await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
    await brain.relate({ from: b, to: c, type: VerbType.RelatedTo })
    await brain.relate({ from: d, to: e, type: VerbType.RelatedTo })
  })

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

  it('partitions the graph into connected groups (isolated nodes are singletons)', async () => {
    const { groups, count } = await brain.graph.communities()
    expect(count).toBe(3)
    const asSets = groups.map((g) => new Set(g))
    expect(asSets).toContainEqual(new Set([a, b, c]))
    expect(asSets).toContainEqual(new Set([d, e]))
    expect(asSets).toContainEqual(new Set([f]))
  })

  it('orders groups largest-first', async () => {
    const { groups } = await brain.graph.communities()
    for (let i = 1; i < groups.length; i++) {
      expect(groups[i - 1].length).toBeGreaterThanOrEqual(groups[i].length)
    }
    expect(groups[0]).toHaveLength(3) // the a-b-c cluster
  })

  it('directed mode groups by strong connectivity (a→b→c is NOT mutually reachable)', async () => {
    // Undirected: {a,b,c} is one group. Directed: no cycle, so each is its own SCC.
    const undirected = await brain.graph.communities()
    expect(undirected.groups).toContainEqual(expect.arrayContaining([a, b, c]))

    const directed = await brain.graph.communities({ directed: true })
    // a,b,c split into singletons; d,e split too; f stays singleton → 6 groups.
    expect(directed.count).toBe(6)
    expect(directed.groups.every((g) => g.length === 1)).toBe(true)
  })

  it('directed mode keeps a cycle together as one strongly-connected community', async () => {
    // x → y → z → x is a cycle: mutually reachable → one SCC even when directed.
    const x = await brain.add({ type: NounType.Concept, data: 'X' })
    const y = await brain.add({ type: NounType.Concept, data: 'Y' })
    const z = await brain.add({ type: NounType.Concept, data: 'Z' })
    await brain.relate({ from: x, to: y, type: VerbType.RelatedTo })
    await brain.relate({ from: y, to: z, type: VerbType.RelatedTo })
    await brain.relate({ from: z, to: x, type: VerbType.RelatedTo })

    const directed = await brain.graph.communities({ directed: true })
    const asSets = directed.groups.map((g) => new Set(g))
    expect(asSets).toContainEqual(new Set([x, y, z]))
  })

  it('excludes internal-visibility edges by default; includeInternal re-links', async () => {
    // A hidden edge bridging the two clusters is invisible by default.
    await brain.relate({ from: c, to: d, type: VerbType.RelatedTo, visibility: 'internal' })

    const hidden = await brain.graph.communities()
    expect(hidden.count).toBe(3) // bridge hidden → still 3 groups

    const surfaced = await brain.graph.communities({ includeInternal: true })
    const asSets = surfaced.groups.map((g) => new Set(g))
    expect(asSets).toContainEqual(new Set([a, b, c, d, e])) // bridge merges the clusters
  })
})

describe('brain.graph.rank()', () => {
  let brain: Brainy
  let hub: string, a: string, b: string, c: string

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    await brain.init()
    // a, b, c all point at hub → hub is the most "important" node.
    hub = await brain.add({ type: NounType.Person, data: 'HUB' })
    a = await brain.add({ type: NounType.Person, data: 'A' })
    b = await brain.add({ type: NounType.Person, data: 'B' })
    c = await brain.add({ type: NounType.Person, data: 'C' })
    await brain.relate({ from: a, to: hub, type: VerbType.RelatedTo })
    await brain.relate({ from: b, to: hub, type: VerbType.RelatedTo })
    await brain.relate({ from: c, to: hub, type: VerbType.RelatedTo })
  })

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

  it('ranks the most-pointed-to node highest, descending', async () => {
    const ranked = await brain.graph.rank()
    expect(ranked).toHaveLength(4)
    expect(ranked[0].id).toBe(hub)
    for (let i = 1; i < ranked.length; i++) {
      expect(ranked[i - 1].score).toBeGreaterThanOrEqual(ranked[i].score)
    }
  })

  it('scores form a probability distribution (PageRank sums to ~1)', async () => {
    const ranked = await brain.graph.rank()
    const total = ranked.reduce((sum, r) => sum + r.score, 0)
    expect(total).toBeCloseTo(1, 5)
  })

  it('topK returns only the K highest', async () => {
    const top1 = await brain.graph.rank({ topK: 1 })
    expect(top1).toHaveLength(1)
    expect(top1[0].id).toBe(hub)
  })

  it('returns [] on an empty graph', async () => {
    const empty = new Brainy(createTestConfig())
    await empty.init()
    expect(await empty.graph.rank()).toEqual([])
    await empty.close()
  })
})

describe('brain.graph.path()', () => {
  let brain: Brainy
  let a: string, b: string, c: string, d: string, isolated: string

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    await brain.init()
    // Chain a → b → c → d (light edges), plus a heavy direct shortcut a → d.
    // weight is a 0–1 connection strength; `by:'weight'` minimizes summed weight.
    a = await brain.add({ type: NounType.Person, data: 'A' })
    b = await brain.add({ type: NounType.Person, data: 'B' })
    c = await brain.add({ type: NounType.Person, data: 'C' })
    d = await brain.add({ type: NounType.Person, data: 'D' })
    isolated = await brain.add({ type: NounType.Person, data: 'ISO' })
    await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, weight: 0.1 })
    await brain.relate({ from: b, to: c, type: VerbType.RelatedTo, weight: 0.1 })
    await brain.relate({ from: c, to: d, type: VerbType.RelatedTo, weight: 0.1 })
    await brain.relate({ from: a, to: d, type: VerbType.RelatedTo, weight: 0.9 }) // direct but heavy
  })

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

  it('finds the fewest-hops route by default', async () => {
    const route = await brain.graph.path(a, d)
    expect(route).not.toBeNull()
    // a → d direct edge is 1 hop, the shortest.
    expect(route?.nodes).toEqual([a, d])
    expect(route?.relationships).toHaveLength(1)
    expect(route?.cost).toBe(1)
  })

  it('by:"weight" prefers the lighter multi-hop route over the heavy shortcut', async () => {
    const route = await brain.graph.path(a, d, { by: 'weight' })
    expect(route?.nodes).toEqual([a, b, c, d]) // 0.1+0.1+0.1 = 0.3 < 0.9
    expect(route?.relationships).toHaveLength(3)
    expect(route?.cost).toBeCloseTo(0.3, 6)
  })

  it('returns a zero-length route from a node to itself', async () => {
    const route = await brain.graph.path(a, a)
    expect(route).toEqual({ nodes: [a], relationships: [], cost: 0 })
  })

  it('returns null when the target is unreachable', async () => {
    expect(await brain.graph.path(a, isolated)).toBeNull()
  })

  it('direction:"out" cannot walk backwards up the chain', async () => {
    // d has no outgoing edges, so d → a is unreachable following only out-edges.
    expect(await brain.graph.path(d, a, { direction: 'out' })).toBeNull()
    // …but 'both' finds it.
    const both = await brain.graph.path(d, a, { direction: 'both' })
    expect(both?.nodes[0]).toBe(d)
    expect(both?.nodes[both.nodes.length - 1]).toBe(a)
  })

  it('maxDepth abandons routes that are too long', async () => {
    // The light route is 3 hops; cap at 1 hop → only the heavy direct shortcut.
    const capped = await brain.graph.path(a, d, { by: 'weight', maxDepth: 1 })
    expect(capped?.nodes).toEqual([a, d])
    expect(capped?.cost).toBeCloseTo(0.9, 6)
  })
})
