/**
 * Regression guard — the keepsake download must actually download, in WebKit.
 *
 * The bug this exists to catch: `/export` streams the archive by pointing a HIDDEN,
 * SAME-ORIGIN iframe at `/api/v1/export/zip` (deliberately — a top-level navigation to a
 * 404/429 would unload the PWA). Caddy stamped a site-wide `X-Frame-Options: DENY` that
 * also covered `/api/*`. Blink hands a `Content-Disposition: attachment` response to the
 * download manager at the network layer, so Chromium never noticed; WebKit enforces XFO on
 * the frame navigation FIRST and aborts the load. Result: on iOS Safari — the app's primary
 * platform — tapping Download did nothing, silently, with no error anywhere.
 *
 * Why the old suite was structurally blind to it:
 *   - `06-export` ran on `chromium-desktop` only (webkit-iphone's testMatch excluded it),
 *   - and no test in the entire suite ever CLICKED a download button; every archive
 *     assertion used Node `fetch`, which has no frame and therefore no XFO enforcement.
 *
 * So this spec must keep both properties to be worth anything: a real click, in WebKit.
 */
import { test, expect } from '../../fixtures/test';
import { ExportPage } from '../../page-objects';
import { BASE } from '../../helpers/env';

const SLUG = 'e2e-test-event';

function post(path: string, jwt: string) {
  return fetch(BASE + path, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` } });
}

/** The real export job runs image processing; give it head-room over the tiny fixtures. */
async function releaseAndWait(jwt: string) {
  expect((await post('/api/v1/host/gallery/release', jwt)).status).toBe(204);
  await expect
    .poll(
      async () => {
        const res = await fetch(BASE + '/api/v1/export/status', {
          headers: { Authorization: `Bearer ${jwt}` },
        });
        const s = await res.json();
        return s.released === true && s.zip?.status === 'done';
      },
      { timeout: 60_000, intervals: [500] }
    )
    .toBe(true);
}

test.describe('Export — the download actually fires in the browser', () => {
  test.slow();

  test('clicking Download triggers a real download event', async ({ page, host, signIn, db }) => {
    await db.setExportReleased(SLUG, false);
    await releaseAndWait(host.jwt);

    await signIn(page, host);
    const exportPage = new ExportPage(page);
    await exportPage.goto();
    await expect(exportPage.zipDownloadButton).toBeEnabled({ timeout: 10_000 });

    // Capture the frame-level refusal that XFO produces, so a failure reports the CAUSE
    // rather than just a timeout. WebKit logs "Refused to display ... in a frame because it
    // set 'X-Frame-Options'"; Chromium logs nothing here, which is the whole problem.
    const refusals: string[] = [];
    page.on('console', (m) => {
      if (/X-Frame-Options|Refused to display/i.test(m.text())) refusals.push(m.text());
    });

    const downloadPromise = page.waitForEvent('download', { timeout: 30_000 });
    await exportPage.zipDownloadButton.click();

    const download = await downloadPromise.catch((err) => {
      throw new Error(
        `No download event fired after clicking the ZIP button.` +
          (refusals.length
            ? ` The browser refused the iframe navigation: ${refusals.join(' | ')}`
            : ' No X-Frame-Options refusal was logged; check the ticket/readiness path.') +
          `\n${err}`
      );
    });

    expect(download.suggestedFilename()).toMatch(/\.zip$/i);
    // The stream must produce real bytes, not a zero-length placeholder.
    const path = await download.path();
    expect(path).toBeTruthy();
    expect(refusals, 'no frame should have been refused').toEqual([]);
  });

  test('the export endpoints are framable same-origin; everything else stays DENY', async () => {
    // Locks the Caddy carve-out itself, independently of any browser. Cheap, and it fails
    // loudly at the exact layer that regressed if someone reinstates a blanket DENY.
    for (const path of ['/api/v1/export/zip', '/api/v1/export/html']) {
      const res = await fetch(BASE + path);
      expect(res.headers.get('x-frame-options')?.toUpperCase(), `${path} must be framable`).toBe(
        'SAMEORIGIN'
      );
    }

    for (const path of ['/', '/api/v1/feed', '/api/v1/event']) {
      const res = await fetch(BASE + path);
      expect(res.headers.get('x-frame-options')?.toUpperCase(), `${path} must stay DENY`).toBe(
        'DENY'
      );
    }
  });
});
