import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
  parseEmberCsv,
  buildAllCountriesMap,
  EMBER_KEY_PREFIX,
  EMBER_ALL_KEY,
  EMBER_META_KEY,
  EMBER_TTL_SECONDS,
} from '../scripts/seed-ember-electricity.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURE_DIR = resolve(__dirname, 'fixtures');

// ─── Fixture builders ──────────────────────────────────────────────────────

// Real Ember CSV column names (must match COLS in seed-ember-electricity.mjs)
const ISO3_COL = 'ISO 3 code';
const DATE_COL = 'Date';
const SERIES_COL = 'Variable';
const UNIT_COL = 'Unit';
const VALUE_COL = 'Value';

function makeRow(overrides = {}) {
  return {
    [ISO3_COL]: 'USA',
    [DATE_COL]: '2024-01-01',
    [SERIES_COL]: 'Coal',
    [UNIT_COL]: 'TWh',
    [VALUE_COL]: '100',
    ...overrides,
  };
}

/**
 * Build a minimal long-format CSV string from an array of row objects.
 * @param {Array<Record<string, string>>} rows
 * @param {string} [eol] line terminator (default LF). Upstream switched to bare CR in June 2026.
 */
function buildCsv(rows, eol = '\n') {
  const headers = [ISO3_COL, DATE_COL, SERIES_COL, UNIT_COL, VALUE_COL, 'Category'];
  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((h) => row[h] ?? '').join(','));
  }
  return lines.join(eol);
}

function threeCountryFixture() {
  // USA: 2024-01 — Fossil=400, Renewables=300, Nuclear=100, Coal=200, Gas=200, Total=800
  // DEU: 2024-01 — Fossil=200, Renewables=500, Nuclear=0, Coal=100, Gas=100, Total=700
  // FRA: 2024-01 — Fossil=50, Renewables=100, Nuclear=400, Coal=20, Gas=30, Total=550
  const rows = [
    // USA
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '400' }),
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Renewables',       [VALUE_COL]: '300' }),
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Nuclear',          [VALUE_COL]: '100' }),
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Coal',             [VALUE_COL]: '200' }),
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Gas',              [VALUE_COL]: '200' }),
    makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '800' }),
    // DEU (Germany)
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '200' }),
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Renewables',       [VALUE_COL]: '500' }),
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Nuclear',          [VALUE_COL]: '0'   }),
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Coal',             [VALUE_COL]: '100' }),
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Gas',              [VALUE_COL]: '100' }),
    makeRow({ [ISO3_COL]: 'DEU', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '700' }),
    // FRA (France)
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '50'  }),
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Renewables',       [VALUE_COL]: '100' }),
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Nuclear',          [VALUE_COL]: '400' }),
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Coal',             [VALUE_COL]: '20'  }),
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Gas',              [VALUE_COL]: '30'  }),
    makeRow({ [ISO3_COL]: 'FRA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '550' }),
  ];
  return buildCsv(rows);
}

// ─── Tests ────────────────────────────────────────────────────────────────

describe('parseEmberCsv', () => {
  it('parses a minimal 3-country fixture', () => {
    const csv = threeCountryFixture();
    const result = parseEmberCsv(csv);
    assert.ok(result instanceof Map);
    assert.ok(result.size >= 1, 'should have at least 1 country');
  });

  it('includes one entry per fixture country (US, DE, FR)', () => {
    const csv = threeCountryFixture();
    const result = parseEmberCsv(csv);
    assert.ok(result.has('US'), 'should have US');
    assert.ok(result.has('DE'), 'should have DE');
    assert.ok(result.has('FR'), 'should have FR');
  });

  it('computes fossilShare = (fossil_twh / total_twh) * 100', () => {
    const csv = threeCountryFixture();
    const result = parseEmberCsv(csv);
    const us = result.get('US');
    assert.ok(us != null, 'US entry missing');
    // USA: fossil=400, total=800 → 50%
    assert.ok(Math.abs(us.fossilShare - 50) < 0.01, `fossilShare should be 50, got ${us.fossilShare}`);
  });

  it('computes renewShare correctly', () => {
    const csv = threeCountryFixture();
    const result = parseEmberCsv(csv);
    const us = result.get('US');
    // USA: renewables=300, total=800 → 37.5%
    assert.ok(Math.abs(us.renewShare - 37.5) < 0.01, `renewShare should be 37.5, got ${us.renewShare}`);
  });

  it('sets dataMonth to YYYY-MM from date field', () => {
    const csv = threeCountryFixture();
    const result = parseEmberCsv(csv);
    const us = result.get('US');
    assert.equal(us.dataMonth, '2024-01');
  });

  it('selects the most recent month when a country has two months of data', () => {
    const rows = [
      // Jan 2024
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '100' }),
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '200' }),
      // Feb 2024 (later — should be selected)
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '2024-02-01', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '80'  }),
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '2024-02-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '210' }),
    ];
    const csv = buildCsv(rows);
    const result = parseEmberCsv(csv);
    const gb = result.get('GB');
    assert.ok(gb != null, 'GB entry missing');
    assert.equal(gb.dataMonth, '2024-02', 'should use the later month');
    // Feb: fossil=80, total=210 → ~38.1%
    assert.ok(Math.abs(gb.fossilShare - (80 / 210) * 100) < 0.01);
  });

  it('skips rows where unit !== TWh', () => {
    const rows = [
      makeRow({ [ISO3_COL]: 'AUS', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil',           [UNIT_COL]: 'GW', [VALUE_COL]: '100' }),
      makeRow({ [ISO3_COL]: 'AUS', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [UNIT_COL]: 'GW', [VALUE_COL]: '200' }),
    ];
    const csv = buildCsv(rows);
    const result = parseEmberCsv(csv);
    // AUS should not appear since no TWh rows
    assert.ok(!result.has('AU'), 'AU should be excluded when unit is GW');
  });

  it('skips countries where Total Generation is missing', () => {
    const rows = [
      makeRow({ [ISO3_COL]: 'JPN', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Fossil', [VALUE_COL]: '100' }),
      // No Total Generation row for JPN
    ];
    const csv = buildCsv(rows);
    const result = parseEmberCsv(csv);
    assert.ok(!result.has('JP'), 'JP should be excluded when Total Generation is absent');
  });
});

// ---------------------------------------------------------------------------
// Upstream format regression (June 2026): Ember regenerated the monthly CSV
// with (1) bare CR (\r) line endings instead of \n/\r\n, and (2) DD/MM/YYYY
// dates instead of YYYY-MM-DD. Both broke seed-ember-electricity in prod
// ("Ember CSV: no data rows"). These tests pin both formats.
// ---------------------------------------------------------------------------

describe('upstream line-ending changes (CR-only / CRLF)', () => {
  it('parses a CR-only (\\r) CSV — the June 2026 regression', () => {
    const csv = threeCountryFixture().replace(/\n/g, '\r');
    assert.ok(!csv.includes('\n'), 'fixture must be CR-only to reproduce the bug');
    const result = parseEmberCsv(csv);
    assert.ok(result.has('US') && result.has('DE') && result.has('FR'), 'all 3 countries parse from CR-only CSV');
  });

  it('parses a CRLF (\\r\\n) CSV', () => {
    const csv = threeCountryFixture().replace(/\n/g, '\r\n');
    const result = parseEmberCsv(csv);
    assert.ok(result.has('US'), 'US parses from CRLF CSV');
  });
});

describe('upstream date-format change (DD/MM/YYYY)', () => {
  it('extracts dataMonth from DD/MM/YYYY dates', () => {
    const rows = [
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '01/03/2026', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '400' }),
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '01/03/2026', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '800' }),
    ];
    const result = parseEmberCsv(buildCsv(rows));
    const us = result.get('US');
    assert.ok(us != null, 'US entry missing');
    assert.equal(us.dataMonth, '2026-03', 'DD/MM/YYYY 01/03/2026 should yield 2026-03');
  });

  it('selects the most recent month across YEARS with DD/MM/YYYY (no lexical-sort bug)', () => {
    // Lexically "01/12/2025" > "01/01/2026", so naive string max picks the wrong (older) month.
    const rows = [
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '01/12/2025', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '100' }),
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '01/12/2025', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '200' }),
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '01/01/2026', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '80'  }),
      makeRow({ [ISO3_COL]: 'GBR', [DATE_COL]: '01/01/2026', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '210' }),
    ];
    const result = parseEmberCsv(buildCsv(rows));
    const gb = result.get('GB');
    assert.ok(gb != null, 'GB entry missing');
    assert.equal(gb.dataMonth, '2026-01', 'should select Jan 2026, not Dec 2025');
    assert.ok(Math.abs(gb.fossilShare - (80 / 210) * 100) < 0.01, 'shares come from the Jan 2026 month');
  });

  it('still parses legacy YYYY-MM-DD dates (backward compatible)', () => {
    const result = parseEmberCsv(threeCountryFixture());
    assert.equal(result.get('US').dataMonth, '2024-01');
  });
});

describe('real-world combined format (CR-only + DD/MM/YYYY)', () => {
  it('parses a CSV with both June-2026 upstream changes at once', () => {
    const rows = [
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '01/04/2026', [SERIES_COL]: 'Fossil',           [VALUE_COL]: '400' }),
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '01/04/2026', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '800' }),
    ];
    const csv = buildCsv(rows, '\r'); // CR-only + DD/MM/YYYY
    const result = parseEmberCsv(csv);
    const us = result.get('US');
    assert.ok(us != null, 'US should parse from the real-world format');
    assert.equal(us.dataMonth, '2026-04');
    assert.ok(Math.abs(us.fossilShare - 50) < 0.01);
  });
});

describe('schema sentinel', () => {
  it('throws when Fossil series is not present in any row', () => {
    const rows = [
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Coal',             [VALUE_COL]: '100' }),
      makeRow({ [ISO3_COL]: 'USA', [DATE_COL]: '2024-01-01', [SERIES_COL]: 'Total Generation', [VALUE_COL]: '200' }),
    ];
    const csv = buildCsv(rows);
    assert.throws(
      () => parseEmberCsv(csv),
      /Fossil.*series not found|schema changed/i,
      'should throw when Fossil series is absent',
    );
  });
});

describe('buildAllCountriesMap', () => {
  it('returns compact shape without seededAt or country fields', () => {
    const csv = threeCountryFixture();
    const countries = parseEmberCsv(csv);
    const allMap = buildAllCountriesMap(countries);
    for (const [, entry] of Object.entries(allMap)) {
      assert.ok(!('seededAt' in entry), 'compact map should not have seededAt');
      assert.ok(!('country' in entry), 'compact map should not have country');
      assert.ok('dataMonth' in entry, 'compact map should have dataMonth');
      assert.ok('fossilShare' in entry, 'compact map should have fossilShare');
    }
  });

  it('has one entry per parsed country', () => {
    const csv = threeCountryFixture();
    const countries = parseEmberCsv(csv);
    const allMap = buildAllCountriesMap(countries);
    assert.equal(Object.keys(allMap).length, countries.size);
  });
});

describe('exported constants', () => {
  it('EMBER_KEY_PREFIX is correct', () => {
    assert.equal(EMBER_KEY_PREFIX, 'energy:ember:v1:');
  });

  it('EMBER_ALL_KEY is correct', () => {
    assert.equal(EMBER_ALL_KEY, 'energy:ember:v1:_all');
  });

  it('EMBER_META_KEY is correct', () => {
    assert.equal(EMBER_META_KEY, 'seed-meta:energy:ember');
  });

  it('EMBER_TTL_SECONDS is 259200 (72h)', () => {
    assert.equal(EMBER_TTL_SECONDS, 259200);
  });

  it('EMBER_TTL_SECONDS covers 3x the 24h daily cron interval', () => {
    const dailyIntervalSeconds = 24 * 60 * 60; // 86400
    assert.ok(
      EMBER_TTL_SECONDS >= 3 * dailyIntervalSeconds,
      `TTL ${EMBER_TTL_SECONDS}s should be >= ${3 * dailyIntervalSeconds}s (3× daily)`,
    );
  });
});

describe('count-drop guard math', () => {
  it('45/60 is acceptable (75% threshold)', () => {
    const prevCount = 60;
    const newCount = 45;
    const ratio = newCount / prevCount;
    assert.ok(ratio >= 0.75, '45/60 = 75% should pass the guard');
  });

  it('44/60 triggers the guard (below 75%)', () => {
    const prevCount = 60;
    const newCount = 44;
    const ratio = newCount / prevCount;
    assert.ok(ratio < 0.75, '44/60 ≈ 73.3% should trigger the guard');
  });
});

describe('pipeline failure detection logic (non-transactional, e.g. Phase B meta write)', () => {
  it('detects a partial pipeline failure when one command errors', () => {
    const results = [{ result: 'OK' }, { result: 'OK' }, { error: 'NOSCRIPT' }, { result: 'OK' }];
    const failures = results.filter((r) => r?.error || r?.result === 'ERR');
    assert.equal(failures.length, 1, 'should detect 1 failed command');
  });

  it('treats all-OK results as no failures', () => {
    const results = [{ result: 'OK' }, { result: 'OK' }, { result: 'OK' }];
    const failures = results.filter((r) => r?.error || r?.result === 'ERR');
    assert.equal(failures.length, 0, 'all-OK pipeline should have 0 failures');
  });

  it('detects ERR-result commands as failures', () => {
    const results = [{ result: 'OK' }, { result: 'ERR' }];
    const failures = results.filter((r) => r?.error || r?.result === 'ERR');
    assert.equal(failures.length, 1, 'ERR result should count as failure');
  });
});

describe('pipeline shape (plain pipeline, no MULTI/EXEC)', () => {
  it('builds SET commands for each country plus _all', () => {
    const countries = new Map([['US', { foo: 1 }], ['DE', { foo: 2 }]]);
    const cmds = [];
    for (const [iso2, payload] of countries) {
      cmds.push(['SET', `${EMBER_KEY_PREFIX}${iso2}`, JSON.stringify(payload), 'EX', EMBER_TTL_SECONDS]);
    }
    cmds.push(['SET', EMBER_ALL_KEY, '{}', 'EX', EMBER_TTL_SECONDS]);

    assert.equal(cmds.length, 3, '2 country SET + 1 _all SET');
    assert.ok(cmds.every(c => c[0] === 'SET'), 'all commands are SET');
  });

  it('includes DEL for obsolete keys in the pipeline', () => {
    const oldAllMap = { US: {}, DE: {}, JP: {} };
    const newCountryKeys = new Set(['US', 'DE']);
    const oldIso2Set = new Set(Object.keys(oldAllMap));

    const cmds = [];
    cmds.push(['SET', `${EMBER_KEY_PREFIX}US`, '{}', 'EX', EMBER_TTL_SECONDS]);
    cmds.push(['SET', `${EMBER_KEY_PREFIX}DE`, '{}', 'EX', EMBER_TTL_SECONDS]);
    cmds.push(['SET', EMBER_ALL_KEY, '{}', 'EX', EMBER_TTL_SECONDS]);
    for (const iso2 of oldIso2Set) {
      if (!newCountryKeys.has(iso2)) {
        cmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    const delCmds = cmds.filter(c => c[0] === 'DEL');
    assert.equal(delCmds.length, 1);
    assert.equal(delCmds[0][1], `${EMBER_KEY_PREFIX}JP`);
  });
});

describe('publish pipeline includes DEL for obsolete per-country keys', () => {
  it('generates DEL commands for keys in old _all but not in new dataset', () => {
    const oldAllMap = { US: {}, DE: {}, JP: {} };
    const newCountryKeys = new Set(['US', 'DE']); // JP removed
    const oldIso2Set = new Set(Object.keys(oldAllMap));

    const delCmds = [];
    for (const iso2 of oldIso2Set) {
      if (!newCountryKeys.has(iso2)) {
        delCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    assert.equal(delCmds.length, 1, 'should DEL 1 obsolete key');
    assert.equal(delCmds[0][1], `${EMBER_KEY_PREFIX}JP`);
  });

  it('generates no DEL when new dataset is a superset of old', () => {
    const oldAllMap = { US: {}, DE: {} };
    const newCountryKeys = new Set(['US', 'DE', 'FR']);
    const oldIso2Set = new Set(Object.keys(oldAllMap));

    const delCmds = [];
    for (const iso2 of oldIso2Set) {
      if (!newCountryKeys.has(iso2)) {
        delCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    assert.equal(delCmds.length, 0, 'no DELs needed when new is superset');
  });

  it('handles empty old _all (first run)', () => {
    const oldAllMap = null;
    const newCountryKeys = new Set(['US', 'DE']);
    const oldIso2Set = oldAllMap && typeof oldAllMap === 'object' ? new Set(Object.keys(oldAllMap)) : new Set();

    const delCmds = [];
    for (const iso2 of oldIso2Set) {
      if (!newCountryKeys.has(iso2)) {
        delCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    assert.equal(delCmds.length, 0, 'no DELs on first run');
  });
});

describe('preservePreviousSnapshot restore DELs new-only keys', () => {
  it('generates DEL for keys in new dataset but not in old stash', () => {
    const oldAllMap = { US: {}, DE: {} };
    const newCountryKeys = new Set(['US', 'DE', 'XX']);

    const delCmds = [];
    const oldIso2Set = new Set(Object.keys(oldAllMap));
    for (const iso2 of newCountryKeys) {
      if (!oldIso2Set.has(iso2)) {
        delCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    assert.equal(delCmds.length, 1, 'should DEL 1 new-only key');
    assert.equal(delCmds[0][1], `${EMBER_KEY_PREFIX}XX`);
  });

  it('generates no DEL when new dataset is subset of old stash', () => {
    const oldAllMap = { US: {}, DE: {}, FR: {} };
    const newCountryKeys = new Set(['US', 'DE']);

    const delCmds = [];
    const oldIso2Set = new Set(Object.keys(oldAllMap));
    for (const iso2 of newCountryKeys) {
      if (!oldIso2Set.has(iso2)) {
        delCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    assert.equal(delCmds.length, 0, 'no DELs when new is subset of old');
  });
});

describe('preservePreviousSnapshot cleanup of new-only keys', () => {
  it('includes DEL commands for new-only keys when restoring from stash', () => {
    const stashedAllMap = { US: {}, DE: {} };
    const newCountryKeys = new Set(['US', 'DE', 'XX', 'YY']);

    const restoreCmds = [];
    // Simulate restore SET commands
    for (const [iso2, val] of Object.entries(stashedAllMap)) {
      restoreCmds.push(['SET', `${EMBER_KEY_PREFIX}${iso2}`, JSON.stringify(val), 'EX', EMBER_TTL_SECONDS]);
    }
    restoreCmds.push(['SET', EMBER_ALL_KEY, JSON.stringify(stashedAllMap), 'EX', EMBER_TTL_SECONDS]);

    // Add DEL for new-only keys
    const oldIso2Set = new Set(Object.keys(stashedAllMap));
    for (const iso2 of newCountryKeys) {
      if (!oldIso2Set.has(iso2)) {
        restoreCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
      }
    }

    const delCmds = restoreCmds.filter(c => c[0] === 'DEL');
    assert.equal(delCmds.length, 2, 'should DEL XX and YY');
    const delKeys = delCmds.map(c => c[1]).sort();
    assert.deepEqual(delKeys, [`${EMBER_KEY_PREFIX}XX`, `${EMBER_KEY_PREFIX}YY`]);
  });

  it('skips DEL when newCountryKeys is null (error before parse)', () => {
    const stashedAllMap = { US: {} };
    const newCountryKeys = null;

    const restoreCmds = [];
    if (newCountryKeys) {
      const oldIso2Set = new Set(Object.keys(stashedAllMap));
      for (const iso2 of newCountryKeys) {
        if (!oldIso2Set.has(iso2)) {
          restoreCmds.push(['DEL', `${EMBER_KEY_PREFIX}${iso2}`]);
        }
      }
    }

    assert.equal(restoreCmds.length, 0, 'no DELs when newCountryKeys is null');
  });
});

describe('health cascade: seedError priority over hasData', () => {
  function resolveStatus({ seedError, hasData, seedStale, size }) {
    let status;
    if (seedError === true) {
      status = 'SEED_ERROR';
    } else if (!hasData) {
      status = 'EMPTY';
    } else if (size === 0) {
      status = 'EMPTY_DATA';
    } else if (seedStale === true) {
      status = 'STALE_SEED';
    } else {
      status = 'OK';
    }
    return status;
  }

  it('returns SEED_ERROR when seedError=true and hasData=false', () => {
    const status = resolveStatus({ seedError: true, hasData: false, seedStale: true, size: 0 });
    assert.equal(status, 'SEED_ERROR', 'seedError should take priority over !hasData');
  });

  it('returns SEED_ERROR when seedError=true and hasData=true', () => {
    const status = resolveStatus({ seedError: true, hasData: true, seedStale: false, size: 100 });
    assert.equal(status, 'SEED_ERROR', 'seedError should take priority even when data exists');
  });

  it('returns EMPTY when seedError=false and hasData=false', () => {
    const status = resolveStatus({ seedError: false, hasData: false, seedStale: false, size: 0 });
    assert.equal(status, 'EMPTY', 'no error + no data should be EMPTY');
  });

  it('returns OK when seedError=false and hasData=true and not stale', () => {
    const status = resolveStatus({ seedError: false, hasData: true, seedStale: false, size: 100 });
    assert.equal(status, 'OK');
  });
});

describe('health endpoint status agreement for error meta', () => {
  it('seed-health.js logic emits "error" for meta.status="error"', () => {
    // Simulates seed-health.js lines 131-148 logic
    const meta = { fetchedAt: Date.now(), recordCount: 100, status: 'error', error: 'test failure' };
    const isError = meta.status === 'error';
    const ageMs = Date.now() - (meta.fetchedAt || 0);
    const maxStalenessMs = 1440 * 2 * 60 * 1000;
    const stale = ageMs > maxStalenessMs || isError;
    const status = stale ? (isError ? 'error' : 'stale') : 'ok';
    assert.equal(status, 'error', 'seed-health.js should report "error" for meta.status=error');
  });

  it('health.js SEED_ERROR is the correct status for meta.status="error" (not STALE_SEED)', () => {
    // Verifies the expected contract: meta.status=error → SEED_ERROR (not STALE_SEED)
    // This test documents the intended behavior after the fix
    const meta = { fetchedAt: Date.now(), recordCount: 100, status: 'error', error: 'test failure' };
    const seedError = meta?.status === 'error';
    const seedStale = seedError; // error implies stale

    let status;
    if (seedError) {
      status = 'SEED_ERROR';
    } else if (seedStale) {
      status = 'STALE_SEED';
    } else {
      status = 'OK';
    }
    assert.equal(status, 'SEED_ERROR', 'explicit error meta should yield SEED_ERROR, not STALE_SEED');
  });
});

describe('preservePreviousSnapshot recordCount fallback', () => {
  it('uses null (not 0) when existingMeta is unavailable', () => {
    const existingMeta = null;
    const recordCount = existingMeta?.recordCount ?? null;
    assert.equal(recordCount, null, 'should be null, not 0');
    const serialized = JSON.stringify({ recordCount });
    assert.ok(serialized.includes('"recordCount":null'), 'null should be serialized');
  });

  it('preserves existing recordCount when meta is readable', () => {
    const existingMeta = { recordCount: 180, fetchedAt: Date.now() };
    const recordCount = existingMeta?.recordCount ?? null;
    assert.equal(recordCount, 180);
  });

  it('null recordCount does not enable count-drop guard', () => {
    const prevMeta = { recordCount: null, status: 'error' };
    const guardActive = prevMeta && typeof prevMeta === 'object' && prevMeta.recordCount > 0;
    assert.equal(guardActive, false, 'null recordCount should not activate guard');
  });
});

describe('dataWritten flag prevents stash restore after successful pipeline', () => {
  it('skips restore when dataWritten=true (data is correct, only meta failed)', () => {
    const stashedAllMap = { US: {}, DE: {} };
    const dataWritten = true;
    const shouldRestore = stashedAllMap && typeof stashedAllMap === 'object' && !dataWritten;
    assert.equal(shouldRestore, false, 'should not restore stash when data was written successfully');
  });

  it('allows restore when dataWritten=false (pipeline failed or never ran)', () => {
    const stashedAllMap = { US: {}, DE: {} };
    const dataWritten = false;
    const shouldRestore = stashedAllMap && typeof stashedAllMap === 'object' && !dataWritten;
    assert.ok(shouldRestore, 'should restore stash when data was not written');
  });

  it('skips TTL extension when dataWritten=true and no stash', () => {
    const stashedAllMap = null;
    const dataWritten = true;
    const shouldExtendTtl = !dataWritten;
    assert.equal(shouldExtendTtl, false, 'should not extend TTL when data is already correct');
  });
});

// ---------------------------------------------------------------------------
// Golden fixture: upstream CSV format regression guard
// ---------------------------------------------------------------------------

describe('golden fixture (Ember monthly CSV)', () => {
  const csv = readFileSync(resolve(FIXTURE_DIR, 'ember-monthly-sample.csv'), 'utf-8');

  it('parseEmberCsv returns at least 1 country from the fixture', () => {
    const countries = parseEmberCsv(csv);
    assert.ok(countries.size >= 1, `expected >=1 country, got ${countries.size}`);
  });

  it('fixture contains US and DE', () => {
    const countries = parseEmberCsv(csv);
    assert.ok(countries.has('US'), 'US missing');
    assert.ok(countries.has('DE'), 'DE missing');
  });

  it('each entry has expected share fields', () => {
    const expected = ['fossilShare', 'renewShare', 'nuclearShare', 'coalShare', 'gasShare', 'demandTwh', 'dataMonth'];
    const countries = parseEmberCsv(csv);
    for (const [iso2, entry] of countries) {
      for (const field of expected) {
        assert.ok(field in entry, `${iso2} missing field ${field}`);
      }
    }
  });

  it('share values are numbers or null', () => {
    const countries = parseEmberCsv(csv);
    for (const [iso2, entry] of countries) {
      for (const key of ['fossilShare', 'renewShare', 'coalShare', 'gasShare']) {
        const val = entry[key];
        assert.ok(val === null || typeof val === 'number', `${iso2}.${key} should be number|null, got ${typeof val}`);
      }
    }
  });

  it('dataMonth is YYYY-MM format', () => {
    const countries = parseEmberCsv(csv);
    for (const [iso2, entry] of countries) {
      assert.match(entry.dataMonth, /^\d{4}-\d{2}$/, `${iso2} dataMonth format wrong: ${entry.dataMonth}`);
    }
  });

  it('US fossilShare is 50% (400/800)', () => {
    const countries = parseEmberCsv(csv);
    const us = countries.get('US');
    assert.ok(us != null);
    assert.ok(Math.abs(us.fossilShare - 50) < 0.01, `US fossilShare should be 50, got ${us.fossilShare}`);
  });
});
