import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/core/Arbiter.js';

function setupDirectArbiter() {
  const arbiter = new Arbiter();
  arbiter.addNode('user:1', 'user');
  arbiter.addNode('doc:1', 'doc');
  arbiter.setRelationConfig('can_read', { type: 'direct' });
  return arbiter;
}

test('ADR-035/042 explainability: audit captures provenance conflict and trust precedence', () => {
  const arbiter = setupDirectArbiter();
  arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.25);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.9 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
  assert.equal(explain.decision.possibility, 0.25);
  assert.equal(explain.trace.path[0].source, 'persistent');

  const provenance = explain.audit?.provenance;
  assert.ok(provenance);
  assert.equal(provenance.has_partial_inputs, true);
  assert.equal(provenance.partial_fact_used, false);
  assert.equal(provenance.effective_source, 'persistent');
  assert.equal(provenance.provenance_conflicts.length, 1);

  const conflict = provenance.provenance_conflicts[0];
  assert.equal(conflict.reason_code, 'higher_trust_source_preferred');
  assert.equal(conflict.winner_source, 'persistent');
  assert.equal(conflict.loser_source, 'partial');
  assert.equal(conflict.relation, 'can_read');
});

test('ADR-035/042 explainability: audit marks partial facts used with no conflict', () => {
  const arbiter = setupDirectArbiter();

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 1.0 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
  assert.equal(explain.decision.possibility, 1.0);
  assert.equal(explain.trace.path[0].source, 'partial');

  const provenance = explain.audit?.provenance;
  assert.ok(provenance);
  assert.equal(provenance.has_partial_inputs, true);
  assert.equal(provenance.partial_fact_used, true);
  assert.equal(provenance.effective_source, 'partial');
  assert.deepEqual(provenance.provenance_conflicts, []);
});

test('ADR-035/042 explainability: chain path conflicts are auditable per edge', () => {
  const arbiter = new Arbiter();
  arbiter.addNode('user:1', 'user');
  arbiter.addNode('device:1', 'device');
  arbiter.addNode('account:1', 'account');

  arbiter.setRelationConfig('device_link', { type: 'direct' });
  arbiter.setRelationConfig('logged_in_as', { type: 'direct' });
  arbiter.setRelationConfig('can_login', {
    type: 'chain',
    steps: [
      { relation: 'device_link', direction: 'out' },
      { relation: 'logged_in_as', direction: 'out' }
    ]
  });

  arbiter.addRelation('user:1', 'device_link', 'device:1', 0.2);
  arbiter.addRelation('device:1', 'logged_in_as', 'account:1', 0.2);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'device_link', dst: 'device:1', possibility: 0.95 },
      { src: 'device:1', relation: 'logged_in_as', dst: 'account:1', possibility: 0.95 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_login', 'account:1', { partialGraph });
  assert.equal(explain.decision.possibility, 0.2);

  const conflicts = explain.audit?.provenance?.provenance_conflicts || [];
  assert.ok(conflicts.length >= 2);
  const conflictRels = new Set(conflicts.map((c) => c.relation));
  assert.ok(conflictRels.has('device_link'));
  assert.ok(conflictRels.has('logged_in_as'));
  for (const c of conflicts) {
    assert.equal(c.reason_code, 'higher_trust_source_preferred');
    assert.equal(c.winner_source, 'persistent');
    assert.equal(c.loser_source, 'partial');
  }
});

test('ADR-035/042 explainability: multi-hop path conflicts are auditable per edge', () => {
  const arbiter = new Arbiter();
  arbiter.addNode('user:1', 'user');
  arbiter.addNode('group:1', 'group');
  arbiter.addNode('doc:1', 'doc');
  arbiter.setRelationConfig('can_reach', { type: 'multi_hop', relation: 'link', maxDepth: 3 });

  arbiter.addRelation('user:1', 'link', 'group:1', 0.3);
  arbiter.addRelation('group:1', 'link', 'doc:1', 0.3);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'link', dst: 'group:1', possibility: 0.9 },
      { src: 'group:1', relation: 'link', dst: 'doc:1', possibility: 0.9 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_reach', 'doc:1', { partialGraph });
  assert.equal(explain.decision.possibility, 0.3);

  const conflicts = explain.audit?.provenance?.provenance_conflicts || [];
  assert.ok(conflicts.length >= 1);
  for (const c of conflicts) {
    assert.equal(c.relation, 'link');
    assert.equal(c.reason_code, 'higher_trust_source_preferred');
    assert.equal(c.winner_source, 'persistent');
  }
});

test('ADR-035/042 explainability: conflict identities respect key redaction mode', () => {
  const arbiter = setupDirectArbiter();
  arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', {
    partialGraph,
    redaction: 'keys'
  });

  const conflict = explain.audit?.provenance?.provenance_conflicts?.[0];
  assert.ok(conflict);
  assert.notEqual(conflict.src, 'user:1');
  assert.notEqual(conflict.object, 'doc:1');
  assert.equal(conflict.src.length, 64);
  assert.equal(conflict.object.length, 64);
  assert.equal(explain.request.userKey, conflict.src);
  assert.equal(explain.request.objectKey, conflict.object);
});

test('ADR-035/042 explainability: hash redaction keeps keys and still includes hashes', () => {
  const arbiter = setupDirectArbiter();
  arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', {
    partialGraph,
    redaction: 'hash'
  });

  const conflict = explain.audit?.provenance?.provenance_conflicts?.[0];
  assert.ok(conflict);
  assert.equal(conflict.src, 'user:1');
  assert.equal(conflict.object, 'doc:1');
  assert.equal(typeof explain.request.userKeyHash, 'string');
  assert.equal(typeof explain.request.objectKeyHash, 'string');
  assert.equal(explain.request.userKeyHash.length, 64);
  assert.equal(explain.request.objectKeyHash.length, 64);
});

test('ADR-035/042 explainability: audit provenance contract has stable shape', () => {
  const arbiter = setupDirectArbiter();
  arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);

  const partialGraph = {
    relations: [
      { src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
  const provenance = explain.audit?.provenance;
  assert.ok(provenance);

  assert.deepEqual(Object.keys(provenance).sort(), [
    'effective_source',
    'has_partial_inputs',
    'partial_fact_used',
    'policy_conflicts',
    'provenance_conflicts'
    ,
    'used_facts'
  ]);

  assert.equal(typeof provenance.has_partial_inputs, 'boolean');
  assert.equal(typeof provenance.partial_fact_used, 'boolean');
  assert.equal(typeof provenance.effective_source, 'string');
  assert.ok(Array.isArray(provenance.provenance_conflicts));
  assert.ok(Array.isArray(provenance.used_facts));
  assert.ok(Array.isArray(provenance.policy_conflicts));
  assert.ok(provenance.provenance_conflicts.length >= 1);

  const conflict = provenance.provenance_conflicts[0];
  assert.deepEqual(Object.keys(conflict).sort(), [
    'decision_possibility',
    'loser_source',
    'object',
    'partial_layer_name',
    'partial_possibility',
    'partial_reducer_applied',
    'partial_source_class',
    'persistent_possibility',
    'reason_code',
    'relation',
    'resolution',
    'rule_type',
    'src',
    'winner_source'
  ]);
});

test('ADR-042 explainability: used facts and policy conflicts are surfaced', () => {
  const arbiter = new Arbiter();
  arbiter.addNode('user:1', 'user');
  arbiter.addNode('doc:1', 'doc');
  // The direct rule checks 'gateway_context_ref', which IS allowlisted at
  // the request_observed layer (can_read is not).
  arbiter.setRelationConfig('can_read', { type: 'direct', relation: 'gateway_context_ref' });

  const partialGraph = {
    options: {
      reducers: {
        delegated_authority: 'strongest'
      }
    },
    relations: [
      {
        src: 'user:1',
        relation: 'gateway_context_ref',
        dst: 'doc:1',
        possibility: 1.0,
        layer_name: 'request_observed',
        source_class: 'gateway_observed'
      }
    ]
  };

  const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
  const provenance = explain.audit?.provenance;
  assert.ok(provenance);

  const used = provenance.used_facts[0];
  assert.ok(used);
  assert.equal(used.layer_name, 'request_observed');
  assert.equal(used.source_class, 'gateway_observed');
  assert.equal(used.source, 'partial');
  assert.equal(used.used, true);

  const policy = provenance.policy_conflicts;
  assert.ok(policy.some((p) => p.kind === 'invalid_reducer_config'));
});
