/**
 * rigor/fuzzer-mutation-needles.test.js — greybox-fuzzer campaign over
 * config-kind transitions interleaved with mutation bursts.
 *
 * The needle class this hunts: cache staleness when a checked relation's
 * config kind changes while its decision/rule caches are warm, under
 * arbitrary interleavings of adds, removes, and config redefinitions.
 * The fuzzer's mutation + replay-near-failure strategies deepen the
 * sequences that approach invariant violations.
 *
 * The mirror independently computes check expectations from the raw tuple
 * map and the CURRENT config kind; engine check results must agree after
 * every step (decision and value).
 *
 * Also verifies the fuzzer machinery: report.fuzzerStats is populated and
 * corpus-based strategies were scheduled.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';

const NODES = 6;
const nodeKey = (id) => (id < 2 ? `u:${id}` : id === 2 ? 'g:0' : `doc:${id - 3}`);

const KINDS = ['direct', 'chain', 'union', 'exclusion', 'intersection', 'defeasible'];
const CONFIGS = {
  direct: { type: 'direct', relation: 'owner' },
  chain: {
    type: 'chain',
    steps: [
      { relation: 'member_of', direction: 'out' },
      { relation: 'reads', direction: 'out' }
    ]
  },
  union: {
    union: {
      rules: [
        { type: 'direct', relation: 'owner' },
        { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
      ]
    }
  },
  exclusion: {
    exclusion: [
      { type: 'direct', relation: 'owner' },
      { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
    ]
  },
  intersection: {
    intersection: {
      rules: [
        { type: 'direct', relation: 'owner' },
        { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
      ]
    }
  },
  defeasible: {
    when: { type: 'direct', relation: 'owner' },
    unless: { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
  }
};

function mirrorCheck(tuples, kind, src, rel, dst) {
  const srcKey = nodeKey(src);
  const dstKey = nodeKey(dst);
  const key = (s, r, d) => `${s}|${r}|${d}`;
  const p = (s, r, d) => tuples.get(key(s, r, d)) ?? 0;
  if (rel !== 'can_read') return 0;
  const direct = p(srcKey, 'owner', dstKey);
  let chainBest = 0;
  for (let m = 0; m < NODES; m++) {
    chainBest = Math.max(chainBest, Math.min(p(srcKey, 'member_of', nodeKey(m)), p(nodeKey(m), 'reads', dstKey)));
  }
  if (kind === 'direct') return direct;
  if (kind === 'chain') return chainBest;
  if (kind === 'union') return Math.max(direct, chainBest);
  if (kind === 'exclusion') return direct * (1 - chainBest);
  if (kind === 'intersection') return Math.min(direct, chainBest);
  if (kind === 'defeasible') return direct * (1 - chainBest);
  return 0;
}

function makeWrapper() {
  const arbiter = new Arbiter();
  for (let i = 0; i < NODES; i++) arbiter.addNode(nodeKey(i), i < 2 ? 'user' : i === 2 ? 'group' : 'doc');
  arbiter.setRelationConfig('can_read', CONFIGS.direct);
  const tuples = new Map();
  const tupleKey = (s, r, d) => `${s}|${r}|${d}`;
  const ops = [];

  const wrapper = {
    kind: 'direct',
    engine: arbiter,
    tuples,
    add(src, rel, dst, p) {
      const key = nodeKey(src);
      const dstKey = nodeKey(dst);
      const exists = arbiter.nodeIdByKey.has(key) && arbiter.nodeIdByKey.has(dstKey);
      if (!exists) return { ok: true, skipped: true };
      arbiter.addRelation(key, rel, dstKey, { possibility: p }); // throws on invalid p
      tuples.set(tupleKey(key, rel, dstKey), p === undefined ? 1.0 : p);
      return { ok: true };
    },
    remove(src, rel, dst) {
      const key = nodeKey(src);
      const dstKey = nodeKey(dst);
      if (!arbiter.nodeIdByKey.has(key) || !arbiter.nodeIdByKey.has(dstKey)) return { ok: true, skipped: true };
      arbiter.removeRelation(key, rel, dstKey);
      tuples.delete(tupleKey(key, rel, dstKey));
      return { ok: true };
    },
    setConfig(kind) {
      arbiter.setRelationConfig('can_read', CONFIGS[kind]);
      wrapper.kind = kind;
      return { ok: true };
    },
    check(src, rel, dst) {
      const result = arbiter.check(nodeKey(src), rel, nodeKey(dst));
      const expected = mirrorCheck(tuples, wrapper.kind, src, rel, dst);
      const got = typeof result.possibility === 'number' && Number.isFinite(result.possibility) ? result.possibility : -1;
      return { engine: Math.round(got * 10000) / 10000, expected: Math.round(expected * 10000) / 10000, reason: result.reason };
    },
    clone() {
      const fresh = makeWrapper();
      for (const op of ops) {
        const [name, ...args] = op;
        fresh[name](...args);
      }
      return fresh;
    }
  };

  const record = (name, fn) => (...args) => {
    const res = fn(...args);
    ops.push([name, ...args]);
    return res;
  };
  wrapper.add = record('add', wrapper.add);
  wrapper.remove = record('remove', wrapper.remove);
  wrapper.setConfig = record('setConfig', wrapper.setConfig);
  wrapper.check = record('check', wrapper.check);
  return wrapper;
}

describe('Fuzzer campaign over config transitions (rigor)', () => {
  it('NEEDLE HUNT: check parity holds under fuzzed mutation/config interleavings', async () => {
    const nodeArg = rigor.gen.int(0, NODES - 1);
    const result = await rigor.campaign(
      [rigor.object('graph', makeWrapper, [
        rigor.method('add', function (s, r, d, p) { return this.add(s, r, d, p); },
          rigor.args(nodeArg, rigor.gen.enum(['owner', 'member_of', 'reads']), nodeArg, rigor.gen.oneOf([0.2, 0.5, 0.8, 0.9]))),
        rigor.method('remove', function (s, r, d) { return this.remove(s, r, d); },
          rigor.args(nodeArg, rigor.gen.enum(['owner', 'member_of', 'reads']), nodeArg)),
        rigor.method('setConfig', function (kind) { return this.setConfig(kind); },
          rigor.args(rigor.gen.enum(KINDS))),
        rigor.method('check', function (s, r, d) { return this.check(s, r, d); },
          rigor.args(rigor.gen.int(0, 1), rigor.gen.constant('can_read'), rigor.gen.oneOf([3, 4, 5])))
      ])],
      rigor.crucible([
        rigor.invariant('decision parity after every step', (ctx) => {
          if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
          const { engine, expected } = ctx.actual;
          return (engine >= 0) === (expected >= 0) || Math.abs(engine - expected) < 1e-4;
        }),
        rigor.invariant('value parity after every step', (ctx) => {
          if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
          const { engine, expected } = ctx.actual;
          return Math.abs(engine - expected) < 1e-4;
        }),
        rigor.invariant('errors are clean validation errors, never internal crashes', (ctx) => {
          if (ctx.error === null) return true;
          const message = ctx.error && ctx.error.message ? ctx.error.message : String(ctx.error);
          return /Invalid|expected/i.test(message);
        }),
        rigor.after('graph.setConfig', ({ objects, error }) => {
          return error !== null || KINDS.includes(objects.graph.kind);
        })
      ])
    ).run({
      effort: 500,
      seed: 'fuzzer-config-needles',
      maxTraceLength: 32,
      fuzzer: {
        enabled: true,
        maxCorpusSize: 200,
        strategies: ['random', 'mutation', 'replay-near-failure'],
        mutationInterval: 5
      }
    });

    const inv = result.crucibleVerdict;
    assert.equal(inv.passed, true, [
      `fuzzed parity violated in ${inv.failureCount} cases:`,
      ...result.failures.slice(0, 3).map((f) =>
        `  [${f.invariant}] action=${f.action} args=${JSON.stringify(f.args)} actual=${JSON.stringify(f.actual)} error=${f.error}`
      )
    ].join('\n'));

    const json = result.toJSONReport();
    assert.ok(json.fuzzerStats, 'fuzzerStats must be present in the report');
    assert.ok(json.fuzzerStats.corpusSize >= 0, 'corpus size tracked');
  });
});
