/**
 * U1 (#2084 M4) — per-function taint summary harvest.
 *
 * Fixtures parse REAL TypeScript through the shared CFG/import harness, so the
 * harvester consumes the exact `FunctionCfg` / `FunctionDefUse` /
 * `FunctionSiteMatches` structures the pipeline produces. The four summary
 * edge categories are asserted directly: param→return, param→callee-arg,
 * param→sink, source→return.
 */

import { describe, it, expect } from 'vitest';
import { cfgOf, importsFor } from '../../helpers/ts-cfg-harness.js';
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
import { computeReachingDefs } from '../../../src/core/ingestion/cfg/reaching-defs.js';
import {
  buildTaintImportIndex,
  matchFunctionSites,
} from '../../../src/core/ingestion/taint/match.js';
import type { SourceSinkSanitizerSpec } from '../../../src/core/ingestion/taint/source-sink-config.js';
import { harvestFunctionSummary } from '../../../src/core/ingestion/taint/summary-harvest.js';

const SPEC: SourceSinkSanitizerSpec = {
  sources: [{ kind: 'remote-input', objects: ['req'], properties: ['body', 'query', 'params'] }],
  sinks: [
    { name: 'exec', kind: 'command-injection', args: [0], global: true },
    { name: 'query', kind: 'sql-injection', args: [0], anyReceiver: true },
  ],
  sanitizers: [{ name: 'escape', neutralizes: ['command-injection'], global: true }],
};

function harvest(code: string, spec: SourceSinkSanitizerSpec = SPEC, fnIndex = 0) {
  const cfg: FunctionCfg = cfgOf(code, fnIndex);
  const defUse = computeReachingDefs(cfg);
  const matches = matchFunctionSites(cfg, spec, buildTaintImportIndex(importsFor(code)));
  return harvestFunctionSummary(cfg, defUse, matches).facts;
}

describe('harvestFunctionSummary — param→return', () => {
  it('records a param flowing straight to return', () => {
    const f = harvest(`function f(x: string) { return x; }`);
    expect(f.paramCount).toBe(1);
    expect(f.paramToReturn).toEqual([{ param: 0 }]);
  });

  it('records a param returned through a local assignment', () => {
    const f = harvest(`function f(x: string) { const y = x; return y; }`);
    expect(f.paramToReturn).toEqual([{ param: 0 }]);
  });

  it('records receiver-TITO return (x.trim())', () => {
    const f = harvest(`function f(x: string) { return x.trim(); }`);
    expect(f.paramToReturn.map((r) => r.param)).toContain(0);
  });

  it('does not record an unrelated param', () => {
    const f = harvest(`function f(x: string, y: string) { return x; }`);
    expect(f.paramToReturn.map((r) => r.param)).toEqual([0]);
  });
});

describe('harvestFunctionSummary — param→callee-arg', () => {
  it('records a param flowing into a callee argument', () => {
    const f = harvest(`function f(x: string) { helper(x); }`);
    const ca = f.paramToCallArg;
    expect(ca.length).toBeGreaterThanOrEqual(1);
    expect(ca.some((c) => c.param === 0 && c.argIndex === 0 && c.calleeName === 'helper')).toBe(
      true,
    );
  });

  it('records the correct argument index', () => {
    const f = harvest(`function f(x: string) { helper(a, x); }`);
    expect(f.paramToCallArg.some((c) => c.param === 0 && c.argIndex === 1)).toBe(true);
  });
});

describe('harvestFunctionSummary — param→sink', () => {
  it('records a param reaching a modelled sink', () => {
    const f = harvest(`function f(x: string) { exec(x); }`);
    expect(f.paramToSink).toEqual([{ param: 0, sinkKind: 'command-injection' }]);
  });

  it('a sanitizer neutralises the matching sink kind', () => {
    const f = harvest(`function f(x: string) { const y = escape(x); exec(y); }`);
    // escape neutralises command-injection on the path to exec → no param→sink.
    expect(f.paramToSink).toEqual([]);
  });
});

describe('harvestFunctionSummary — call-arg sanitizer exclusions (#2084 review P1-2)', () => {
  it('carries the neutralized kind onto a param→callee-arg edge', () => {
    // x → escape(x) → y → helper(y): the call-arg edge to the user fn `helper`
    // records that command-injection was neutralised on the path.
    const f = harvest(`function f(x: string) { const y = escape(x); helper(y); }`);
    const edge = f.paramToCallArg.find((c) => c.calleeName === 'helper');
    expect(edge).toBeDefined();
    expect(edge!.neutralized).toEqual(['command-injection']);
  });

  it('records no neutralized when the param reaches the call directly', () => {
    const f = harvest(`function f(x: string) { helper(x); }`);
    const edge = f.paramToCallArg.find((c) => c.calleeName === 'helper');
    expect(edge).toBeDefined();
    expect(edge!.neutralized).toBeUndefined();
  });
});

describe('harvestFunctionSummary — source→callee-arg (fixpoint seed)', () => {
  it('records a source passed directly into a callee argument', () => {
    const f = harvest(`function f() { runIt(req.body); }`);
    expect(f.sourceToCallArg.some((s) => s.argIndex === 0 && s.calleeName === 'runIt')).toBe(true);
  });

  it('records a source passed via a local into a callee argument', () => {
    const f = harvest(`function f() { const u = req.body; runIt(u); }`);
    expect(f.sourceToCallArg.some((s) => s.calleeName === 'runIt')).toBe(true);
  });
});

describe('harvestFunctionSummary — call-result seeds (#2084 review P1-1)', () => {
  it('records a generative call result reaching a sink via a local', () => {
    const f = harvest(`function f() { const t = getInput(); exec(t); }`);
    expect(f.callResults.some((cr) => cr.calleeName === 'getInput' && cr.dest.to === 'sink')).toBe(
      true,
    );
  });

  it('records a call result flowing into another callee arg', () => {
    const f = harvest(`function f() { const t = getInput(); forward(t); }`);
    expect(
      f.callResults.some(
        (cr) =>
          cr.calleeName === 'getInput' &&
          cr.dest.to === 'callArg' &&
          cr.dest.toCallee === 'forward',
      ),
    ).toBe(true);
  });

  it('records a bare `return getInput()` as a call result → return', () => {
    const f = harvest(`function f() { return getInput(); }`);
    expect(
      f.callResults.some((cr) => cr.calleeName === 'getInput' && cr.dest.to === 'return'),
    ).toBe(true);
  });

  it('does not record call results for sink/sanitizer calls', () => {
    const f = harvest(`function f(x: string) { exec(escape(x)); }`);
    // exec is a sink, escape is a sanitizer — neither is a user-fn call result.
    expect(f.callResults.some((cr) => cr.calleeName === 'exec' || cr.calleeName === 'escape')).toBe(
      false,
    );
  });
});

describe('harvestFunctionSummary — source→return', () => {
  it('records a generated source returned directly', () => {
    const f = harvest(`function f() { return req.body; }`);
    expect(f.sourceToReturn).toEqual([{ sourceKind: 'remote-input' }]);
  });

  it('records a generated source returned via a local', () => {
    const f = harvest(`function f() { const u = req.body; return u; }`);
    expect(f.sourceToReturn).toEqual([{ sourceKind: 'remote-input' }]);
  });

  it('is empty when no source is present', () => {
    const f = harvest(`function f(x: string) { return x; }`);
    expect(f.sourceToReturn).toEqual([]);
  });
});

describe('harvestFunctionSummary — documented limitations', () => {
  it('all-simple params map to their formal argument position', () => {
    const f = harvest(`function f(a: string, b: string) { exec(b); }`);
    // `b` is formal param 1 — the index the interproc solver joins against.
    expect(f.paramToSink).toEqual([{ param: 1, sinkKind: 'command-injection' }]);
  });

  it('destructured param before a simple param shifts the index (known FN, pinned)', () => {
    // `function f([a, b], x)` — formal positions are [a,b]=0, x=1. The harvest
    // assigns by binding ordinal (a=0, b=1, x=2), so x's port is 2, not the
    // formal 1 the solver joins against → documented cross-function FN. Pinned
    // so the behaviour is a known boundary, not a silent surprise; the proper
    // fix (formal-param index from the worker) is deferred.
    const f = harvest(`function f([a, b]: string[], x: string) { exec(x); }`);
    const xSink = f.paramToSink.find((s) => s.sinkKind === 'command-injection');
    expect(xSink).toBeDefined();
    // Current (limited) behaviour: ordinal index 2, NOT the formal index 1.
    expect(xSink!.param).toBe(2);
  });
});

describe('harvestFunctionSummary — edges & gaps', () => {
  it('empty summary for a param-less, site-less function', () => {
    const f = harvest(`function f() { const a = 1; return a; }`);
    expect(f.paramToReturn).toEqual([]);
    expect(f.paramToCallArg).toEqual([]);
    expect(f.paramToSink).toEqual([]);
    expect(f.sourceToReturn).toEqual([]);
  });

  it('reports a coverage gap when reaching-defs is not computed', () => {
    // A hand-built CFG with no bindings → reaching-defs returns no-facts.
    const cfg = cfgOf(`function f(x: string) { return x; }`);
    const bare = { ...cfg, bindings: undefined } as FunctionCfg;
    const defUse = computeReachingDefs(bare);
    const matches = matchFunctionSites(bare, SPEC, buildTaintImportIndex([]));
    const r = harvestFunctionSummary(bare, defUse, matches);
    expect(r.status).toBe('coverage-gap');
  });
});
