/**
 * rigor/computed-rule.test.js — js-rigor property tests for ComputedRule.
 *
 * ComputedRule delegates to arbiter.authChecker.check(userKey, computedRelation,
 * objectKey, options) and adapts the result. Properties verified:
 *
 *   - result.possibility equals the delegated authChecker.check result.possibility
 *   - result.reason defaults to 'computed_delegation' if delegated has no reason
 *   - result.reason passes through the delegated reason when present
 *   - meta.ruleType='computed' and meta.computedRelation=rule.relation
 *   - meta.delegated=true
 *   - collectedValues pass through from delegated result
 *   - trackEvaluation=true produces a populated result.evaluation block
 *   - result.shape is stable across many inputs
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { ComputedRule } from '../../src/authorization/rules/ComputedRule.js';

const RELATIONS = ['owner', 'viewer', 'editor', 'member', 'parent'];

/**
 * Build an arbiter stub whose authChecker.check returns a programmable value.
 * Records all calls for assertions.
 */
function makeArbiter(delegate) {
  const calls = [];
  const arbiter = {
    authChecker: {
      check(userKey, computedRelation, objectKey, options) {
        calls.push({ userKey, computedRelation, objectKey, hasVisited: !!options._visited, hasCurrentRel: !!options._currentRelation });
        return delegate(userKey, computedRelation, objectKey, options);
      }
    }
  };
  return { arbiter, calls };
}

describe('ComputedRule evaluation (rigor)', () => {
  it('result.possibility equals delegated authChecker.check result.possibility', async () => {
    async function check(userKey, computedRel, objectKey, possibility) {
      const { arbiter, calls } = makeArbiter(() => ({ possibility, reliability: 1.0 }));
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        {}
      );
      if (result.possibility !== possibility) {
        throw new Error(`result.possibility=${result.possibility}, expected ${possibility}`);
      }
      // Delegation must have happened with the rule.relation as the computed relation
      if (calls.length !== 1) throw new Error(`expected 1 authChecker.check call, got ${calls.length}`);
      if (calls[0].userKey !== userKey) throw new Error(`userKey=${calls[0].userKey}, expected ${userKey}`);
      if (calls[0].computedRelation !== computedRel) throw new Error(`computedRelation=${calls[0].computedRelation}, expected ${computedRel}`);
      if (calls[0].objectKey !== objectKey) throw new Error(`objectKey=${calls[0].objectKey}, expected ${objectKey}`);
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30),
          rigor.gen.float({ min: 0, max: 1 })
        )
      )],
      rigor.crucible([
        rigor.invariant('possibility-passthrough', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-possibility-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'possibility-passthrough');
    assert.ok(inv);
    assert.equal(inv.passed, true, `possibility passthrough violated in ${inv.failureCount} cases`);
  });

  it('result.reason defaults to "computed_delegation" when delegated has no reason', async () => {
    async function check(userKey, computedRel, objectKey) {
      const { arbiter } = makeArbiter(() => ({ possibility: 0.5, reliability: 1.0 }));
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        {}
      );
      if (result.reason !== 'computed_delegation') {
        throw new Error(`reason=${result.reason}, expected 'computed_delegation'`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30)
        )
      )],
      rigor.crucible([
        rigor.invariant('reason-default', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-reason-default', effort: 500 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'reason-default');
    assert.ok(inv);
    assert.equal(inv.passed, true, `reason default violated in ${inv.failureCount} cases`);
  });

  it('result.reason passes through the delegated reason when present', async () => {
    async function check(userKey, computedRel, objectKey, reason) {
      const { arbiter } = makeArbiter(() => ({ possibility: 0.5, reliability: 1.0, reason }));
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        {}
      );
      if (result.reason !== reason) {
        throw new Error(`reason=${result.reason}, expected ${reason}`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30),
          rigor.gen.enum(['direct_match', 'no_relation', 'inferred', 'chain_match', 'computed_delegation'])
        )
      )],
      rigor.crucible([
        rigor.invariant('reason-passthrough', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-reason-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'reason-passthrough');
    assert.ok(inv);
    assert.equal(inv.passed, true, `reason passthrough violated in ${inv.failureCount} cases`);
  });

  it('meta.ruleType="computed" and meta.computedRelation=rule.relation', async () => {
    async function check(userKey, computedRel, objectKey) {
      const { arbiter } = makeArbiter(() => ({ possibility: 0.5, reliability: 1.0 }));
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        { includeMeta: true, trackEvaluation: false }
      );
      if (!result.meta) throw new Error(`result.meta is missing (full result: ${JSON.stringify(result)})`);
      if (result.meta.ruleType !== 'computed') {
        throw new Error(`meta.ruleType=${result.meta.ruleType}, expected 'computed' (full meta: ${JSON.stringify(result.meta)})`);
      }
      if (result.meta.computedRelation !== computedRel) {
        throw new Error(`meta.computedRelation=${result.meta.computedRelation}, expected ${computedRel}`);
      }
      if (result.meta.delegated !== true) {
        throw new Error(`meta.delegated=${result.meta.delegated}, expected true`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30)
        )
      )],
      rigor.crucible([
        rigor.invariant('meta-contract', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-meta-contract', effort: 800 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'meta-contract');
    assert.ok(inv);
    assert.equal(inv.passed, true, `meta contract violated in ${inv.failureCount} cases`);
  });

  it('collectedValues pass through from delegated result', async () => {
    async function check(userKey, computedRel, objectKey, nValues) {
      const values = Array.from({ length: nValues }, (_, i) => ({
        value: i + 1,
        possibility: 0.5,
        path: [userKey, objectKey],
        source: { entityKey: userKey, relation: computedRel, step: 0 },
        metadata: { timestamp: 1000, reliability: 1.0 }
      }));
      const { arbiter } = makeArbiter(() => ({ possibility: 0.5, reliability: 1.0, collectedValues: values }));
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        {}
      );
      if (result.collectedValues.length !== nValues) {
        throw new Error(`collectedValues.length=${result.collectedValues.length}, expected ${nValues}`);
      }
      for (let i = 0; i < nValues; i++) {
        if (result.collectedValues[i].value !== values[i].value) {
          throw new Error(`collectedValues[${i}].value=${result.collectedValues[i].value}, expected ${values[i].value}`);
        }
      }
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30),
          rigor.gen.int(0, 5)
        )
      )],
      rigor.crucible([
        rigor.invariant('collected-values-passthrough', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-collected-values', effort: 800 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'collected-values-passthrough');
    assert.ok(inv);
    assert.equal(inv.passed, true, `collected values passthrough violated in ${inv.failureCount} cases`);
  });

  it('result.possibility defaults to 0 when delegated has no possibility', async () => {
    async function check(userKey, computedRel, objectKey) {
      const { arbiter } = makeArbiter(() => ({ reliability: 1.0 }));  // no possibility
      const rule = new ComputedRule(arbiter);
      const result = rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        new Set(),
        'unused',
        {}
      );
      if (result.possibility !== 0) {
        throw new Error(`result.possibility=${result.possibility}, expected 0 (fallback when delegated is undefined)`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30)
        )
      )],
      rigor.crucible([
        rigor.invariant('possibility-fallback-zero', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-possibility-fallback', effort: 500 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'possibility-fallback-zero');
    assert.ok(inv);
    assert.equal(inv.passed, true, `possibility fallback violated in ${inv.failureCount} cases`);
  });

  it('authChecker.check is called with the visited set and currentRelation passed through options', async () => {
    async function check(userKey, computedRel, objectKey, currentRel) {
      const { arbiter, calls } = makeArbiter(() => ({ possibility: 0.5, reliability: 1.0 }));
      const rule = new ComputedRule(arbiter);
      const visited = new Set([`visited:1`, `visited:2`]);
      rule.evaluate(
        0, userKey, 1, objectKey,
        { type: 'computed', relation: computedRel },
        visited,
        currentRel,
        {}
      );
      if (calls.length !== 1) throw new Error(`expected 1 call, got ${calls.length}`);
      if (!calls[0].hasVisited) throw new Error('authChecker.check did not receive options._visited');
      if (!calls[0].hasCurrentRel) throw new Error('authChecker.check did not receive options._currentRelation');
      return true;
    }

    const report = await rigor.campaign(
      [rigor.fn('check', check,
        rigor.args(
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS),
          rigor.gen.string(1, 30),
          rigor.gen.enum(RELATIONS)
        )
      )],
      rigor.crucible([
        rigor.invariant('options-passthrough', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'computed-rule-options-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});

    if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'options-passthrough');
    assert.ok(inv);
    assert.equal(inv.passed, true, `options passthrough violated in ${inv.failureCount} cases`);
  });
});
