/**
 * Regression tests for HAPI per-country and GDELT split circuit breakers (PR #879).
 *
 * Root cause: two instances of the shared-breaker anti-pattern fixed in the same
 * audit pass that caught the World Bank breaker bug (PR #877):
 *
 *   1. hapiBreaker — single shared breaker used in a Promise.allSettled loop over
 *      20 countries. 2 failures in any country tripped the breaker for ALL countries,
 *      and the last country's result overwrote the cache for every other country.
 *      Fix: getHapiBreaker(iso2) Map — one breaker per ISO2 country code.
 *
 *   2. gdeltBreaker — one breaker shared between fetchGdeltArticles (military/conflict
 *      queries, 10-min cache) and fetchPositiveGdeltArticles (peace/humanitarian queries,
 *      different topic set). Failures in one function silenced the other, and the 10-min
 *      cache stored whichever query ran last, poisoning the other function's results.
 *      Fix: positiveGdeltBreaker — dedicated breaker for the positive sentiment path.
 */

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, pathToFileURL } from 'node:url';

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

const readSrc = (relPath) => readFileSync(resolve(root, relPath), 'utf-8');

// ============================================================
// 1. Static analysis: conflict/index.ts — per-country HAPI breakers
// ============================================================

describe('conflict/index.ts — per-country HAPI circuit breakers', () => {
  const src = readSrc('src/services/conflict/index.ts');

  // Scoped slices to avoid false positives from comments or unrelated code
  const breakerSection = src.slice(src.indexOf('hapiBreakers'), src.indexOf('hapiBreakers') + 400);
  const fnStart = src.indexOf('export async function fetchHapiSummary');
  assert.ok(fnStart !== -1, 'fetchHapiSummary not found in conflict/index.ts — was it renamed?');
  const fnBody = src.slice(fnStart, src.indexOf('\nexport ', fnStart + 1));

  it('does NOT have a single shared hapiBreaker', () => {
    assert.doesNotMatch(
      src,
      /\bconst\s+hapiBreaker\s*=/,
      'Single shared hapiBreaker must not exist — use getHapiBreaker(iso2) instead',
    );
  });

  it('has a hapiBreakers Map for per-country instances', () => {
    assert.match(
      breakerSection,
      /new\s+Map/,
      'hapiBreakers Map must exist to store per-country circuit breakers',
    );
  });

  it('has a getHapiBreaker(iso2) factory function', () => {
    assert.match(
      src,
      /function\s+getHapiBreaker\s*\(\s*iso2/,
      'getHapiBreaker(iso2) factory function must exist',
    );
  });

  it('fetchHapiSummary calls getHapiBreaker(iso2).execute not a shared breaker', () => {
    assert.match(
      fnBody,
      /getHapiBreaker\s*\(\s*iso2\s*\)\s*\.execute/,
      'fetchHapiSummary must use getHapiBreaker(iso2).execute, not a shared hapiBreaker',
    );
  });

  it('per-country breaker names embed iso2', () => {
    assert.match(
      breakerSection,
      /name\s*:\s*`HDX HAPI:\$\{iso2\}`/,
      'Breaker name must embed iso2 (e.g. "HDX HAPI:US") for unique IndexedDB persistence per country',
    );
  });
});

// ============================================================
// 2. Static analysis: gdelt-intel.ts — split breakers per query type
// ============================================================

describe('gdelt-intel.ts — dedicated circuit breakers per GDELT query type', () => {
  const src = readSrc('src/services/gdelt-intel.ts');

  // Scoped function body slices
  const posStart = src.indexOf('export async function fetchPositiveGdeltArticles');
  assert.ok(posStart !== -1, 'fetchPositiveGdeltArticles not found in gdelt-intel.ts — was it renamed?');
  const posBody = src.slice(posStart, src.indexOf('\nexport ', posStart + 1));
  const regStart = src.indexOf('export async function fetchGdeltArticles');
  assert.ok(regStart !== -1, 'fetchGdeltArticles not found in gdelt-intel.ts — was it renamed?');
  const regBody = src.slice(regStart, src.indexOf('\nexport ', regStart + 1));

  it('has a dedicated positiveGdeltBreaker separate from gdeltBreaker', () => {
    assert.match(
      src,
      /\bpositiveGdeltBreaker\s*=\s*createCircuitBreaker/,
      'positiveGdeltBreaker must be a separate createCircuitBreaker instance',
    );
  });

  it('GDELT breakers have distinct names', () => {
    assert.match(
      src,
      /GDELT Intelligence/,
      'gdeltBreaker must have name "GDELT Intelligence"',
    );
    assert.match(
      src,
      /GDELT Positive/,
      'positiveGdeltBreaker must have name "GDELT Positive"',
    );
  });

  it('fetchGdeltArticles uses gdeltBreaker, NOT positiveGdeltBreaker', () => {
    assert.match(
      regBody,
      /gdeltBreaker\.execute/,
      'fetchGdeltArticles must use gdeltBreaker.execute',
    );
    assert.doesNotMatch(
      regBody,
      /positiveGdeltBreaker\.execute/,
      'fetchGdeltArticles must NOT use positiveGdeltBreaker',
    );
  });

  it('fetchPositiveGdeltArticles uses positiveGdeltBreaker, NOT gdeltBreaker', () => {
    assert.match(
      posBody,
      /positiveGdeltBreaker\.execute/,
      'fetchPositiveGdeltArticles must use positiveGdeltBreaker.execute',
    );
    // word-boundary prevents matching `positiveGdeltBreaker.execute`
    assert.doesNotMatch(
      posBody,
      /\bgdeltBreaker\.execute/,
      'fetchPositiveGdeltArticles must NOT use gdeltBreaker (only positiveGdeltBreaker)',
    );
  });
});

// ============================================================
// 3. Behavioral: circuit breaker isolation
// ============================================================

describe('CircuitBreaker isolation — HAPI per-country independence', () => {
  const CIRCUIT_BREAKER_URL = pathToFileURL(
    resolve(root, 'src/utils/circuit-breaker.ts'),
  ).href;

  it('HAPI: failure in one country does not trip another', async () => {
    const { createCircuitBreaker, clearAllCircuitBreakers } = await import(
      `${CIRCUIT_BREAKER_URL}?t=${Date.now()}`
    );

    clearAllCircuitBreakers();

    try {
      const breakerUS = createCircuitBreaker({ name: 'HDX HAPI:US', cacheTtlMs: 30 * 60 * 1000 });
      const breakerRU = createCircuitBreaker({ name: 'HDX HAPI:RU', cacheTtlMs: 30 * 60 * 1000 });

      const fallback = { summary: null };
      const alwaysFail = () => { throw new Error('HDX HAPI unavailable'); };

      // Force breakerUS into cooldown (2 failures = maxFailures)
      await breakerUS.execute(alwaysFail, fallback); // failure 1
      await breakerUS.execute(alwaysFail, fallback); // failure 2 → cooldown
      assert.equal(breakerUS.isOnCooldown(), true, 'breakerUS should be on cooldown after 2 failures');

      // breakerRU must NOT be affected
      assert.equal(breakerRU.isOnCooldown(), false, 'breakerRU must not be on cooldown when breakerUS fails');

      // breakerRU should still call through successfully
      const goodData = { summary: { countryCode: 'RU', conflictEvents: 12, displacedPersons: 5000 } };
      const result = await breakerRU.execute(async () => goodData, fallback);
      assert.deepEqual(result, goodData, 'breakerRU should return live data unaffected by breakerUS cooldown');
    } finally {
      clearAllCircuitBreakers();
    }
  });

  it('HAPI: different countries cache independently (no cross-country poisoning)', async () => {
    const { createCircuitBreaker, clearAllCircuitBreakers } = await import(
      `${CIRCUIT_BREAKER_URL}?t=${Date.now()}`
    );

    clearAllCircuitBreakers();

    try {
      const breakerUS = createCircuitBreaker({ name: 'HDX HAPI:US', cacheTtlMs: 30 * 60 * 1000 });
      const breakerRU = createCircuitBreaker({ name: 'HDX HAPI:RU', cacheTtlMs: 30 * 60 * 1000 });

      const fallback = { summary: null };
      const usData = { summary: { countryCode: 'US', conflictEvents: 3, displacedPersons: 100 } };
      const ruData = { summary: { countryCode: 'RU', conflictEvents: 47, displacedPersons: 120000 } };

      // Populate both caches with different data
      await breakerUS.execute(async () => usData, fallback);
      await breakerRU.execute(async () => ruData, fallback);

      // Each must return its own cached value; pass a fallback fn that would return wrong data
      const cachedUS = await breakerUS.execute(async () => fallback, fallback);
      const cachedRU = await breakerRU.execute(async () => fallback, fallback);

      assert.equal(cachedUS.summary?.countryCode, 'US',
        'breakerUS cache must return US data, not RU data');
      assert.equal(cachedRU.summary?.countryCode, 'RU',
        'breakerRU cache must return RU data, not US data');
      assert.notEqual(cachedUS.summary?.conflictEvents, cachedRU.summary?.conflictEvents,
        'Cached conflict event counts must be independent per country');
    } finally {
      clearAllCircuitBreakers();
    }
  });
});

describe('CircuitBreaker isolation — GDELT split breaker independence', () => {
  const CIRCUIT_BREAKER_URL = pathToFileURL(
    resolve(root, 'src/utils/circuit-breaker.ts'),
  ).href;

  it('GDELT: positive breaker failure does not trip regular breaker', async () => {
    const { createCircuitBreaker, clearAllCircuitBreakers } = await import(
      `${CIRCUIT_BREAKER_URL}?t=${Date.now()}`
    );

    clearAllCircuitBreakers();

    try {
      const gdelt = createCircuitBreaker({ name: 'GDELT Intelligence', cacheTtlMs: 10 * 60 * 1000 });
      const positive = createCircuitBreaker({ name: 'GDELT Positive', cacheTtlMs: 10 * 60 * 1000 });

      const fallback = { articles: [], totalArticles: 0 };
      const alwaysFail = () => { throw new Error('GDELT API unavailable'); };

      // Force positive breaker into cooldown (2 failures)
      await positive.execute(alwaysFail, fallback); // failure 1
      await positive.execute(alwaysFail, fallback); // failure 2 → cooldown
      assert.equal(positive.isOnCooldown(), true, 'positive breaker should be on cooldown after 2 failures');

      // gdelt breaker must NOT be affected
      assert.equal(gdelt.isOnCooldown(), false, 'gdelt breaker must not be on cooldown when positive fails');

      // gdelt should still call through successfully
      const realArticles = { articles: [{ url: 'https://news.example/military', title: 'Conflict update' }], totalArticles: 1 };
      const result = await gdelt.execute(async () => realArticles, fallback);
      assert.deepEqual(result, realArticles, 'gdelt breaker should return live data unaffected by positive cooldown');
    } finally {
      clearAllCircuitBreakers();
    }
  });

  it('GDELT: regular and positive breakers cache different data independently', async () => {
    const { createCircuitBreaker, clearAllCircuitBreakers } = await import(
      `${CIRCUIT_BREAKER_URL}?t=${Date.now()}`
    );

    clearAllCircuitBreakers();

    try {
      const gdelt = createCircuitBreaker({ name: 'GDELT Intelligence', cacheTtlMs: 10 * 60 * 1000 });
      const positive = createCircuitBreaker({ name: 'GDELT Positive', cacheTtlMs: 10 * 60 * 1000 });

      const fallback = { articles: [], totalArticles: 0 };
      const militaryData = { articles: [{ url: 'https://news.example/military', title: 'Military operations' }], totalArticles: 1 };
      const peaceData    = { articles: [{ url: 'https://good.example/peace', title: 'Peace agreement' }], totalArticles: 1 };

      // Populate both caches with different data
      await gdelt.execute(async () => militaryData, fallback);
      await positive.execute(async () => peaceData, fallback);

      // Each must return its own cached value; pass fallback fn that would return wrong data
      const cachedGdelt    = await gdelt.execute(async () => fallback, fallback);
      const cachedPositive = await positive.execute(async () => fallback, fallback);

      assert.ok(
        cachedGdelt.articles[0]?.url.includes('military'),
        'gdelt cache must return military article URL, not peace article',
      );
      assert.ok(
        cachedPositive.articles[0]?.url.includes('peace'),
        'positive cache must return peace article URL, not military article',
      );
      assert.notEqual(
        cachedGdelt.articles[0]?.url,
        cachedPositive.articles[0]?.url,
        'Cached article URLs must be distinct per breaker (no cross-contamination)',
      );
    } finally {
      clearAllCircuitBreakers();
    }
  });
});
