import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

import {
  spearman,
  pearson,
  rankArray,
  detectOutliers,
  fetchWorldRiskIndex,
  HYPOTHESES,
  benchmarkCliExitCode,
  isStrictValidationCli,
  runBenchmark,
  currentCacheFormulaLocal,
  currentMethodologyFormulaLocal,
} from '../scripts/benchmark-resilience-external.mjs';

describe('rankArray', () => {
  it('assigns sequential ranks for distinct values', () => {
    assert.deepEqual(rankArray([10, 30, 20]), [1, 3, 2]);
  });

  it('assigns average ranks for tied values', () => {
    assert.deepEqual(rankArray([10, 20, 20, 30]), [1, 2.5, 2.5, 4]);
  });

  it('handles single element', () => {
    assert.deepEqual(rankArray([5]), [1]);
  });

  it('handles all tied', () => {
    assert.deepEqual(rankArray([7, 7, 7]), [2, 2, 2]);
  });
});

describe('pearson', () => {
  it('returns 1 for perfectly correlated arrays', () => {
    const r = pearson([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]);
    assert.ok(Math.abs(r - 1) < 1e-10, `expected ~1, got ${r}`);
  });

  it('returns -1 for perfectly inversely correlated arrays', () => {
    const r = pearson([1, 2, 3, 4, 5], [10, 8, 6, 4, 2]);
    assert.ok(Math.abs(r - (-1)) < 1e-10, `expected ~-1, got ${r}`);
  });

  it('returns near 0 for uncorrelated arrays', () => {
    const r = pearson([1, 2, 3, 4, 5, 6], [3, 1, 6, 2, 5, 4]);
    assert.ok(Math.abs(r) < 0.5, `expected near 0, got ${r}`);
  });

  it('returns NaN for arrays shorter than 3', () => {
    assert.ok(Number.isNaN(pearson([1, 2], [3, 4])));
  });

  it('returns 0 when one array is constant', () => {
    assert.equal(pearson([1, 1, 1, 1], [1, 2, 3, 4]), 0);
  });
});

describe('spearman', () => {
  it('returns 1 for monotonically increasing relationship', () => {
    const r = spearman([1, 2, 3, 4, 5], [10, 20, 30, 40, 50]);
    assert.ok(Math.abs(r - 1) < 1e-10, `expected ~1, got ${r}`);
  });

  it('returns -1 for monotonically decreasing relationship', () => {
    const r = spearman([1, 2, 3, 4, 5], [50, 40, 30, 20, 10]);
    assert.ok(Math.abs(r - (-1)) < 1e-10, `expected ~-1, got ${r}`);
  });

  it('handles non-linear monotonic relationship', () => {
    const r = spearman([1, 2, 3, 4, 5], [1, 4, 9, 16, 25]);
    assert.ok(Math.abs(r - 1) < 1e-10, `expected ~1 for monotonic, got ${r}`);
  });

  it('returns NaN for arrays shorter than 3', () => {
    assert.ok(Number.isNaN(spearman([1], [2])));
  });
});

describe('detectOutliers', () => {
  it('returns empty for small arrays', () => {
    assert.deepEqual(detectOutliers([1, 2, 3], [3, 2, 1], ['A', 'B', 'C']), []);
  });

  it('detects an outlier in synthetic data', () => {
    const wm = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
    const ext = [100, 90, 80, 70, 60, 50, 40, 30, 20, 500];
    const codes = ['AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ'];
    const outliers = detectOutliers(wm, ext, codes);
    assert.ok(outliers.length > 0, 'expected at least one outlier');
    assert.ok(outliers.some(o => o.countryCode === 'JJ'), 'expected JJ to be an outlier');
  });

  it('returns empty when relationship is perfectly linear', () => {
    const wm = [10, 20, 30, 40, 50, 60, 70, 80];
    const ext = [80, 70, 60, 50, 40, 30, 20, 10];
    const codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
    const outliers = detectOutliers(wm, ext, codes);
    assert.equal(outliers.length, 0, 'perfect linear should have no outliers');
  });
});

describe('HYPOTHESES', () => {
  // INFORM (negative) + HDI (positive) + WRI (negative). FSI was dropped
  // (no fresh bulk data) and ND-GAIN is deferred until the seeder can unzip
  // the 2026 archive — both documented at the top of benchmark-resilience-external.mjs.
  it('has 3 hypothesis entries', () => {
    assert.equal(HYPOTHESES.length, 3);
  });

  it('each hypothesis has required fields', () => {
    for (const h of HYPOTHESES) {
      assert.ok(h.index, 'missing index');
      assert.ok(h.pillar, 'missing pillar');
      assert.ok(['positive', 'negative'].includes(h.direction), `invalid direction: ${h.direction}`);
      assert.ok(typeof h.minSpearman === 'number', 'minSpearman must be a number');
      assert.ok(h.minSpearman > 0 && h.minSpearman < 1, `minSpearman out of range: ${h.minSpearman}`);
    }
  });

  it('INFORM expects negative correlation', () => {
    const inform = HYPOTHESES.find(h => h.index === 'INFORM');
    assert.equal(inform.direction, 'negative');
  });

  it('HDI expects positive correlation', () => {
    const hdi = HYPOTHESES.find(h => h.index === 'HDI');
    assert.equal(hdi.direction, 'positive');
  });

  it('WorldRiskIndex expects negative correlation', () => {
    const wri = HYPOTHESES.find(h => h.index === 'WorldRiskIndex');
    assert.equal(wri.direction, 'negative');
  });
});

describe('fetchWorldRiskIndex', () => {
  const originalFetch = globalThis.fetch;

  it('uses the WRI vulnerability component rather than the exposure-dominated composite W score', async () => {
    globalThis.fetch = async () => new Response([
      'WRI.Country,ISO3.Code,Year,W,V',
      'United States,USA,2023,99,12',
      'United States,USA,2024,98,13',
      'United Kingdom,GBR,2024,88,22',
    ].join('\n'));
    try {
      const { scores, source } = await fetchWorldRiskIndex();
      assert.equal(source, 'live');
      assert.equal(scores.get('US'), 13, 'latest-year V component should be used, not W');
      assert.equal(scores.get('GB'), 22);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it('accepts a dotted WRI.Vulnerability column if HDX renames the short V header', async () => {
    globalThis.fetch = async () => new Response([
      'WRI.Country,ISO3.Code,Year,W,WRI.Vulnerability',
      'United States,USA,2024,98,31',
      'United Kingdom,GBR,2024,88,24',
    ].join('\n'));
    try {
      const { scores } = await fetchWorldRiskIndex();
      assert.equal(scores.get('US'), 31);
      assert.equal(scores.get('GB'), 24);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });
});

describe('benchmark CLI exit policy', () => {
  it('does not fail non-strict cron mode for cold-start skips', () => {
    assert.equal(benchmarkCliExitCode({ skipped: true, reason: 'no-wm-scores' }, false), 0);
    assert.equal(benchmarkCliExitCode({ skipped: true, reason: 'no-wm-scores' }, true), 1);
  });

  it('fails completed runs with failed hypotheses even outside strict mode', () => {
    const result = { hypotheses: [{ index: 'INFORM', pass: true }, { index: 'HDI', pass: false }] };
    assert.equal(benchmarkCliExitCode(result, false), 1);
    assert.equal(benchmarkCliExitCode(result, true), 1);
  });

  it('recognizes explicit strict mode only', () => {
    assert.equal(isStrictValidationCli(['node', 'script.mjs'], {}), false);
    assert.equal(isStrictValidationCli(['node', 'script.mjs', '--strict'], {}), true);
    assert.equal(isStrictValidationCli(['node', 'script.mjs'], { RESILIENCE_VALIDATION_STRICT: '1' }), true);
  });
});

describe('runBenchmark (mocked)', () => {
  it('produces correct output shape with mock data', async () => {
    const wmScores = new Map([
      ['US', 85], ['GB', 78], ['DE', 80], ['FR', 76], ['JP', 82],
      ['IN', 45], ['BR', 50], ['NG', 30], ['SO', 20], ['CH', 88],
    ]);

    const mockInform = async () => ({
      scores: new Map([
        ['US', 2.1], ['GB', 2.5], ['DE', 2.3], ['FR', 2.8], ['JP', 2.0],
        ['IN', 5.5], ['BR', 4.8], ['NG', 7.2], ['SO', 8.5], ['CH', 1.8],
      ]),
      source: 'mock',
    });

    const mockNdGain = async () => ({
      scores: new Map([
        ['US', 72], ['GB', 70], ['DE', 71], ['FR', 68], ['JP', 73],
        ['IN', 42], ['BR', 45], ['NG', 35], ['SO', 28], ['CH', 75],
      ]),
      source: 'mock',
    });

    const mockWri = async () => ({
      scores: new Map([
        ['US', 3.8], ['GB', 4.2], ['DE', 3.6], ['FR', 4.5], ['JP', 5.1],
        ['IN', 7.1], ['BR', 5.9], ['NG', 9.3], ['SO', 12.1], ['CH', 2.9],
      ]),
      source: 'mock',
    });

    const mockFsi = async () => ({
      scores: new Map([
        ['US', 38], ['GB', 36], ['DE', 30], ['FR', 35], ['JP', 28],
        ['IN', 72], ['BR', 68], ['NG', 98], ['SO', 112], ['CH', 25],
      ]),
      source: 'mock',
    });

    const result = await runBenchmark({
      wmScores,
      fetchInform: mockInform,
      fetchHdi: mockNdGain,  // HDI test fixture reused — same correlation pattern
      fetchWri: mockWri,
      dryRun: true,
    });

    assert.ok(result.generatedAt > 0, 'missing generatedAt');
    assert.equal(result._formula, currentCacheFormulaLocal());
    assert.equal(result.methodologyFormula, currentMethodologyFormulaLocal());
    assert.ok(result.license, 'missing license note');
    assert.equal(result.hypotheses.length, 3, 'expected 3 hypotheses');
    assert.ok(result.correlations.INFORM, 'missing INFORM correlation');
    assert.ok(result.correlations.HDI, 'missing HDI correlation');
    assert.ok(result.correlations.WorldRiskIndex, 'missing WorldRiskIndex correlation');
    assert.ok(Array.isArray(result.outliers), 'outliers must be an array');
    assert.ok(result.sourceStatus, 'missing sourceStatus');

    for (const [, corr] of Object.entries(result.correlations)) {
      assert.ok(typeof corr.spearman === 'number', 'spearman must be number');
      assert.ok(typeof corr.pearson === 'number', 'pearson must be number');
      assert.ok(typeof corr.n === 'number', 'n must be number');
      assert.equal(corr.n, 10, `expected 10 countries, got ${corr.n}`);
    }

    for (const h of result.hypotheses) {
      assert.ok(typeof h.pass === 'boolean', 'pass must be boolean');
      assert.ok(h.index, 'hypothesis must have index');
      assert.ok(h.pillar, 'hypothesis must have pillar');
    }
  });

  it('INFORM shows negative correlation with mock data', async () => {
    const wmScores = new Map([
      ['US', 85], ['GB', 78], ['DE', 80], ['FR', 76], ['JP', 82],
      ['IN', 45], ['BR', 50], ['NG', 30], ['SO', 20], ['CH', 88],
    ]);

    const mockInform = async () => ({
      scores: new Map([
        ['US', 2.1], ['GB', 2.5], ['DE', 2.3], ['FR', 2.8], ['JP', 2.0],
        ['IN', 5.5], ['BR', 4.8], ['NG', 7.2], ['SO', 8.5], ['CH', 1.8],
      ]),
      source: 'mock',
    });
    const emptyFetcher = async () => ({ scores: new Map(), source: 'mock' });

    const result = await runBenchmark({
      wmScores,
      fetchInform: mockInform,
      fetchHdi: emptyFetcher,
      fetchWri: emptyFetcher,
      dryRun: true,
    });

    const informCorr = result.correlations.INFORM;
    assert.ok(informCorr.spearman < 0, `INFORM spearman should be negative, got ${informCorr.spearman}`);
  });

  it('handles empty external indices gracefully', async () => {
    const wmScores = new Map([['US', 85], ['GB', 78], ['DE', 80]]);
    const emptyFetcher = async () => ({ scores: new Map(), source: 'unavailable' });

    const result = await runBenchmark({
      wmScores,
      fetchInform: emptyFetcher,
      fetchHdi: emptyFetcher,
      fetchWri: emptyFetcher,
      dryRun: true,
    });

    assert.equal(result.hypotheses.filter(h => h.pass).length, 0, 'no hypotheses should pass with empty data');
    assert.equal(result.outliers.length, 0, 'no outliers with empty data');
  });

  it('outlier entries have commentary', async () => {
    const n = 20;
    const wmScores = new Map();
    const informScores = new Map();
    const codes = [];
    for (let i = 0; i < n; i++) {
      const code = String.fromCharCode(65 + Math.floor(i / 26)) + String.fromCharCode(65 + (i % 26));
      codes.push(code);
      wmScores.set(code, 10 + i * 4);
      informScores.set(code, 9 - i * 0.4);
    }
    wmScores.set(codes[n - 1], 10);
    informScores.set(codes[n - 1], 0.5);

    const result = await runBenchmark({
      wmScores,
      fetchInform: async () => ({ scores: informScores, source: 'mock' }),
      fetchHdi: async () => ({ scores: new Map(), source: 'mock' }),
      fetchWri: async () => ({ scores: new Map(), source: 'mock' }),
      dryRun: true,
    });

    for (const o of result.outliers) {
      assert.ok(o.commentary, `outlier ${o.countryCode} missing commentary`);
      assert.ok(typeof o.residual === 'number', 'residual must be number');
      assert.ok(o.index, 'outlier must have index name');
    }
  });
});
