import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import vm from 'node:vm';

const source = await readFile(new URL('../service-worker.js', import.meta.url), 'utf8');

function loadWorker({ scope = 'https://example.test/', cacheKeys = [], currentHits = new Map(), fetchImpl = async () => { throw new Error('offline'); } } = {}) {
  const listeners = new Map();
  const deleted = [];
  const puts = [];
  let cacheOpenCount = 0;
  const currentCache = {
    addAll: async () => {},
    put: async request => { puts.push(typeof request === 'string' ? request : request.url); },
    match: async request => currentHits.get(typeof request === 'string' ? request : request.url),
  };
  const caches = {
    keys: async () => cacheKeys,
    delete: async key => { deleted.push(key); return true; },
    open: async () => { cacheOpenCount += 1; return currentCache; },
    match: async () => { throw new Error('global cache matching must not be used'); },
  };
  const self = {
    registration: { scope },
    addEventListener: (type, handler) => listeners.set(type, handler),
    skipWaiting: async () => {},
    clients: { claim: async () => {} },
  };
  vm.runInNewContext(source, { self, caches, fetch: fetchImpl, URL, Promise, String });
  return { listeners, deleted, puts, getCacheOpenCount: () => cacheOpenCount };
}

async function dispatchExtendable(handler) {
  let pending;
  handler({ waitUntil(value) { pending = value; } });
  await pending;
}

async function dispatchFetch(handler, request) {
  let response;
  handler({ request, respondWith(value) { response = value; } });
  return response;
}

test('root activation deletes only its obsolete Timmy caches including the legacy v4 cache', async () => {
  const { listeners, deleted } = loadWorker({
    cacheKeys: [
      'timmy-shell-v4',
      'timmy-shell:/:v4',
      'timmy-shell:/:v5',
      'timmy-shell:/timmy-staging/:v4',
      'sibling-shared-origin-cache',
    ],
  });

  await dispatchExtendable(listeners.get('activate'));

  assert.deepEqual(deleted.sort(), ['timmy-shell-v4', 'timmy-shell:/:v4']);
});

test('offline shell lookup uses only the current named cache', async () => {
  const request = { method: 'GET', url: 'https://example.test/app.js' };
  const current = { body: 'current app' };
  const { listeners } = loadWorker({ currentHits: new Map([[request.url, current]]) });

  const response = await dispatchFetch(listeners.get('fetch'), request);

  assert.equal(await response, current);
});

test('scoped API GET requests bypass the shell cache entirely', async () => {
  const request = { method: 'GET', url: 'https://example.test/timmy-staging/api/healthz' };
  const networkResponse = { headers: { get: () => 'no-store' }, clone: () => ({}) };
  const { listeners, puts, getCacheOpenCount } = loadWorker({
    scope: 'https://example.test/timmy-staging/',
    fetchImpl: async () => networkResponse,
  });

  const response = await dispatchFetch(listeners.get('fetch'), request);

  assert.equal(await response, networkResponse);
  assert.equal(getCacheOpenCount(), 0);
  assert.deepEqual(puts, []);
});

test('no-store shell responses are returned without being cached', async () => {
  const request = { method: 'GET', url: 'https://example.test/styles.css' };
  const networkResponse = { headers: { get: () => 'private, no-store' }, clone: () => ({}) };
  const { listeners, puts } = loadWorker({ fetchImpl: async () => networkResponse });

  const response = await dispatchFetch(listeners.get('fetch'), request);
  await response;
  await Promise.resolve();

  assert.deepEqual(puts, []);
});
