// Contract test for the registry-driven per-indicator extraction plan
// used by scripts/compare-resilience-current-vs-proposed.mjs. Pins two
// acceptance-apparatus invariants:
//
//   1. Every indicator in INDICATOR_REGISTRY has a corresponding
//      EXTRACTION_RULES row (implemented OR not-implemented with a
//      reason). No silent omissions.
//   2. All six repair-plan construct-risk indicators (energy mix +
//      electricity consumption + energy import dependency + WGI
//      sub-pillars + recovery fiscal indicators) are 'implemented'
//      in the harness, so PR 1 / PR 3 / PR 4 can measure
//      pre-vs-post effective-influence against their baselines.

import test from 'node:test';
import assert from 'node:assert/strict';
import wgiIndicatorKeys from '../shared/wgi-indicator-keys.json' with { type: 'json' };

const scriptMod = await import('../scripts/compare-resilience-current-vs-proposed.mjs');
const registryMod = await import('../server/worldmonitor/resilience/v1/_indicator-registry.ts');

const { buildIndicatorExtractionPlan, applyExtractionRule, EXTRACTION_RULES } = scriptMod;
const { INDICATOR_REGISTRY } = registryMod;

test('every INDICATOR_REGISTRY entry has an EXTRACTION_RULES row', () => {
  const missing = INDICATOR_REGISTRY.filter((spec) => !(spec.id in EXTRACTION_RULES));
  assert.deepEqual(
    missing.map((s) => s.id),
    [],
    'new indicator(s) added to INDICATOR_REGISTRY without adding an EXTRACTION_RULES entry; ' +
      'add an extractor or an explicit { type: "not-implemented", reason }',
  );
});

test('extraction plan row exists for every registry entry', () => {
  const plan = buildIndicatorExtractionPlan(INDICATOR_REGISTRY);
  assert.equal(plan.length, INDICATOR_REGISTRY.length);
  for (const entry of plan) {
    assert.ok(['implemented', 'not-implemented', 'unregistered-in-harness'].includes(entry.extractionStatus));
  }
});

test('"not-implemented" rows carry a reason string', () => {
  const plan = buildIndicatorExtractionPlan(INDICATOR_REGISTRY);
  for (const entry of plan) {
    if (entry.extractionStatus === 'not-implemented') {
      assert.ok(
        typeof entry.reason === 'string' && entry.reason.length > 0,
        `indicator ${entry.indicator} marked not-implemented but has no reason`,
      );
    }
  }
});

test('all construct-risk indicators flagged by the repair plan are implemented', () => {
  // The repair plan §3.1–§3.2, §4.3, §4.4 specifically names these
  // indicators as the ones whose effective influence must be
  // measurable pre- and post-change. If any becomes 'not-implemented',
  // the acceptance apparatus for that PR evaporates. IDs match
  // INDICATOR_REGISTRY exactly — the registry renames macroFiscal
  // fiscal-space sub-indicators with a `recovery*` prefix when they
  // live in the fiscalSpace dimension.
  const mustBeImplemented = [
    'gasShare',
    'coalShare',
    'renewShare',
    'electricityConsumption',
    'energyImportDependency',
    'govRevenuePct',
    'recoveryGovRevenue',
    'recoveryFiscalBalance',
    'recoveryDebtToGdp',
    'recoveryReserveMonths',
    'recoveryDebtToReserves',
    'recoveryImportHhi',
  ];
  const plan = buildIndicatorExtractionPlan(INDICATOR_REGISTRY);
  const byId = Object.fromEntries(plan.map((p) => [p.indicator, p]));
  for (const id of mustBeImplemented) {
    assert.ok(byId[id], `construct-risk indicator ${id} is not in the extraction plan`);
    assert.equal(
      byId[id].extractionStatus,
      'implemented',
      `construct-risk indicator ${id} must be extractable; got "${byId[id].extractionStatus}": ${byId[id].reason ?? ''}`,
    );
  }
});

test('core-tier indicator coverage meets a minimum floor', () => {
  // Drives the extractionCoverage summary in the output. Floor raised
  // after wiring the exported scorer-aggregate helpers (summarizeCyber,
  // summarizeOutages, summarizeGps, summarizeUcdp, summarizeUnrest,
  // getThreatSummaryScore, getCountryDisplacement, countTradeRestrictions,
  // countTradeBarriers). The only Core-tier indicators still unextracted
  // are those whose scorer inputs are genuinely global scalars
  // (shippingStress, transitDisruption, energyPriceStress) or require
  // unexported time-series helpers (fxVolatility, fxDeviation,
  // aquastatScore, householdDebtService, etc.).
  const plan = buildIndicatorExtractionPlan(INDICATOR_REGISTRY);
  const coreTotal = plan.filter((p) => p.tier === 'core').length;
  const coreImplemented = plan.filter((p) => p.tier === 'core' && p.extractionStatus === 'implemented').length;
  assert.ok(
    coreImplemented / coreTotal >= 0.80,
    `core-tier extraction coverage fell below 80%: ${coreImplemented}/${coreTotal}`,
  );
});

test('the three "no per-country variance" indicators stay not-implemented with correct reason', () => {
  // shippingStress / transitDisruption / energyPriceStress are
  // scorer-level GLOBAL scalars — Pearson(global, overall) is 0 or
  // NaN by construction. They must NOT be marked implemented: any
  // future implementation that appears to extract them is wrong
  // unless it re-expresses them as per-country effective contribution.
  const plan = buildIndicatorExtractionPlan(INDICATOR_REGISTRY);
  const byId = Object.fromEntries(plan.map((p) => [p.indicator, p]));
  for (const id of ['shippingStress', 'transitDisruption', 'energyPriceStress']) {
    assert.equal(byId[id]?.extractionStatus, 'not-implemented', `${id} should stay not-implemented (no per-country variance)`);
    assert.match(byId[id].reason, /no per-country variance|global/i);
  }
});

test('applyExtractionRule — static-path navigates nested object fields', () => {
  const rule = { type: 'static-path', path: ['iea', 'energyImportDependency', 'value'] };
  const sources = { staticRecord: { iea: { energyImportDependency: { value: 42 } } } };
  assert.equal(applyExtractionRule(rule, sources, 'AE'), 42);
});

test('applyExtractionRule — recovery-country-field uses .countries[iso2].<field>', () => {
  const rule = { type: 'recovery-country-field', key: 'resilience:recovery:fiscal-space:v1', field: 'govRevenuePct' };
  const sources = { fiscalSpace: { countries: { AE: { govRevenuePct: 30 } } } };
  assert.equal(applyExtractionRule(rule, sources, 'AE'), 30);
});

test('applyExtractionRule — static-wgi reads .wgi.indicators[code].value', () => {
  // WGI keys are World-Bank standard codes (VA.EST, PV.EST, etc.)
  const rule = { type: 'static-wgi', code: 'RL.EST' };
  const sources = { staticRecord: { wgi: { indicators: { 'RL.EST': { value: 1.2 } } } } };
  assert.equal(applyExtractionRule(rule, sources, 'DE'), 1.2);
});

test('WGI extraction rules stay aligned with the canonical shared key list', () => {
  const wgiRegistryIds = [
    'wgiVoiceAccountability',
    'wgiPoliticalStability',
    'wgiGovernmentEffectiveness',
    'wgiRegulatoryQuality',
    'wgiRuleOfLaw',
    'wgiControlOfCorruption',
  ];
  const extractionCodes = wgiRegistryIds.map((id) => EXTRACTION_RULES[id]?.code);
  assert.deepEqual(extractionCodes, wgiIndicatorKeys);
});

test('applyExtractionRule — static-wgi-mean averages all six WGI sub-pillars', () => {
  const rule = { type: 'static-wgi-mean' };
  const values = [1.0, -1.0, 0.5, -0.5, 2.0, 0.0];
  const indicators = Object.fromEntries(wgiIndicatorKeys.map((key, i) => [key, { value: values[i] }]));
  const sources = { staticRecord: { wgi: { indicators } } };
  assert.equal(applyExtractionRule(rule, sources, 'DE'), values.reduce((sum, value) => sum + value, 0) / wgiIndicatorKeys.length);
});

test('applyExtractionRule — static-wgi-mean ignores WGI keys outside the scorer contract', () => {
  const rule = { type: 'static-wgi-mean' };
  const values = [1.0, -1.0, 0.5, -0.5, 2.0, 0.0];
  const indicators = {
    ...Object.fromEntries(wgiIndicatorKeys.map((key, i) => [key, { value: values[i] }])),
    'ROGUE.EST': { value: -2.5 },
  };
  const sources = { staticRecord: { wgi: { indicators } } };
  assert.equal(applyExtractionRule(rule, sources, 'DE'), values.reduce((sum, value) => sum + value, 0) / wgiIndicatorKeys.length);
});

test('applyExtractionRule — missing values return null (pairwise-drop contract)', () => {
  const rule = { type: 'static-path', path: ['iea', 'energyImportDependency', 'value'] };
  assert.equal(applyExtractionRule(rule, {}, 'AE'), null);
  assert.equal(applyExtractionRule(rule, { staticRecord: null }, 'AE'), null);
  assert.equal(applyExtractionRule(rule, { staticRecord: { iea: null } }, 'AE'), null);
});

test('applyExtractionRule — not-implemented rules short-circuit to null', () => {
  const rule = { type: 'not-implemented', reason: 'test' };
  assert.equal(applyExtractionRule(rule, {}, 'AE'), null);
});

test('applyExtractionRule — summarize-cyber wires through exported scorer helper', () => {
  const rule = { type: 'summarize-cyber' };
  const cyber = { threats: [{ country: 'AE', severity: 'CRITICALITY_LEVEL_CRITICAL' }] };
  // Pass a stub helper to prove the rule dispatches through it.
  const helpers = {
    summarizeCyber: (raw, cc) => ({
      weightedCount: raw.threats.filter((t) => t.country === cc).length * 3,
    }),
  };
  assert.equal(applyExtractionRule(rule, { cyber }, 'AE', helpers), 3);
  // Without the helper available, rule falls back to null.
  assert.equal(applyExtractionRule(rule, { cyber }, 'AE', {}), null);
});

test('applyExtractionRule — summarize-outages-penalty computes 4/2/1 weighting', () => {
  const rule = { type: 'summarize-outages-penalty' };
  const outages = { outages: [] };
  const helpers = {
    summarizeOutages: () => ({ total: 1, major: 2, partial: 3 }),
  };
  // penalty = 1*4 + 2*2 + 3*1 = 11
  assert.equal(applyExtractionRule(rule, { outages }, 'AE', helpers), 11);
});

test('applyExtractionRule — displacement-field reads per-country entry by field name', () => {
  const rule = { type: 'displacement-field', field: 'totalDisplaced' };
  const displacement = {};
  const helpers = {
    getCountryDisplacement: () => ({ totalDisplaced: 12345, hostTotal: 678 }),
  };
  assert.equal(applyExtractionRule(rule, { displacement }, 'SY', helpers), 12345);
});

test('applyExtractionRule — count-trade-restrictions uses scorer-exported counter', () => {
  const rule = { type: 'count-trade-restrictions' };
  const tradeRestrictions = { restrictions: [] };
  const helpers = { countTradeRestrictions: () => 5 };
  assert.equal(applyExtractionRule(rule, { tradeRestrictions }, 'AE', helpers), 5);
  // Zero coerces to null (pairwise-drop contract for empty signals).
  assert.equal(applyExtractionRule(rule, { tradeRestrictions }, 'AE', { countTradeRestrictions: () => 0 }), null);
});

test('applyExtractionRule — imported-fossil-dependence recomputes the scorer composite', () => {
  // PR 1 §3.2: the scorer computes
  //   importedFossilDependence = fossilElectricityShare × max(netImports, 0) / 100
  // Extractor MUST recompute the same composite, otherwise gate-9's
  // effective-influence measurement for this indicator is wrong for
  // every net-exporter (composite collapses to 0) and under-reports
  // every net-importer (modulated by netImports).
  const rule = { type: 'imported-fossil-dependence-composite' };

  // Net importer: fossilShare 80% × max(60, 0) / 100 = 48
  const netImporter = {
    staticRecord: { iea: { energyImportDependency: { value: 60 } } },
    bulkV1: {
      'resilience:fossil-electricity-share:v1': { countries: { AE: { value: 80 } } },
    },
  };
  assert.equal(applyExtractionRule(rule, netImporter, 'AE'), 48);

  // Net exporter: max(-40, 0) = 0 → composite = 0 regardless of fossilShare
  const netExporter = {
    staticRecord: { iea: { energyImportDependency: { value: -40 } } },
    bulkV1: {
      'resilience:fossil-electricity-share:v1': { countries: { NO: { value: 90 } } },
    },
  };
  assert.equal(applyExtractionRule(rule, netExporter, 'NO'), 0);

  // Missing either input → null
  assert.equal(applyExtractionRule(rule, { staticRecord: null, bulkV1: {} }, 'XX'), null);
  assert.equal(applyExtractionRule(rule, {
    staticRecord: { iea: { energyImportDependency: { value: 50 } } },
    bulkV1: { 'resilience:fossil-electricity-share:v1': { countries: {} } },
  }, 'XX'), null);
});

test('applyExtractionRule — aquastat stress vs availability gated by indicator tag', () => {
  // Mirror scoreAquastatValue in _dimension-scorers.ts: both indicators
  // share .aquastat.value, but the .aquastat.indicator tag classifies
  // which family the reading belongs to. A stress-family country must
  // NOT contribute a reading to the availability extractor, and vice
  // versa, otherwise the Pearson correlation mixes two different
  // construct scales.
  const stressRule = { type: 'static-aquastat-stress' };
  const availabilityRule = { type: 'static-aquastat-availability' };

  const stressCountry = { staticRecord: { aquastat: { value: 42, indicator: 'Water stress (withdrawal/availability)' } } };
  const availabilityCountry = { staticRecord: { aquastat: { value: 1500, indicator: 'Renewable water availability per capita' } } };
  const unknownCountry = { staticRecord: { aquastat: { value: 99, indicator: 'Some unrecognised tag' } } };
  const missingCountry = { staticRecord: { aquastat: { value: null, indicator: 'stress' } } };

  // Stress-tagged country: only the stress extractor returns the value.
  assert.equal(applyExtractionRule(stressRule, stressCountry, 'AE'), 42);
  assert.equal(applyExtractionRule(availabilityRule, stressCountry, 'AE'), null);

  // Availability-tagged country: only the availability extractor returns.
  assert.equal(applyExtractionRule(availabilityRule, availabilityCountry, 'DE'), 1500);
  assert.equal(applyExtractionRule(stressRule, availabilityCountry, 'DE'), null);

  // Unknown tag: neither extractor returns (pairwise-drop).
  assert.equal(applyExtractionRule(stressRule, unknownCountry, 'XX'), null);
  assert.equal(applyExtractionRule(availabilityRule, unknownCountry, 'XX'), null);

  // Missing value: null regardless of tag.
  assert.equal(applyExtractionRule(stressRule, missingCountry, 'XX'), null);
});

test('applyExtractionRule — aquastatScore returns scorer-normalized semantic score', () => {
  const rule = { type: 'static-aquastat-score' };
  const stressCountry = { staticRecord: { aquastat: { value: 42, indicator: 'Water stress (withdrawal/availability)' } } };
  const availabilityCountry = { staticRecord: { aquastat: { value: 1500, indicator: 'Renewable water availability per capita' } } };
  const unknownPercentCountry = { staticRecord: { aquastat: { value: 75, indicator: 'Some unrecognised tag' } } };

  assert.ok(Math.abs(applyExtractionRule(rule, stressCountry, 'AE') - 58) < 1e-9);
  assert.equal(applyExtractionRule(rule, availabilityCountry, 'DE'), 30);
  assert.equal(applyExtractionRule(rule, unknownPercentCountry, 'XX'), 75);
});
