import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { safeFetch, safeFetchText, getFetchMetrics, inferFetchSource } from '../apis/utils/fetch.mjs';
import { formatStaleAlert, shouldSendStaleAlert } from '../lib/stale-alerts.mjs';

test('safeFetch reports HTML as degraded JSON response', async () => {
  const originalFetch = globalThis.fetch;
  const source = 'unit-html-once';
  globalThis.fetch = async () => ({
    ok: true,
    status: 200,
    headers: { get: () => 'text/html' },
    text: async () => '<html>not json</html>',
  });
  try {
    const data = await safeFetch('https://example.test/json', { retries: 0, source });
    assert.match(data.error, /Expected JSON/);
    const bucket = getFetchMetrics().bySource[source];
    assert.equal(bucket.requests, 1);
    assert.equal(bucket.ok, 0);
    assert.equal(bucket.failed, 1);
    assert.equal(bucket.lastStatus, 200);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test('safeFetch records HTTP failure once with status and bytes', async () => {
  const originalFetch = globalThis.fetch;
  const source = 'unit-http-failure-once';
  globalThis.fetch = async () => ({
    ok: false,
    status: 503,
    headers: { get: () => 'application/json' },
    text: async () => 'service unavailable',
  });
  try {
    const data = await safeFetch('https://example.test/fail', { retries: 0, source });
    assert.match(data.error, /HTTP 503/);
    const bucket = getFetchMetrics().bySource[source];
    assert.equal(bucket.requests, 1);
    assert.equal(bucket.ok, 0);
    assert.equal(bucket.failed, 1);
    assert.equal(bucket.lastStatus, 503);
    assert.equal(bucket.bytes, 'service unavailable'.length);
    assert.match(bucket.lastError, /HTTP 503/);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test('safeFetch retry metrics count one record per attempt', async () => {
  const originalFetch = globalThis.fetch;
  const source = 'unit-retry-attempts';
  let calls = 0;
  globalThis.fetch = async () => {
    calls += 1;
    if (calls === 1) {
      return {
        ok: false,
        status: 502,
        headers: { get: () => 'application/json' },
        text: async () => 'bad gateway',
      };
    }
    return {
      ok: true,
      status: 200,
      headers: { get: () => 'application/json' },
      text: async () => '{"ok":true}',
    };
  };
  try {
    const data = await safeFetch('https://example.test/retry', { retries: 1, source });
    assert.equal(data.ok, true);
    assert.equal(calls, 2);
    const bucket = getFetchMetrics().bySource[source];
    assert.equal(bucket.requests, 2);
    assert.equal(bucket.ok, 1);
    assert.equal(bucket.failed, 1);
    assert.equal(bucket.lastStatus, 200);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test('safeFetchText returns text and byte count', async () => {
  const originalFetch = globalThis.fetch;
  globalThis.fetch = async () => ({
    ok: true,
    status: 200,
    text: async () => 'hello',
  });
  try {
    const data = await safeFetchText('https://example.test/rss', { retries: 0, source: 'rss-unit' });
    assert.equal(data.text, 'hello');
    assert.equal(data.bytes, 5);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test('safeFetch attributes unlabelled requests to a stable provider source', async () => {
  const originalFetch = globalThis.fetch;
  globalThis.fetch = async () => ({
    ok: true,
    status: 200,
    headers: { get: () => 'application/json' },
    text: async () => '{"observations":[]}',
  });
  try {
    const data = await safeFetch('https://api.fred.stlouisfed.org/fred/series/observations?series_id=VIXCLS', { retries: 0 });
    assert.deepEqual(data, { observations: [] });
    const bucket = getFetchMetrics().bySource.FRED;
    assert.ok(bucket.requests >= 1);
    assert.equal(bucket.lastStatus, 200);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test('inferFetchSource returns provider names and host fallback', () => {
  assert.equal(inferFetchSource('https://api.bls.gov/publicAPI/v2/timeseries/data/CPI'), 'BLS');
  assert.equal(inferFetchSource('https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC'), 'YahooFinance');
  assert.equal(inferFetchSource('https://unknown.example.test/path'), 'unknown.example.test');
});

test('SSE endpoint sends reconnect guidance and clears heartbeat timer', () => {
  const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
  const config = readFileSync(new URL('../crucix.config.mjs', import.meta.url), 'utf8');
  assert.match(config, /sseHeartbeatIntervalMs/);
  assert.match(server, /retry: 10000\\n/);
  assert.match(server, /setInterval\(\(\) =>/);
  assert.match(server, /: heartbeat/);
  assert.match(server, /clearInterval\(heartbeat\)/);
  assert.match(server, /X-Accel-Buffering/);
});

test('intelligence store defines durable memory and prediction lifecycle tables', () => {
  const store = readFileSync(new URL('../lib/intelligence-store.mjs', import.meta.url), 'utf8');
  assert.match(store, /CREATE TABLE IF NOT EXISTS events/);
  assert.match(store, /stable_id TEXT NOT NULL UNIQUE/);
  assert.match(store, /hypothesis TEXT/);
  assert.match(store, /evidence_json TEXT/);
  assert.match(store, /outcome_state TEXT DEFAULT 'open'/);
  assert.match(store, /evaluatePredictions/);
  assert.match(store, /queryMemory/);
  assert.match(store, /listPredictions/);
});

test('server exposes memory-backed query APIs and dashboard memory action', () => {
  const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
  const html = readFileSync(new URL('../dashboard/public/jarvis.html', import.meta.url), 'utf8');
  assert.match(server, /\/api\/memory\/search/);
  assert.match(server, /\/api\/memory\/predictions/);
  assert.match(server, /action === 'memory'/);
  assert.match(html, /runTerminalAction\('memory'\)/);
});

test('terminal action endpoints avoid URL tokens and include hardening gates', () => {
  const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
  assert.match(server, /app\.post\('\/api\/action'/);
  assert.match(server, /app\.post\('\/api\/sweep'/);
  assert.match(server, /x-crucix-token/);
  assert.match(server, /sameOriginPost/);
  assert.match(server, /rateLimitTerminalAction/);
  assert.match(server, /auditTerminalAction/);
  assert.doesNotMatch(server, /req\.query\.token/);
});

test('dashboard exposes token configuration flow without devtools edits', () => {
  const html = readFileSync(new URL('../dashboard/public/jarvis.html', import.meta.url), 'utf8');
  assert.match(html, /configureTerminalActionToken/);
  assert.match(html, /crucix_sweep_token/);
  assert.match(html, /x-crucix-token/);
  assert.match(html, /SET TOKEN/);
});

test('server dashboard shell does not embed an operational snapshot', () => {
  const html = readFileSync(new URL('../dashboard/public/jarvis.html', import.meta.url), 'utf8');
  assert.match(html, /let D = createDashboardShellData\(\);/);
  assert.doesNotMatch(html, /2026-04-03T16:18:10\.188Z/);
  assert.doesNotMatch(html, /Trump announced new strikes on Iran/);
});

test('server dashboard fetches api data before initialization', () => {
  const html = readFileSync(new URL('../dashboard/public/jarvis.html', import.meta.url), 'utf8');
  const serverMode = html.indexOf('if (canProbeApi)');
  const apiFetch = html.indexOf("fetch('/api/data')");
  const firstInitAfterServerMode = html.indexOf('init();', serverMode);

  assert.ok(serverMode > -1);
  assert.ok(apiFetch > serverMode);
  assert.ok(firstInitAfterServerMode > apiFetch);
});

test('stale alert is skipped for fresh health and resets active key', () => {
  const state = { lastStaleAlertKey: 'old', lastStaleAlertAt: 100 };
  const decision = shouldSendStaleAlert({ stale: false }, state, { now: 200 });
  assert.equal(decision.send, false);
  assert.equal(decision.reason, 'not_stale');
  assert.equal(state.lastStaleAlertKey, null);
});

test('stale alert sends once and deduplicates during cooldown', () => {
  const state = {};
  const health = {
    stale: true,
    lastSuccessfulSweep: '2026-05-17T08:00:00.000Z',
    lastSweepError: 'network timeout',
    sourcesFailed: 2,
    sourcesDegraded: 1,
  };

  const first = shouldSendStaleAlert(health, state, { now: 1_000, cooldownMs: 60_000 });
  const second = shouldSendStaleAlert(health, state, { now: 2_000, cooldownMs: 60_000 });

  assert.equal(first.send, true);
  assert.equal(second.send, false);
  assert.equal(second.reason, 'cooldown');
});

test('stale alert repeats after cooldown', () => {
  const state = {};
  const health = { stale: true, lastSuccessfulSweep: 'a', lastSweepError: 'timeout', sourcesFailed: 1 };

  assert.equal(shouldSendStaleAlert(health, state, { now: 1_000, cooldownMs: 60_000 }).send, true);
  assert.equal(shouldSendStaleAlert(health, state, { now: 62_000, cooldownMs: 60_000 }).send, true);
});

test('stale alert message includes operator context and affected sources', () => {
  const message = formatStaleAlert({
    status: 'stale',
    stale: true,
    dataAgeSeconds: 7200,
    lastSuccessfulSweep: '2026-05-17T08:00:00.000Z',
    lastSweep: '2026-05-17T10:00:00.000Z',
    lastSweepError: 'GDELT timeout',
    sourcesOk: 20,
    sourcesDegraded: 3,
    sourcesFailed: 2,
    sourceHealth: [
      { name: 'GDELT', status: 'degraded', error: 'timeout' },
      { name: 'Reddit', status: 'no_credentials' },
    ],
  }, { dashboardUrl: 'https://terminal.example.test', context: 'failed sweep' });

  assert.match(message, /CRUCIX STALE DATA ALERT/);
  assert.match(message, /Data age: 120 minutes/);
  assert.match(message, /GDELT: degraded \(timeout\)/);
  assert.match(message, /Dashboard: https:\/\/terminal\.example\.test/);
});

test('scenario watchlist feature is wired into sweep, briefing, and dashboard', () => {
  const scenarios = readFileSync(new URL('../lib/scenarios.mjs', import.meta.url), 'utf8');
  const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
  const html = readFileSync(new URL('../dashboard/public/jarvis.html', import.meta.url), 'utf8');
  const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
  assert.match(scenarios, /DEFAULT_SCENARIOS/);
  assert.match(scenarios, /runsDir, 'scenarios\.json'/);
  assert.match(scenarios, /scenario-state\.json/);
  assert.match(scenarios, /watching.*building.*confirmed/s);
  assert.match(server, /evaluateScenarios\(synthesized, delta, RUNS_DIR\)/);
  assert.match(server, /\*Scenario Watchlist\*/);
  assert.match(html, /Scenario Watchlist/);
  assert.match(readme, /runs\/scenarios\.json/);
});
