/**
 * A resumed keepsake download must never splice two different archives together.
 *
 * The download endpoint re-resolves `export_current` on EVERY request, and a download ticket
 * outlives several redemptions. So the dangerous sequence was:
 *
 *   guest's 500 MB download drops at 500 MB
 *   → host takes a photo down (epoch bumps, rebuild lands, old generation pruned)
 *   → client resumes with `Range: bytes=500000000-`
 *   → server seeks 500 MB into a DIFFERENT file of a different length and streams it
 *   → the client concatenates the two halves into a structurally corrupt ZIP
 *
 * Nothing anywhere logged an error. The archive is the one artifact the whole event exists to
 * produce, so a partial is now served only against a matching `If-Range` validator.
 */
import { test, expect } from '../../fixtures/test';
import { seedUpload } from '../../helpers/seed';
import { BASE } from '../../helpers/env';

test.describe('Export — a resume cannot splice two archives', () => {
  test('partial content requires a matching If-Range; a blind Range restarts instead', async ({
    host,
  }) => {
    test.setTimeout(60_000);
    const bearer = { Authorization: `Bearer ${host.jwt}` };

    await seedUpload(host.jwt, { caption: 'resumable' });

    const rel = await fetch(`${BASE}/api/v1/host/gallery/release`, {
      method: 'POST',
      headers: bearer,
    });
    expect(rel.status).toBe(204);

    await expect
      .poll(
        async () => {
          const res = await fetch(`${BASE}/api/v1/export/status`, { headers: bearer });
          return (await res.json()).zip?.status;
        },
        { timeout: 45_000, intervals: [500] }
      )
      .toBe('done');

    const mint = async () => {
      const r = await fetch(`${BASE}/api/v1/export/ticket?kind=zip`, {
        method: 'POST',
        headers: bearer,
      });
      return (await r.json()).ticket as string;
    };
    const url = async () => `${BASE}/api/v1/export/zip?ticket=${encodeURIComponent(await mint())}`;

    // 1. The full download advertises a validator. Without one a browser will not even attempt a
    //    resume, so this header is what makes the feature work at all — and it is what the
    //    partial below is checked against.
    const full = await fetch(await url());
    expect(full.status).toBe(200);
    const etag = full.headers.get('etag');
    expect(etag, 'the archive must carry an ETag or no client can resume safely').toBeTruthy();
    expect(full.headers.get('accept-ranges')).toBe('bytes');

    // 2. A resume that PROVES continuity gets its partial.
    const resumed = await fetch(await url(), {
      headers: { Range: 'bytes=0-99', 'If-Range': etag! },
    });
    expect(resumed.status, 'a matching If-Range must still get 206').toBe(206);
    expect(resumed.headers.get('content-range')).toMatch(/^bytes 0-99\/\d+$/);

    // 3. A resume that cannot prove it — `curl -C -`, `wget -c`, the Android download manager —
    //    gets the whole file instead of a silently spliced one. Restarting a download is a cost;
    //    a corrupt keepsake is not recoverable.
    const blind = await fetch(await url(), { headers: { Range: 'bytes=0-99' } });
    expect(blind.status, 'a Range with no If-Range must NOT be served as a partial').toBe(200);
    expect(blind.headers.get('content-range')).toBeNull();

    // 4. And a stale validator — the exact case that used to splice — is refused a partial too.
    const stale = await fetch(await url(), {
      headers: { Range: 'bytes=0-99', 'If-Range': '"Gallery.some-other-event.99.zip-123"' },
    });
    expect(stale.status, 'an If-Range from a different generation must not get a partial').toBe(
      200
    );
  });
});
