import express, {
  type NextFunction,
  type Request,
  type Response,
} from 'express';
import request from 'supertest';
import { createJsonBodyParser, VOICE_EXTRACT_PATH } from './body-parsers';

/**
 * Guards a bug that made the voice endpoint completely unusable while surfacing as a
 * generic 500: the large-body limit stopped applying, so every real recording — anything
 * past roughly 20 seconds of audio — was rejected by Express's 100 kb default.
 */

type ProbeBody = { keys?: number; type?: string };

function buildApp(): express.Express {
  const app = express();
  app.use(createJsonBodyParser());
  app.post('*splat', (req: Request, res: Response) => {
    res.json({ keys: Object.keys((req.body ?? {}) as object).length });
  });
  // Surface body-parser's own error instead of Express's HTML default page.
  app.use(
    (
      err: { status?: number; type?: string },
      _req: Request,
      res: Response,
      next: NextFunction,
    ) => {
      if (res.headersSent) {
        next(err);
        return;
      }
      res.status(err.status ?? 500).json({ type: err.type });
    },
  );
  return app;
}

const bodyOfKb = (kb: number) => ({ audio: 'A'.repeat(kb * 1024) });

describe('createJsonBodyParser', () => {
  it('accepts a body far past the default limit on the voice route', async () => {
    const res = await request(buildApp())
      .post(VOICE_EXTRACT_PATH)
      .send(bodyOfKb(300));
    expect(res.status).toBe(200);
    expect((res.body as ProbeBody).keys).toBe(1);
  });

  it('accepts a realistic worst-case recording', async () => {
    // Two minutes of opus is well under 1 MB, but wav is far larger; 4 MB must pass.
    const res = await request(buildApp())
      .post(VOICE_EXTRACT_PATH)
      .send(bodyOfKb(4096));
    expect(res.status).toBe(200);
  });

  it('keeps the default limit on every other route', async () => {
    // The larger limit must not leak app-wide as a side effect.
    const res = await request(buildApp())
      .post('/api/auth/login')
      .send(bodyOfKb(300));
    expect(res.status).toBe(413);
    expect((res.body as ProbeBody).type).toBe('entity.too.large');
  });

  it('still parses ordinary bodies on ordinary routes', async () => {
    const res = await request(buildApp())
      .post('/api/auth/login')
      .send({ email: 'a@b.c' });
    expect(res.status).toBe(200);
    expect((res.body as ProbeBody).keys).toBe(1);
  });

  it('widens the limit for the spellings Express itself accepts', async () => {
    // Express routes case-insensitively and ignores a trailing slash by default, so these
    // all reach the voice controller. Any of them taking the 100 kb parser would 413 a
    // real recording and read as a broken microphone.
    for (const path of [
      '/api/voice/extract/',
      '/API/Voice/Extract',
      '/api/Voice/extract/',
    ]) {
      const res = await request(buildApp()).post(path).send(bodyOfKb(300));
      expect(res.status).toBe(200);
    }
  });

  it('does not widen the limit for a path that merely looks similar', async () => {
    for (const path of [
      '/api/voice/extract/extra',
      '/api/voice',
      '/voice/extract',
      // Express ignores one trailing slash, not two — this one never routes, so it must
      // not get the large parser either.
      '/api/voice/extract//',
    ]) {
      const res = await request(buildApp()).post(path).send(bodyOfKb(300));
      expect(res.status).toBe(413);
    }
  });
});
