/**
 * Function-as-value capture tests (#756) — registration-linking for callbacks.
 *
 * A function name used as a VALUE (passed as an argument, assigned to a
 * field/function pointer, placed in a struct/object initializer or function
 * table) must produce a `references` edge from the registration site to the
 * function, so `callers`/`impact` surface where a callback is wired up.
 *
 * Safety properties verified here, per the dynamic-dispatch discipline
 * ("a wrong edge is worse than none"):
 *  - decoy: an ambiguous cross-file name (no import, ≥2 definitions) → NO edge
 *  - same-file priority: a same-file definition beats a same-named decoy
 *  - kind filter: a class/variable passed as a value never gets a
 *    function-ref edge
 *  - self: a function passing itself → no self-loop
 *  - drain: all resolvable function_ref rows leave unresolved_refs (no
 *    batched-resolver runaway), and re-index is idempotent
 */

import { describe, it, expect, beforeAll, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import type { Edge } from '../src/types';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';

beforeAll(async () => {
  await initGrammars();
  await loadAllGrammars();
});

/** Incoming edges to `name`'s node that came from function-as-value capture. */
function fnRefEdgesInto(cg: CodeGraph, name: string): Edge[] {
  const targets = cg.getNodesByName(name);
  const edges: Edge[] = [];
  for (const t of targets) {
    for (const e of cg.getIncomingEdges(t.id)) {
      if (e.kind === 'references' && e.metadata?.fnRef === true) {
        edges.push(e);
      }
    }
  }
  return edges;
}

/** Names of the source nodes of the given edges, sorted. */
function sourceNames(cg: CodeGraph, edges: Edge[]): string[] {
  const names: string[] = [];
  for (const e of edges) {
    const n = cg.getNode(e.source);
    if (n) names.push(n.name);
  }
  return names.sort();
}

describe('Function-as-value capture (#756)', () => {
  let tmpDir: string | undefined;
  afterEach(() => {
    if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
    tmpDir = undefined;
  });

  it('C: registration sites produce references edges (the #756 scenario)', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-c-'));
    fs.writeFileSync(
      path.join(tmpDir, 'driver.c'),
      [
        'struct ops { void (*recv_cb)(int); void (*send_cb)(int); };',
        'typedef void (*cb_t)(int);',
        '',
        'static void my_recv_cb(int x) { (void)x; }',
        'static void my_send_cb(int x) { (void)x; }',
        '',
        'void register_handler(void (*cb)(int)) { cb(1); }',
        '',
        'void direct_caller(void) { my_recv_cb(5); }',
        '',
        'void arg_registrar(void) { register_handler(my_recv_cb); }',
        'void addr_registrar(void) { register_handler(&my_recv_cb); }',
        'void assign_registrar(struct ops *o) { o->recv_cb = my_recv_cb; }',
        '',
        'static struct ops global_ops = { .recv_cb = my_recv_cb, .send_cb = my_send_cb };',
        'static cb_t cb_table[] = { my_recv_cb, my_send_cb };',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      const intoRecv = fnRefEdgesInto(cg, 'my_recv_cb');
      expect(sourceNames(cg, intoRecv)).toEqual([
        'addr_registrar',
        'arg_registrar',
        'assign_registrar',
        'driver.c', // file-scope: designated init + positional table (deduped per source)
      ]);

      // The direct call is still a `calls` edge — unchanged by this feature.
      const recv = cg.getNodesByName('my_recv_cb')[0]!;
      const callEdges = cg
        .getIncomingEdges(recv.id)
        .filter((e) => e.kind === 'calls');
      expect(sourceNames(cg, callEdges)).toEqual(['direct_caller']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('TypeScript: arg / object / array / member / assignment forms', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-ts-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.ts'),
      [
        'export function targetCb(x: number): void { console.log(x); }',
        'function registerHandler(cb: (x: number) => void): void { cb(1); }',
        '',
        'export function argRegistrar(): void { registerHandler(targetCb); }',
        'export function timerRegistrar(): void { setTimeout(targetCb, 100); }',
        'export function objRegistrar(): unknown { return { recv: targetCb }; }',
        'export function arrRegistrar(): unknown { return [targetCb]; }',
        '',
        'class Emitter { cb: ((x: number) => void) | null = null; }',
        'export function assignRegistrar(e: Emitter): void { e.cb = targetCb; }',
        '',
        'interface Btn { on(ev: string, cb: () => void): void; }',
        'export class Comp {',
        '  handleClick(): void {}',
        '  wire(btn: Btn): void { btn.on("click", this.handleClick); }',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      expect(sourceNames(cg, fnRefEdgesInto(cg, 'targetCb'))).toEqual([
        'argRegistrar',
        'arrRegistrar',
        'assignRegistrar',
        'objRegistrar',
        'timerRegistrar',
      ]);
      // `this.handleClick` resolves class-scoped (#808): the target must be a
      // method of the ENCLOSING class, in the same file.
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'handleClick'))).toEqual(['wire']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('resolves an imported callback across files via its import', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-import-'));
    fs.writeFileSync(
      path.join(tmpDir, 'handlers.ts'),
      'export function onMessage(x: number): void { console.log(x); }\n'
    );
    fs.writeFileSync(
      path.join(tmpDir, 'wiring.ts'),
      [
        "import { onMessage } from './handlers';",
        'export function wire(bus: { on(cb: (x: number) => void): void }): void {',
        '  bus.on(onMessage);',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const edges = fnRefEdgesInto(cg, 'onMessage');
      expect(sourceNames(cg, edges)).toContain('wire');
      // The edge must target the handlers.ts definition.
      const target = cg.getNode(edges[0]!.target);
      expect(target?.filePath.endsWith('handlers.ts')).toBe(true);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('DECOY: ambiguous cross-file name without an import resolves to NO edge', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-decoy-'));
    // Two same-named functions in different files…
    fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'export function process(x: number): void {}\n');
    fs.writeFileSync(path.join(tmpDir, 'b.ts'), 'export function process(x: number): void {}\n');
    // …and a registrar that names `process` WITHOUT importing it. The name
    // still passes the extraction gate only if imported/defined here — it is
    // neither, so this asserts the gate; even if it leaked through, the
    // ambiguity rule (unique-only cross-file) must yield no edge.
    fs.writeFileSync(
      path.join(tmpDir, 'c.ts'),
      'export function wire(bus: { on(cb: unknown): void }, process: unknown): void { bus.on(process); }\n'
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const edges = fnRefEdgesInto(cg, 'process');
      expect(sourceNames(cg, edges)).not.toContain('wire');
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('SAME-FILE PRIORITY: a same-file definition beats a same-named decoy elsewhere', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-samefile-'));
    fs.writeFileSync(path.join(tmpDir, 'decoy.c'), 'void my_cb(int x) { (void)x; }\n');
    fs.writeFileSync(
      path.join(tmpDir, 'real.c'),
      [
        'static void my_cb(int x) { (void)x; }',
        'void register_handler(void (*cb)(int)) { cb(1); }',
        'void wire(void) { register_handler(my_cb); }',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const wires = fnRefEdgesInto(cg, 'my_cb').filter((e) => {
        const src = cg.getNode(e.source);
        return src?.name === 'wire';
      });
      expect(wires).toHaveLength(1);
      const target = cg.getNode(wires[0]!.target);
      expect(target?.filePath.endsWith('real.c')).toBe(true);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('KIND FILTER: a class passed as a value gets no function-ref edge', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-kind-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.ts'),
      [
        'export class Strategy { run(): void {} }',
        'export function consume(x: unknown): void { void x; }',
        'export function wire(): void { consume(Strategy); }',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const strategy = cg.getNodesByName('Strategy').find((n) => n.kind === 'class')!;
      const fnRef = cg
        .getIncomingEdges(strategy.id)
        .filter((e) => e.metadata?.fnRef === true);
      expect(fnRef).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('SELF: a function registering itself produces no self-loop', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-self-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.ts'),
      [
        'declare function schedule(cb: () => void): void;',
        'export function retry(): void { schedule(retry); }',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const retry = cg.getNodesByName('retry')[0]!;
      const selfLoops = cg
        .getIncomingEdges(retry.id)
        .filter((e) => e.source === retry.id && e.metadata?.fnRef === true);
      expect(selfLoops).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('C++: &Cls::method member pointers resolve scoped; bare ids are free-function-only', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-cpp-'));
    fs.writeFileSync(
      path.join(tmpDir, 'widget.cpp'),
      [
        'struct Widget {',
        '  void on_click(int x);',
        '};',
        'void Widget::on_click(int x) { (void)x; }',
        'struct Decoy {',
        '  void on_click(int x);',
        '};',
        'void Decoy::on_click(int x) { (void)x; }',
        'void free_cb(int x) { (void)x; }',
        'void bare_fn(int x) { (void)x; }',
        'void reg(void* p) { (void)p; }',
        'void wire() {',
        '  auto p = &Widget::on_click;', // qualified — must hit Widget, not Decoy
        '  reg(p);',
        '  reg(&free_cb);', // explicit address-of — captured
        '  reg(bare_fn);', // bare id in args — NOT captured for C++ (addressOfOnly)
        '}',
        // A method named like a local: passing the LOCAL must not resolve to
        // the method (cpp args accept only explicit & forms).
        'struct Buf { char* out(); };',
        'void copy_to(void* out_) { (void)out_; }',
        'void caller(char* out) { copy_to(out); }',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      // Qualified member pointer resolves to Widget::on_click specifically.
      const onClicks = cg.getNodesByName('on_click');
      const widgetOnClick = onClicks.find((n) => n.qualifiedName.includes('Widget'))!;
      const decoyOnClick = onClicks.find((n) => n.qualifiedName.includes('Decoy'))!;
      const intoWidget = cg
        .getIncomingEdges(widgetOnClick.id)
        .filter((e) => e.metadata?.fnRef === true);
      expect(intoWidget).toHaveLength(1);
      expect(cg.getNode(intoWidget[0]!.source)?.name).toBe('wire');
      expect(
        cg.getIncomingEdges(decoyOnClick.id).filter((e) => e.metadata?.fnRef === true)
      ).toHaveLength(0);

      // Explicit &fn resolves; bare identifier in C++ args does NOT (the
      // generic-name collision class: fmt's `begin`/`out`/`size` params).
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'free_cb'))).toContain('wire');
      expect(fnRefEdgesInto(cg, 'bare_fn')).toHaveLength(0);

      // The local `out` param must NOT produce an edge to Buf::out.
      const outMethod = cg.getNodesByName('out').find((n) => n.kind === 'method');
      if (outMethod) {
        expect(
          cg.getIncomingEdges(outMethod.id).filter((e) => e.metadata?.fnRef === true)
        ).toHaveLength(0);
      }
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('Pascal: := event wiring, @addr and bare args', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-pas-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.pas'),
      [
        'unit Main;',
        'interface',
        'type',
        '  TCallback = procedure(X: Integer);',
        '  THolder = class',
        '  public',
        '    OnFire: TCallback;',
        '    procedure Wire;',
        '  end;',
        'procedure TargetCb(X: Integer);',
        'procedure RegisterHandler(Cb: TCallback);',
        'procedure ArgRegistrar;',
        'procedure AddrRegistrar;',
        'implementation',
        'procedure TargetCb(X: Integer);',
        'begin',
        '  WriteLn(X);',
        'end;',
        'procedure RegisterHandler(Cb: TCallback);',
        'begin',
        '  Cb(1);',
        'end;',
        'procedure ArgRegistrar;',
        'begin',
        '  RegisterHandler(TargetCb);',
        'end;',
        'procedure AddrRegistrar;',
        'begin',
        '  RegisterHandler(@TargetCb);',
        'end;',
        'procedure THolder.Wire;',
        'begin',
        '  OnFire := TargetCb;',
        'end;',
        'end.',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'TargetCb'))).toEqual([
        'AddrRegistrar',
        'ArgRegistrar',
        'Wire',
      ]);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('THIS-MEMBER SCOPING: this.X resolves only to the enclosing class, never elsewhere', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-thisx-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.ts'),
      [
        'declare const bus: { on(ev: string, cb: () => void): void };',
        // Decoy: a same-named method on an UNRELATED class.
        'export class Decoy { refresh(): void {} }',
        'export class Panel {',
        '  views: number[] = [];', // property (post-#808), shares no name
        '  refresh(): void {}',
        '  wire(): void {',
        '    bus.on("update", this.refresh);', // → Panel::refresh, not Decoy::refresh
        '    bus.on("data", this.views as never);', // property → NO edge
        '    bus.on("gone", this.missing as never);', // unknown member → NO edge
        '  }',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      const refreshes = cg.getNodesByName('refresh');
      const panelRefresh = refreshes.find((n) => n.qualifiedName.includes('Panel'))!;
      const decoyRefresh = refreshes.find((n) => n.qualifiedName.includes('Decoy'))!;

      const intoPanel = cg
        .getIncomingEdges(panelRefresh.id)
        .filter((e) => e.metadata?.fnRef === true);
      expect(intoPanel).toHaveLength(1);
      expect(cg.getNode(intoPanel[0]!.source)?.name).toBe('wire');
      expect(
        cg.getIncomingEdges(decoyRefresh.id).filter((e) => e.metadata?.fnRef === true)
      ).toHaveLength(0);

      // The property and the unknown member produce nothing.
      const views = cg.getNodesByName('views').find((n) => n.kind === 'property');
      if (views) {
        expect(
          cg.getIncomingEdges(views.id).filter((e) => e.metadata?.fnRef === true)
        ).toHaveLength(0);
      }
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('INHERITED this.X: resolves on a supertype via the second pass, never on unrelated classes', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-inherit-'));
    fs.writeFileSync(
      path.join(tmpDir, 'base.ts'),
      'export class FormBase { handleSubmit(): void {} }\n'
    );
    fs.writeFileSync(
      path.join(tmpDir, 'unrelated.ts'),
      'export class Unrelated { handleSubmit(): void {} }\n'
    );
    fs.writeFileSync(
      path.join(tmpDir, 'login.ts'),
      [
        "import { FormBase } from './base';",
        'declare const bus: { on(ev: string, cb: () => void): void };',
        'export class LoginForm extends FormBase {',
        '  wire(): void { bus.on("submit", this.handleSubmit); }',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const handleSubmits = cg.getNodesByName('handleSubmit');
      const baseM = handleSubmits.find((n) => n.qualifiedName.includes('FormBase'))!;
      const unrelatedM = handleSubmits.find((n) => n.qualifiedName.includes('Unrelated'))!;

      const intoBase = cg.getIncomingEdges(baseM.id).filter((e) => e.metadata?.fnRef === true);
      expect(intoBase).toHaveLength(1);
      expect(cg.getNode(intoBase[0]!.source)?.name).toBe('wire');
      expect(
        cg.getIncomingEdges(unrelatedM.id).filter((e) => e.metadata?.fnRef === true)
      ).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('JAVA: Type::method cross-file, this::/super:: scoped, variable:: yields nothing', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-java-'));
    fs.writeFileSync(
      path.join(tmpDir, 'Handlers.java'),
      [
        'package com.example;',
        'public class Handlers {',
        '    public static void onMessage(int x) { System.out.println(x); }',
        '}',
      ].join('\n')
    );
    fs.writeFileSync(
      path.join(tmpDir, 'BaseForm.java'),
      ['package com.example;', 'public class BaseForm {', '    void baseHandler(int x) {}', '}'].join('\n')
    );
    fs.writeFileSync(
      path.join(tmpDir, 'Main.java'),
      [
        'package com.example;',
        'import com.example.Handlers;',
        'import java.util.function.IntConsumer;',
        'public class Main extends BaseForm {',
        '    static void registerHandler(IntConsumer cb) { cb.accept(1); }',
        '    void run0() {}',
        '    void crossFile() { registerHandler(Handlers::onMessage); }',
        '    void thisRef() { registerHandler(this::run0); }',
        '    void superRef() { registerHandler(super::baseHandler); }',
        '    void varRef(Main m) { registerHandler(m::run0); }',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      expect(sourceNames(cg, fnRefEdgesInto(cg, 'onMessage'))).toEqual(['crossFile']);
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'baseHandler'))).toEqual(['superRef']);
      // this::run0 resolves class-scoped; m::run0 (variable receiver) must NOT
      // add a second edge — exactly one source.
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'run0'))).toEqual(['thisRef']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('KOTLIN: companion-object refs resolve cross-file without imports; decoy companion untouched', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-ktcomp-'));
    // Same package, no imports — the Java/Kotlin reality the name gate can't
    // see, which is why qualified `Type::member` candidates skip it.
    fs.writeFileSync(
      path.join(tmpDir, 'Handlers.kt'),
      [
        'class KtHandlers {',
        '  companion object {',
        '    fun handle(x: Int) {}',
        '  }',
        '}',
        'class Decoy {',
        '  companion object {',
        '    fun handle(x: Int) {}',
        '  }',
        '}',
      ].join('\n')
    );
    fs.writeFileSync(
      path.join(tmpDir, 'Wirer.kt'),
      [
        'fun register(cb: Any) {}',
        'class Wirer {',
        '  fun wire() { register(KtHandlers::handle) }',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const handles = cg.getNodesByName('handle');
      const target = handles.find((n) => n.qualifiedName.includes('KtHandlers'))!;
      const decoy = handles.find((n) => n.qualifiedName.includes('Decoy'))!;
      const into = cg.getIncomingEdges(target.id).filter((e) => e.metadata?.fnRef === true);
      expect(into).toHaveLength(1);
      expect(cg.getNode(into[0]!.source)?.name).toBe('wire');
      expect(cg.getIncomingEdges(decoy.id).filter((e) => e.metadata?.fnRef === true)).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('SWIFT SCOPING: bare ids hit only the enclosing type’s methods; top-level bare hits functions only', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-swiftscope-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.swift'),
      [
        'func register(_ cb: (Int) -> Void) { cb(1) }',
        'class Monitor {',
        '  func report(_ x: Int) {}',
        '  func wire() { register(report) }', // implicit self → Monitor::report
        '}',
        'class Other {',
        // `report` here is a PARAMETER; Monitor::report must not win.
        '  func use(report: (Int) -> Void) { register(report) }',
        '}',
        'func topLevel() { register(report) }', // no implicit self → no method target
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const edges = fnRefEdgesInto(cg, 'report');
      expect(sourceNames(cg, edges)).toEqual(['wire']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('C UNGATED TABLES: a command table names handlers defined in OTHER files (redis pattern)', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-ctable-'));
    // Handler defined in its own file…
    fs.writeFileSync(path.join(tmpDir, 't_string.c'), 'void getCommand(int c) { (void)c; }\n');
    // …and registered in a table in ANOTHER file, with no import mechanism (C).
    fs.writeFileSync(
      path.join(tmpDir, 'server.c'),
      [
        'struct cmd { const char *name; void (*proc)(int); };',
        'static struct cmd commandTable[] = {',
        '  { "get", getCommand },',
        '};',
      ].join('\n')
    );
    // Ambiguity safety: two files define dupCmd; a third table references it →
    // NO edge (unique-or-drop).
    fs.writeFileSync(path.join(tmpDir, 'dup_a.c'), 'void dupCmd(int c) { (void)c; }\n');
    fs.writeFileSync(path.join(tmpDir, 'dup_b.c'), 'void dupCmd(int c) { (void)c; }\n');
    fs.writeFileSync(
      path.join(tmpDir, 'other.c'),
      [
        'struct cmd2 { void (*proc)(int); };',
        'static struct cmd2 otherTable[] = { { dupCmd } };',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      // Cross-file unique handler resolves from the table's file.
      const intoGet = fnRefEdgesInto(cg, 'getCommand');
      expect(sourceNames(cg, intoGet)).toEqual(['server.c']);
      const target = cg.getNode(intoGet[0]!.target);
      expect(target?.filePath.endsWith('t_string.c')).toBe(true);

      // Ambiguous handler resolves to NOTHING — silent beats wrong.
      expect(fnRefEdgesInto(cg, 'dupCmd')).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('PHP: HOF string callables, [$this,…] and [Cls::class,…] arrays; non-HOF strings ignored', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-php-'));
    fs.writeFileSync(
      path.join(tmpDir, 'handlers.php'),
      "<?php\nfunction cmp_items($a, $b) { return $a <=> $b; }\n"
    );
    fs.writeFileSync(
      path.join(tmpDir, 'main.php'),
      [
        '<?php',
        'class Saver {',
        '    public function onSave($x) {}',
        '    public function wire() {',
        "        register_shutdown_function([$this, 'onSave']);",
        '    }',
        '}',
        'class Loader {',
        '    public static function load($cls) {}',
        '}',
        'function sorter($items) {',
        "    usort($items, 'cmp_items');", // known HOF, cross-file string → edge
        "    spl_autoload_register([Loader::class, 'load']);",
        "    some_random_fn('cmp_items');", // NOT a known HOF → no edge
        '    return $items;',
        '}',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      // Exactly ONE source for cmp_items: the usort site, not some_random_fn.
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'cmp_items'))).toEqual(['sorter']);
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'onSave'))).toEqual(['wire']);
      expect(sourceNames(cg, fnRefEdgesInto(cg, 'load'))).toEqual(['sorter']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('RUBY HOOKS: before_action/rescue_from symbols resolve class-scoped incl. inherited; validates is excluded', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-rubyhooks-'));
    fs.writeFileSync(
      path.join(tmpDir, 'posts_controller.rb'),
      [
        'class ApplicationController',
        '  def authenticate; end',
        'end',
        '',
        'class PostsController < ApplicationController',
        '  before_action :authenticate', // inherited → ApplicationController
        '  after_save :reindex',
        '  validates :title, presence: true', // attributes, NOT methods → no edge
        '  rescue_from StandardError, with: :render_500',
        '',
        '  def reindex; end',
        '  def render_500; end',
        '  def title; end',
        'end',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();

      const auth = fnRefEdgesInto(cg, 'authenticate');
      expect(auth).toHaveLength(1);
      expect(cg.getNode(auth[0]!.target)?.qualifiedName).toContain('ApplicationController');

      expect(fnRefEdgesInto(cg, 'reindex')).toHaveLength(1);
      expect(fnRefEdgesInto(cg, 'render_500')).toHaveLength(1);
      // `validates :title` names an attribute — the same-named METHOD must
      // get no registration edge.
      expect(fnRefEdgesInto(cg, 'title')).toHaveLength(0);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });

  it('DRAIN: resolvable function_ref rows leave unresolved_refs; re-index is stable', async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-drain-'));
    fs.writeFileSync(
      path.join(tmpDir, 'main.c'),
      [
        'static void cb_a(int x) { (void)x; }',
        'void reg(void (*cb)(int)) { cb(1); }',
        'void wire(void) { reg(cb_a); }',
      ].join('\n')
    );

    const cg = CodeGraph.initSync(tmpDir);
    try {
      await cg.indexAll();
      const stats1 = cg.getStats();

      // No function_ref rows may linger for resolvable names — the batched
      // resolver must have drained them (delete keyed on the ORIGINAL stored
      // ref; the #760 runaway came from violating that).
      const db = (cg as unknown as { db: { prepare(sql: string): { all(): unknown[] } } }).db;
      let leftover: unknown[] = [];
      try {
        leftover = db
          .prepare("SELECT * FROM unresolved_refs WHERE reference_kind = 'function_ref'")
          .all();
      } catch {
        // If internals aren't reachable this guard is covered by the edge
        // assertions below.
      }
      expect(leftover).toHaveLength(0);

      // Re-index: identical node/edge counts (idempotent, no accumulation).
      await cg.indexAll();
      const stats2 = cg.getStats();
      expect(stats2.totalNodes).toBe(stats1.totalNodes);
      expect(stats2.totalEdges).toBe(stats1.totalEdges);

      expect(sourceNames(cg, fnRefEdgesInto(cg, 'cb_a'))).toEqual(['wire']);
    } finally {
      cg.destroy();
      tmpDir = undefined;
    }
  });
});
