/**
 * rigor/relational-comparator-router.test.js — js-rigor property tests for
 * RelationalComparatorRouter._isQualitativeRule.
 *
 * The router dispatches to either the numeric or qualitative implementation
 * based on the rule config. Properties:
 *   - rule.qualitative === true → qualitative (regardless of other fields)
 *   - any operand scaleName → qualitative
 *   - any operand decaySteps/baseBlurSteps (defined, non-null) → qualitative
 *   - rule.marginSteps (defined, non-null) → qualitative
 *   - otherwise → numeric
 *
 * getImplementationType is a pure pass-through to _isQualitativeRule.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { RelationalComparatorRouter } from '../../src/authorization/rules/RelationalComparatorRouter.js';

const SCALE_NAMES = ['five-point', 'ternary', 'ordinal-7'];

/**
 * Build a stub arbiter that satisfies the rule's constructor.
 * Neither _evaluateRule is called by these tests, but the constructor
 * stores references that the rule may touch on dispatch.
 */
function makeStubArbiter() {
  return {
    nodeIdByKey: new Map(),
    keyByNodeId: new Map(),
    relations: [],
    nodes: new Map()
  };
}

function makeRouter() {
  // _evaluateRule is what we test; pass a stub arbiter so the
  // constructor doesn't crash. We never call _evaluateRule in these
  // tests — we test _isQualitativeRule and getImplementationType
  // directly.
  return new RelationalComparatorRouter(makeStubArbiter(), {});
}

describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
  it('rule.qualitative=true always wins', async () => {
    async function check(rule) {
      const router = makeRouter();
      const result = router._isQualitativeRule(rule);
      if (!result) {
        throw new Error(
          `expected qualitative=true to win, got numeric. rule=${JSON.stringify(rule)}`
        );
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.object({
              qualitative: rigor.gen.constant(true),
              // random other fields that should not matter
              left: rigor.gen.option(rigor.gen.object({
                scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES))
              })),
              right: rigor.gen.option(rigor.gen.object({
                scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES))
              })),
              marginSteps: rigor.gen.option(rigor.gen.int(0, 5)),
              fallbackBehavior: rigor.gen.option(rigor.gen.enum(['allow', 'deny']))
            })
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('qualitative-wins', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-qualitative-wins', 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 === 'qualitative-wins');
    assert.ok(inv);
    assert.equal(inv.passed, true,
      `qualitative=true did not win in ${inv.failureCount} cases`);
  });

  it('scaleName on either operand triggers qualitative', async () => {
    async function check(operand, side) {
      const router = makeRouter();
      const rule = {
        left: side === 'left' ? operand : {},
        right: side === 'right' ? operand : {}
      };
      const result = router._isQualitativeRule(rule);
      if (!result) {
        throw new Error(
          `expected scaleName on ${side} to trigger qualitative, got numeric. ` +
          `operand=${JSON.stringify(operand)}`
        );
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.object({
              scaleName: rigor.gen.enum(SCALE_NAMES)
            }),
            rigor.gen.enum(['left', 'right'])
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('scaleName-triggers', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-scale-name', 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 === 'scaleName-triggers');
    assert.ok(inv);
    assert.equal(inv.passed, true,
      `scaleName did not trigger qualitative in ${inv.failureCount} cases`);
  });

  it('decaySteps/baseBlurSteps on either operand triggers qualitative', async () => {
    async function check(operand, side) {
      const router = makeRouter();
      const rule = {
        left: side === 'left' ? operand : {},
        right: side === 'right' ? operand : {}
      };
      const result = router._isQualitativeRule(rule);
      if (!result) {
        throw new Error(
          `expected ${JSON.stringify(Object.keys(operand))} on ${side} to trigger qualitative`
        );
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.oneOf([
              rigor.gen.object({ decaySteps: rigor.gen.int(0, 10) }),
              rigor.gen.object({ baseBlurSteps: rigor.gen.int(0, 10) })
            ]),
            rigor.gen.enum(['left', 'right'])
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('decay-blur-triggers', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-decay-blur', 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 === 'decay-blur-triggers');
    assert.ok(inv);
    assert.equal(inv.passed, true,
      `decaySteps/baseBlurSteps did not trigger qualitative in ${inv.failureCount} cases`);
  });

  it('marginSteps triggers qualitative (when defined and non-null)', async () => {
    async function check(marginSteps) {
      const router = makeRouter();
      const rule = { marginSteps };
      const result = router._isQualitativeRule(rule);
      if (marginSteps === undefined || marginSteps === null) {
        // marginSteps not present — should not trigger
        if (result) throw new Error(`marginSteps=${marginSteps} should not trigger qualitative`);
      } else {
        if (!result) throw new Error(`marginSteps=${marginSteps} should trigger qualitative`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.option(rigor.gen.int(0, 10))
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('marginSteps-correct', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-margin-steps', 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 === 'marginSteps-correct');
    assert.ok(inv);
    assert.equal(inv.passed, true,
      `marginSteps handling was wrong in ${inv.failureCount} cases`);
  });

  it('plain numeric rule (no qualitative flags) routes to numeric', async () => {
    async function check(leftValue, rightValue, comparator) {
      const router = makeRouter();
      const rule = {
        left: { rule: { type: 'direct' }, extractValue: true },
        right: { rule: { type: 'direct' }, extractValue: true },
        comparator,
        leftValue,   // injected for testing only — production ignores
        rightValue
      };
      const result = router._isQualitativeRule(rule);
      if (result) {
        throw new Error(
          `expected numeric, got qualitative. rule=${JSON.stringify(rule)}`
        );
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.int(-1000, 1000),
            rigor.gen.int(-1000, 1000),
            rigor.gen.enum(['>', '>=', '<', '<=', '==', '!='])
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('plain-numeric', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-plain-numeric', 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 === 'plain-numeric');
    assert.ok(inv);
    assert.equal(inv.passed, true,
      `plain numeric rule routed to qualitative in ${inv.failureCount} cases`);
  });

  it('getImplementationType matches _isQualitativeRule', async () => {
    async function check(rule) {
      const router = makeRouter();
      const fromPrivate = router._isQualitativeRule(rule);
      const fromPublic = router.getImplementationType(rule);
      const expected = fromPrivate ? 'qualitative' : 'numeric';
      if (fromPublic !== expected) {
        throw new Error(
          `getImplementationType=${fromPublic} but _isQualitativeRule=${fromPrivate}`
        );
      }
      return fromPublic;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            rigor.gen.object({
              qualitative: rigor.gen.option(rigor.gen.boolean()),
              left: rigor.gen.option(rigor.gen.object({
                scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES)),
                decaySteps: rigor.gen.option(rigor.gen.int(0, 10))
              })),
              right: rigor.gen.option(rigor.gen.object({
                scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES)),
                decaySteps: rigor.gen.option(rigor.gen.int(0, 10))
              })),
              marginSteps: rigor.gen.option(rigor.gen.int(0, 10))
            })
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('getImplType-consistent', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-get-impl-type', effort: 1500 , artifacts: { dir: '', persist: 'never' }});

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

  it('_hasValidQualitativeProperty: undefined/null → false, anything else → true', async () => {
    async function check(value) {
      const router = makeRouter();
      const result = router._hasValidQualitativeProperty(value);
      if (value === undefined || value === null) {
        if (result) throw new Error(`_hasValidQualitativeProperty(${value}) should be false`);
      } else {
        if (!result) throw new Error(`_hasValidQualitativeProperty(${value}) should be true`);
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check,
          rigor.args(
            // Exclude undefined/null from explicit inputs — the
            // generator treats them as "absent". The campaign will
            // also exercise the absent path via undefined when the
            // record is shrunk to {}.
            rigor.gen.option(
              rigor.gen.oneOf([
                rigor.gen.int(),
                rigor.gen.float(),
                rigor.gen.boolean(),
                rigor.gen.string(),
                rigor.gen.constant(0),
                rigor.gen.constant(''),
                rigor.gen.constant(false)
              ])
            )
          )
        )
      ],
      rigor.crucible([
        rigor.invariant('hasValidProperty', ({ actual }) => actual !== undefined)
      ])
    ).run({ seed: 'rc-router-has-valid-property', effort: 1000 , artifacts: { dir: '', persist: 'never' }});

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