/**
 * Regression guard — the keepsake extracts to files a guest can actually open.
 *
 * `ZipEntryBuilder::new` leaves the external file attribute at zero, and async_zip's host
 * compatibility defaults to Unix — so every entry in BOTH archives was written with a stored mode
 * of 0000. `unzip -Z` showed `?---------` on every line.
 *
 * Windows Explorer ignores Unix modes, which is exactly why this survived. On Linux and macOS,
 * `unzip` faithfully applies what the archive asks for, and the guest gets a folder of photos none
 * of which they can open — plus an index.html the browser refuses with ERR_ACCESS_DENIED.
 *
 * Unconditional: it affected every keepsake ever produced, no hostile input required. And it is
 * invisible server-side — the export succeeds, the ZIP is well-formed, the job writes `done`,
 * /export/status is green. The only way to see it is to extract the real artifact and try to read
 * it, which is what this does.
 *
 * Found while chasing an unrelated ERR_ACCESS_DENIED that looked like a Playwright quirk.
 */
import { test, expect } from '../../fixtures/test';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, rmSync, readFileSync, statSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { seedUpload } from '../../helpers/seed';
import { BASE } from '../../helpers/env';

/** Walk every file under `dir`, ignoring the archives we dropped there ourselves. */
function walk(dir: string, skip: string[] = []): string[] {
  return readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
    const p = join(dir, e.name);
    if (e.isDirectory()) return walk(p, skip);
    return skip.includes(e.name) ? [] : [p];
  });
}

test.describe('Export — the archives extract to readable files', () => {
  test('every entry in both keepsake archives is owner-readable', async ({ host, guest, db }) => {
    test.setTimeout(120_000);
    const bearer = { Authorization: `Bearer ${host.jwt}` };

    const g = await guest('Archivist');
    const id = await seedUpload(g.jwt, { caption: 'ein Foto' });
    await expect.poll(() => db.compressionStatus(id), { timeout: 30_000 }).toBe('done');

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

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

    for (const kind of ['zip', 'html'] as const) {
      const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=${kind}`, {
        method: 'POST',
        headers: bearer,
      });
      const { ticket } = (await ticketRes.json()) as { ticket: string };
      const dl = await fetch(`${BASE}/api/v1/export/${kind}?ticket=${encodeURIComponent(ticket)}`);
      expect(dl.status, `downloading the ${kind} archive`).toBe(200);

      const dir = mkdtempSync(join(tmpdir(), `eventsnap-perms-${kind}-`));
      try {
        const zipPath = join(dir, 'archive.zip');
        writeFileSync(zipPath, Buffer.from(await dl.arrayBuffer()));

        // The mode as STORED in the archive — this is what a guest's unzip will apply. Reading it
        // from the central directory catches the defect even on a filesystem that would mask it.
        const listing = execFileSync('unzip', ['-Z', zipPath], { encoding: 'utf8' });
        const modes = listing
          .split('\n')
          .filter((l) => /^[?d-][rwx-]{9}\s/.test(l))
          .map((l) => l.slice(0, 10));
        expect(modes.length, `${kind}: no entries listed`).toBeGreaterThan(0);
        for (const m of modes) {
          expect(m, `${kind}: an entry is stored mode ${m} — the guest cannot open it`).toMatch(
            /^.r[w-]-/
          );
        }

        // And extraction really does produce readable files.
        execFileSync('unzip', ['-qo', zipPath, '-d', dir]);
        const files = walk(dir, ['archive.zip']);
        expect(files.length, `${kind}: nothing extracted`).toBeGreaterThan(0);
        for (const f of files) {
          expect(statSync(f).mode & 0o400, `${f} is not owner-readable`).toBeTruthy();
          // The assertion that matters to a guest: the bytes actually come out.
          expect(() => readFileSync(f)).not.toThrow();
        }
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    }
  });
});
