/**
 * Storage-quota ENFORCEMENT — the branch that runs in production and, until now, in zero tests.
 *
 * `quota_enabled` and `storage_quota_enabled` both default to TRUE in production
 * (upload.rs: `config::get_bool(.., "quota_enabled", true)`), but global-setup turns them off and
 * the per-test TRUNCATE re-asserts them as 'false' before EVERY test. So the entire quota path was
 * dead under test: the pre-check, the `QuotaExceeded` rejection, and — most importantly — the
 * atomic increment
 *
 *     UPDATE "user" SET total_upload_bytes = total_upload_bytes + $2
 *      WHERE id = $1 AND total_upload_bytes + $2 <= $3
 *
 * whose enforcement is `rows_affected() == 0`. That UPDATE is the ONLY thing stopping two
 * concurrent uploads from the same guest (phone + laptop, or a double-tap) from both passing the
 * stale pre-check and both committing. The five existing unit tests cover `quota_limit_bytes()` —
 * the arithmetic that computes the number — which gave the appearance of coverage while the
 * enforcement had none.
 *
 * Blast radius if it regresses: one guest silently fills the disk, and then NOBODY at the wedding
 * can upload. Those photos don't exist anywhere else.
 *
 * These tests steer the limit via `quota_tolerance` (limit = floor(free_disk * tolerance / active))
 * rather than hoping a real disk happens to be nearly full.
 */
import { test, expect } from '../../fixtures/test';
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
import { uploadPausedMidStream } from '../../helpers/upload-client';
import { BASE } from '../../helpers/env';

const SAMPLE_BYTES = readFileSync(join(process.cwd(), 'fixtures', 'media', 'sample.jpg'));
const SIZE = SAMPLE_BYTES.byteLength;

interface Quota {
  enabled: boolean;
  used_bytes: number;
  limit_bytes: number | null;
  active_uploaders: number;
  free_disk_bytes: number | null;
}

const quotaOf = async (jwt: string): Promise<Quota> =>
  (await fetch(BASE + '/api/v1/me/quota', { headers: { Authorization: `Bearer ${jwt}` } })).json();

function upload(jwt: string, name: string) {
  const form = new FormData();
  form.append('file', new Blob([SAMPLE_BYTES], { type: 'image/jpeg' }), name);
  form.append('content_type', 'image/jpeg');
  return fetch(BASE + '/api/v1/upload', {
    method: 'POST',
    headers: { Authorization: `Bearer ${jwt}` },
    body: form,
  });
}

/**
 * Pick a `quota_tolerance` that makes the per-user ceiling land on `targetBytes`.
 * limit = floor(free_disk * tolerance / max(active, 1))  ⇒  tolerance = target * active / free.
 *
 * `staffJwt` reads the calibration inputs, `jwt` is the guest the limit is being aimed at.
 * They must be different tokens: `free_disk_bytes` and `active_uploaders` are raw server
 * telemetry and `/me/quota` zeroes both for non-staff (handlers/me.rs — "must never reach a
 * guest"). Calibrating off the guest's own response divides by zero and yields a NaN
 * tolerance, which is what silently broke this whole describe block.
 */
async function setLimitTo(
  api: any,
  adminToken: string,
  staffJwt: string,
  jwt: string,
  targetBytes: number
): Promise<number> {
  const q = await quotaOf(staffJwt);
  expect(
    q.free_disk_bytes,
    'the disk must be readable, else quota fails OPEN and proves nothing'
  ).toBeTruthy();
  const active = Math.max(q.active_uploaders, 1);
  const tolerance = (targetBytes * active) / (q.free_disk_bytes as number);
  await api.patchConfig(adminToken, { quota_tolerance: tolerance.toExponential(12) });

  // Read back through the GUEST, whose ceiling is the one under test.
  const after = await quotaOf(jwt);
  expect(after.enabled).toBe(true);
  return after.limit_bytes as number;
}

test.describe('Upload — storage quota enforcement', () => {
  test.beforeEach(async ({ api, adminToken }) => {
    await api.patchConfig(adminToken, {
      quota_enabled: 'true',
      storage_quota_enabled: 'true',
      // The per-user ceiling divides the disk budget by
      // `max(active_uploaders, estimated_guest_count, 1)` — the operator's expected headcount is a
      // FLOOR on the divisor, so the ceiling settles early instead of sliding down all evening as
      // guests arrive. It is seeded at 100, and `setLimitTo` below solves for a target using the
      // OBSERVED uploader count, so every limit it aimed for came out 100x too small and every
      // "within the quota" upload 413'd. Pin it to 1 so the divisor is the count the helper
      // actually controls; the floor itself is exercised by the Rust unit tests.
      estimated_guest_count: '1',
    });
  });

  test('an upload that would exceed the quota is rejected with 413 and stores nothing', async ({
    api,
    adminToken,
    guest,
    host,
  }) => {
    const g = await guest('QuotaOver');
    // Ceiling below one file: the very first upload must be refused.
    const limit = await setLimitTo(api, adminToken, host.jwt, g.jwt, Math.floor(SIZE / 2));
    expect(limit).toBeLessThan(SIZE);

    const res = await upload(g.jwt, 'too-big.jpg');
    expect(res.status, 'over-quota upload must be refused').toBe(413);
    expect((await res.json()).error).toBe('quota_exceeded');

    // Nothing was accounted, and nothing reached the feed.
    expect((await quotaOf(g.jwt)).used_bytes).toBe(0);
    const feed = await (
      await fetch(BASE + '/api/v1/feed', { headers: { Authorization: `Bearer ${g.jwt}` } })
    ).json();
    expect(feed.uploads).toHaveLength(0);
  });

  test('an upload within the quota still succeeds (the guard is not simply "always no")', async ({
    api,
    adminToken,
    guest,
    host,
  }) => {
    const g = await guest('QuotaUnder');
    await setLimitTo(api, adminToken, host.jwt, g.jwt, SIZE * 4);

    expect((await upload(g.jwt, 'fine.jpg')).status).toBe(201);
    expect((await quotaOf(g.jwt)).used_bytes).toBe(SIZE);
  });

  test('two CONCURRENT uploads cannot BOTH slip past the quota (atomic increment)', async ({
    api,
    adminToken,
    guest,
    host,
  }) => {
    const g = await guest('QuotaRacer');

    // Room for exactly ONE file.
    const limit = await setLimitTo(api, adminToken, host.jwt, g.jwt, Math.floor(SIZE * 1.5));
    expect(limit).toBeGreaterThanOrEqual(SIZE);
    expect(limit).toBeLessThan(SIZE * 2);

    // Both uploads are held OPEN MID-BODY, and this is the entire point of the test.
    //
    // The handler loads the user row (and therefore snapshots `total_upload_bytes`) at its very
    // FIRST line — before it streams a single byte of the body. So holding both requests inside
    // the body loop guarantees both are carrying the same stale `total = 0`, and both pre-checks
    // will conclude "this fits". Only the conditional UPDATE can then stop the second.
    //
    // Firing them with a plain `Promise.all` does NOT reproduce this: the first request completes
    // and commits before the second even reads the user row, so the *pre-check* rejects the second
    // and the test passes with the atomic guard deleted. (It did. That is how this test was caught
    // being vacuous, and why it is written this way.)
    const head = SAMPLE_BYTES.subarray(0, 32);
    const tail = SAMPLE_BYTES.subarray(32);
    const a = uploadPausedMidStream(g.jwt, head, tail, { filename: 'race-a.jpg' });
    const b = uploadPausedMidStream(g.jwt, head, tail, { filename: 'race-b.jpg' });

    // Both are now past the pre-flight checks and sitting in the body loop, each holding total = 0.
    await Promise.all([a.checkPassed, b.checkPassed]);

    a.finish();
    b.finish();
    const [ra, rb] = await Promise.all([a.response, b.response]);
    const statuses = [ra.status, rb.status].sort();

    expect(
      statuses,
      `exactly one upload may win the race; got ${JSON.stringify(statuses)} — if BOTH are 201 the atomic guard is gone and a single guest can overrun the disk, after which NOBODY at the party can upload`
    ).toEqual([201, 413]);

    // The invariant that actually matters: accounting never exceeds the ceiling.
    const q = await quotaOf(g.jwt);
    expect(q.used_bytes).toBe(SIZE);
    expect(q.used_bytes).toBeLessThanOrEqual(limit);
  });

  test('GET /me/quota reports the live usage the UI shows the guest', async ({
    api,
    adminToken,
    guest,
    host,
  }) => {
    // Zero test hits before this — and it is the source of the "X von Y MB genutzt" widget.
    const g = await guest('QuotaWidget');
    await setLimitTo(api, adminToken, host.jwt, g.jwt, SIZE * 10);

    const before = await quotaOf(g.jwt);
    expect(before.enabled).toBe(true);
    expect(before.used_bytes).toBe(0);

    expect((await upload(g.jwt, 'one.jpg')).status).toBe(201);

    const after = await quotaOf(g.jwt);
    expect(after.used_bytes).toBe(SIZE);
    expect(after.limit_bytes).toBeGreaterThan(after.used_bytes);
  });
});
