/**
 * rigor/snapshot-adversarial-fuzz.test.js — greybox-fuzzer campaign over
 * malformed snapshot buffers.
 *
 * The needle class this hunts: silently-accepted corruption in
 * deserializeArbiterSnapshot / CondensedGraphBinary reads. A snapshot is
 * caller-supplied restore input, so every read must either succeed into a
 * structurally sound graph or throw a clean, bounded error — never an
 * internal TypeError, never a graph whose edges point outside the node
 * table, never a hang or oversized allocation.
 *
 * Three pinned oracle layers:
 *   1. Error whitelist — failures must be clean (Error/RangeError/SyntaxError
 *      from the guards, JSON, or structural gates), never "Cannot read
 *      properties..."-style internal crashes.
 *   2. Structural integrity on success — every edge endpoint in range,
 *      node keys unique and non-null, and a post-restore smoke check()
 *      never crashes.
 *   3. Hand-crafted directed needles — truncations, corrupt length fields,
 *      legacy version, non-object JSON payloads — documented behaviors,
 *      deterministic.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
import { serializeArbiterSnapshot } from '../../src/core/SnapshotBinary.js';

function buildSeed() {
  const arbiter = new Arbiter();
  arbiter.addNode('u:1', 'user');
  arbiter.addNode('u:2', 'user');
  arbiter.addNode('g:2', 'group');
  arbiter.addNode('doc:9', 'doc');
  arbiter.setRelationConfig('owner', { type: 'direct' });
  arbiter.setRelationConfig('member_of', { type: 'direct' });
  arbiter.addRelation('u:1', 'owner', 'doc:9', { possibility: 0.9 });
  arbiter.addRelation('u:1', 'member_of', 'g:2', { possibility: 0.7 });
  arbiter.addRelation('u:2', 'member_of', 'g:2', { possibility: 0.8 });
  arbiter.addRelation('g:2', 'owner', 'doc:9', { possibility: 1.0 });
  arbiter.valueManager.setTTL('member_of', 30 * 24 * 60 * 60 * 1000);
  arbiter.enableCondensedSnapshot();
  return serializeArbiterSnapshot(arbiter);
}

const SEED = buildSeed();
const FACTORY = () => new Arbiter();

// Buffer.from(ArrayBuffer) is a shared VIEW, not a copy — every mutation
// must copy first or the module-scope seed gets corrupted under node:test's
// parallel execution.
const copySeed = () => Buffer.from(new Uint8Array(SEED));

const readBuffer = (bytes) => {
  return Arbiter.fromSnapshotBinary(Buffer.from(bytes));
};

const STRUCTURAL = (graph) => {
  const nodeIds = graph.nodeIds;
  if (!nodeIds) return 'graph has no node table';
  const count = nodeIds.length;
  if (count < 0) return 'negative node count';
  const seen = new Set();
  for (let i = 0; i < count; i++) {
    const key = graph.getNodeKey(i);
    if (key === null || key === undefined) return `node ${i} has no key`;
    if (seen.has(key)) return `duplicate node key ${key}`;
    seen.add(key);
  }
  // Raw edge arrays: snapshot graphs carry a relation-specific wavelet, so
  // getOutEdges is not available — the arrays ARE the integrity surface.
  const srcs = graph.edgeSrcIds;
  const dsts = graph.edgeDstIds;
  const rels = graph.edgeRelIds;
  if (srcs && dsts && rels) {
    const n = srcs.length;
    if (dsts.length !== n || rels.length !== n) return 'edge array length mismatch';
    for (let i = 0; i < n; i++) {
      if (srcs[i] < 0 || srcs[i] >= count) return `out-of-range src ${srcs[i]}`;
      if (dsts[i] < 0 || dsts[i] >= count) return `out-of-range dst ${dsts[i]}`;
      if (rels[i] < 0) return `negative rel ${rels[i]}`;
    }
  }
  return null;
};

const CLEAN_ERROR = /Invalid|Unsupported|past end of buffer|out of range|Unexpected|parse|not valid|mismatch|must be an|JSON|Expected/i;
const INTERNAL_CRASH = /Cannot read propert|is not a function|is not iterable|Cannot use/;

describe('Adversarial snapshot reads (directed needles)', () => {
  const needles = [];

  const mk = (name, mutate, expect = /Error/) => needles.push({ name, mutate, expect });

  mk('empty buffer', (b) => b.subarray(0, 0), /past end of buffer|Invalid/i);
  mk('one byte', (b) => b.subarray(0, 1), /past end of buffer|Invalid/i);
  mk('header only', (b) => b.subarray(0, 8), /past end of buffer|Invalid/i);
  mk('truncate inside graph section', (b) => b.subarray(0, 14), /past end of buffer|Invalid|out of range/i);
  mk('truncate inside json section', (b) => b.subarray(0, b.length - 7), /past end of buffer|Invalid|Unexpected|parse/i);
  mk('bad magic', (b) => { const c = Buffer.from(b); c[0] = 0xde; c[1] = 0xad; return c; }, /Invalid arbiter snapshot/);
  mk('legacy version 1', (b) => { const c = Buffer.from(b); c[5] = 1; return c; }, /Unsupported arbiter snapshot version/);
  mk('graph length max uint32', (b) => { const c = Buffer.from(b); c.writeUInt32LE(0xffffffff, 8); return c; }, /past end of buffer|out of range/i);
  mk('json length max uint32', (b) => {
    const c = Buffer.from(b);
    const gl = c.readUInt32LE(8);
    c.writeUInt32LE(0xffffffff, 12 + gl);
    return c;
  }, /past end of buffer|parse/i);
  mk('json payload is null', (b) => {
    const c = Buffer.from(b);
    const gl = c.readUInt32LE(8);
    const json = Buffer.from('null');
    c.writeUInt32LE(json.length, 12 + gl);
    return Buffer.concat([c.subarray(0, 12 + gl + 4), json]);
  }, /Invalid arbiter snapshot payload/);
  mk('json payload is array', (b) => {
    const c = Buffer.from(b);
    const gl = c.readUInt32LE(8);
    const json = Buffer.from('[]');
    c.writeUInt32LE(json.length, 12 + gl);
    return Buffer.concat([c.subarray(0, 12 + gl + 4), json]);
  }, /Invalid arbiter snapshot payload/);

  for (const { name, mutate, expect } of needles) {
    it(name, () => {
      const bytes = mutate(copySeed());
      assert.throws(() => readBuffer(bytes), (err) => {
        assert.ok(err instanceof Error, `${name}: non-Error thrown: ${String(err)}`);
        assert.ok(expect.test(err.message), `${name}: message ${JSON.stringify(err.message)} !~ ${expect}`);
        return true;
      });
    });
  }

  it('unmutated seed round-trips intact', () => {
    const restored = readBuffer(SEED);
    const g = restored.snapshotGraph;
    assert.equal(STRUCTURAL(g), null);
    const result = restored.check('u:1', 'owner', 'doc:9');
    assert.ok(Math.abs(result.possibility - 0.9) < 1e-4, `possibility ${result.possibility}`);
    const result2 = restored.check('u:1', 'member_of', 'g:2');
    assert.ok(Math.abs(result2.possibility - 0.7) < 1e-4, `possibility ${result2.possibility}`);
    assert.equal(restored.valueManager.getTTL('member_of'), 30 * 24 * 60 * 60 * 1000);
  });
});

describe('Adversarial snapshot reads (fuzz campaign)', () => {
  it('NEEDLE HUNT: mutated buffers never crash internally or corrupt structurally', async () => {
    const mutations = [];
    const flipBit = (src) => {
      const b = Buffer.from(src); // copy: src is already an independent Buffer
      const at = Math.floor(Math.random() * b.length);
      b[at] ^= 1 << Math.floor(Math.random() * 8);
      return b;
    };
    const truncate = (src) => Buffer.from(src).subarray(0, Math.floor(Math.random() * src.length));
    const corruptLength = (src) => {
      const b = Buffer.from(src);
      const at = [8, b.length - 4][Math.floor(Math.random() * 2)];
      b.writeUInt32LE(Math.floor(Math.random() * 0xffffffff), at);
      return b;
    };
    const spliceGarbage = (src) => {
      const b = Buffer.from(src);
      const at = Math.floor(Math.random() * b.length);
      const junk = Buffer.alloc(1 + Math.floor(Math.random() * 32));
      for (let i = 0; i < junk.length; i++) junk[i] = Math.floor(Math.random() * 256);
      return Buffer.concat([b.subarray(0, at), junk, b.subarray(at)]);
    };
    // Seed-derived mutations: copySeed() first so the shared module-scope
    // seed is never written through (Buffer views share memory).
    for (let i = 0; i < 24; i++) {
      const s = copySeed();
      mutations.push(flipBit(s));
      mutations.push(truncate(s));
      mutations.push(corruptLength(s));
      mutations.push(spliceGarbage(s));
      mutations.push(Buffer.alloc(1 + Math.floor(Math.random() * 96), Math.floor(Math.random() * 256)));
    }
    const pool = mutations.map((b) => Buffer.from(b));
    // Stable scalar args: the rigor fuzzer's storage layer BigInt-hashes
    // recorded args, so raw byte arrays (NaN under conversion) crash the
    // fuzzer itself — index args keep the fuzzer machinery safe while the
    // pool carries the adversarial byte payloads.
    const indexArg = rigor.gen.int(0, pool.length - 1);
    const wrapper = {
      read(idx) {
        try {
          const restored = Arbiter.fromSnapshotBinary(pool[idx]);
          return { ok: true, graph: restored.snapshotGraph };
        } catch (err) {
          return { ok: false, error: err };
        }
      }
    };

    const result = await rigor.campaign(
      [rigor.object('snap', () => wrapper, [
        rigor.method('read', function (idx) { return this.read(idx); },
          rigor.args(indexArg))
      ])],
      rigor.crucible([
        rigor.invariant('failures are clean guard errors, never internal crashes', (ctx) => {
          if (ctx.error !== null) return true;
          const r = ctx.actual;
          if (r.ok) return true;
          const err = r.error;
          if (!(err instanceof Error)) return false;
          const msg = err.message || String(err);
          if (INTERNAL_CRASH.test(msg)) return false;
          return CLEAN_ERROR.test(msg);
        }),
        rigor.invariant('successful reads yield structurally sound graphs', (ctx) => {
          if (ctx.error !== null) return true;
          const r = ctx.actual;
          if (!r.ok) return true;
          const problem = STRUCTURAL(r.graph);
          return problem === null;
        }),
        rigor.after('snap.read', ({ objects, error }) => {
          return error === null || objects.snap.ok;
        })
      ])
    ).run({
      effort: 600,
      seed: 'snapshot-adversarial-fuzz',
      maxTraceLength: 2,
      fuzzer: {
        enabled: true,
        maxCorpusSize: 300,
        strategies: ['random', 'mutation', 'replay-near-failure'],
        mutationInterval: 3
      }
    });

    const inv = result.crucibleVerdict;
    const failedInvariants = (inv.invariants || []).filter((i) => i.passed === false);
    const json = result.toJSONReport();
    const stats = json && json.fuzzerStats;
    assert.equal(inv.passed, true, [
      `adversarial snapshot reads violated in ${failedInvariants.length} invariant(s):`,
      ...failedInvariants.map((i) => `  invariant ${i.name}: ${i.failureCount} failures`),
      `fuzzer stats: ${JSON.stringify(stats || null).slice(0, 300)}`
    ].join('\n'));

    assert.ok(stats && typeof stats === 'object', 'fuzzer stats must be populated');
  });
});
