import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import fc from 'fast-check';
import { Arbiter } from '../../src/core/Arbiter.js';

function hasPathWithin(adj, src, dst, maxDepth) {
  const visited = new Set([src]);
  let frontier = [src];
  let depth = 0;
  while (frontier.length && depth < maxDepth) {
    const next = [];
    for (const node of frontier) {
      const neighbors = adj.get(node) || [];
      for (const n of neighbors) {
        if (n === dst) return true;
        if (!visited.has(n)) {
          visited.add(n);
          next.push(n);
        }
      }
    }
    frontier = next;
    depth++;
  }
  return false;
}

describe('Fast-check: multi-hop invariants', () => {
  test('zero-hop is denied unless explicitly enabled', () => {
    const arbiter = new Arbiter();
    arbiter.addNode('node:0', 'node');
    arbiter.setRelationConfig('link', { type: 'direct' });

    arbiter.setRelationConfig('reachable', { type: 'multi_hop', relation: 'link', maxDepth: 2 });
    const defaultResult = arbiter.check('node:0', 'reachable', 'node:0');
    assert.strictEqual(defaultResult.possibility > 0, false);

    arbiter.setRelationConfig('reachable_allow', {
      type: 'multi_hop',
      relation: 'link',
      maxDepth: 2,
      allowZeroHop: true
    });
    const allowedResult = arbiter.check('node:0', 'reachable_allow', 'node:0');
    assert.strictEqual(allowedResult.possibility > 0, true);
  });

  test('multi-hop reachability matches bounded BFS', () => {
    fc.assert(
      fc.property(
        fc.integer({ min: 2, max: 6 }),
        fc.integer({ min: 1, max: 4 }),
        fc.array(
          fc.record({
            src: fc.integer({ min: 0, max: 5 }),
            dst: fc.integer({ min: 0, max: 5 })
          }),
          { minLength: 1, maxLength: 20 }
        ),
        (nodeCount, maxDepth, edges) => {
          const arbiter = new Arbiter();
          for (let i = 0; i < nodeCount; i++) arbiter.addNode(`node:${i}`, 'node');
          arbiter.setRelationConfig('link', { type: 'direct' });
          arbiter.setRelationConfig('reachable', { type: 'multi_hop', relation: 'link', maxDepth, allowZeroHop: false });

          const adj = new Map();
          for (const edge of edges) {
            const src = edge.src % nodeCount;
            const dst = edge.dst % nodeCount;
            arbiter.addRelation(`node:${src}`, 'link', `node:${dst}`, 1.0);
            const list = adj.get(src) || [];
            list.push(dst);
            adj.set(src, list);
          }

          for (let i = 0; i < nodeCount; i++) {
            for (let j = 0; j < nodeCount; j++) {
              const expected = hasPathWithin(adj, i, j, maxDepth);
              const result = arbiter.check(`node:${i}`, 'reachable', `node:${j}`);
              assert.strictEqual(result.possibility > 0, expected);
            }
          }
        }
      ),
      { numRuns: 30 }
    );
  });
});
