/**
 * Tests for the Learn boundary.
 *
 * The one that matters is `learn token is not a credential for anything else`.
 * Every other assertion here is supporting evidence for it: the design's whole
 * claim is that a code-holder cannot become a principal, and the way that
 * claim fails in practice is not a dramatic bug — it is somebody later
 * deciding it would be simpler to mint a `Principal` with an empty team list
 * and rely on capability checks downstream. That refactor passes every test
 * about learn resources and fails this one.
 *
 * The rest pin decisions that would otherwise fail silently: an embed resolver
 * that accepts a hostile host, a PATCH that promotes a supply video to
 * anon-visible because it validated the input instead of the merged row, and
 * a rate limiter whose window never closes.
 */
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import {
  LEARN_CODE_TRACK,
  LEARN_FRAME_SRC_HOSTS,
  formatLearnDuration,
  learnEmbed,
  learnEmbedUrl,
  learnVisibilityPermitted,
  resolveLearnEmbed,
} from '@pig/core';
import type { Database } from '@pig/db';
import { learnResources, platformSettings } from '@pig/db';
import { createMediaRoutes, LEARN_MEDIA_DIR_ENV, parseByteRange } from '../src/lib/media';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createApp } from '../src/app';
import { loadConfig } from '../src/lib/config';
import {
  LEARN_TOKEN_TTL_MS,
  createAttemptLimiter,
  learnResourceCreateSchema,
  mintLearnToken,
  rateLimitKey,
  verifyLearnToken,
} from '../src/routes/learn';

const ACCESS_CODE = 'carlthefog';

// ---------------------------------------------------------------- the embed

describe('embed allowlist', () => {
  it('resolves a Cap share link to an embed rebuilt from the table', () => {
    const resolved = resolveLearnEmbed('https://video.karti.ai/s/0n6n9p83efnxbs2');
    assert.equal(resolved.ok, true);
    assert.equal(resolved.ok && resolved.provider, 'cap');
    assert.equal(resolved.ok && resolved.externalId, '0n6n9p83efnxbs2');
    assert.equal(resolved.ok && resolved.embedUrl, 'https://video.karti.ai/embed/0n6n9p83efnxbs2');
  });

  it('accepts an embed link too, because that is what people copy', () => {
    const resolved = resolveLearnEmbed('https://video.karti.ai/embed/0n6n9p83efnxbs2');
    assert.equal(resolved.ok && resolved.watchUrl, 'https://video.karti.ai/s/0n6n9p83efnxbs2');
  });

  it('refuses every shape that would put someone else’s bytes in an iframe src', () => {
    // Each of these is a real technique, not a hypothetical. The suffix case
    // is why `hosts` is an exact-match list rather than an `endsWith` check,
    // and the credential case is why a URL that READS as trusted to a human is
    // rejected on the parsed hostname instead.
    const hostile = [
      'javascript:alert(1)',
      'data:text/html,<script>alert(1)</script>',
      'http://video.karti.ai/s/0n6n9p83efnxbs2',
      'https://video.karti.ai@evil.example/s/0n6n9p83efnxbs2',
      'https://evil-video.karti.ai.attacker.test/s/0n6n9p83efnxbs2',
      'https://notvideo.karti.ai/s/0n6n9p83efnxbs2',
      'https://video.karti.ai:8443/s/0n6n9p83efnxbs2',
      'https://video.karti.ai/s/../../admin',
      'https://video.karti.ai/s/0n6n9p83efnxbs2/edit',
      'https://video.karti.ai/s/"><script>alert(1)</script>',
      'https://video.karti.ai/',
      'not a url at all',
    ];
    for (const candidate of hostile) {
      assert.equal(resolveLearnEmbed(candidate).ok, false, `should reject: ${candidate}`);
    }
  });

  it('refuses a recognised but not-yet-enabled provider rather than framing it', () => {
    // Loom is in the table so that enabling it is a flag and a CSP host. Until
    // the CSP host exists, a Loom row would be a card that silently never
    // plays — so the row cannot be created at all.
    const resolved = resolveLearnEmbed('https://www.loom.com/share/0123456789abcdef');
    assert.equal(resolved.ok, false);
    assert.equal(resolved.ok === false && resolved.reason, 'provider_disabled');
  });

  it('re-validates a stored id rather than trusting the database', () => {
    // A row written before the pattern tightened, or by a path that skipped
    // the resolver, must not be framed on the strength of having persisted.
    assert.equal(learnEmbedUrl('cap', '"><iframe src=x'), null);
    assert.equal(learnEmbedUrl('cap', '0n6n9p83efnxbs2'), 'https://video.karti.ai/embed/0n6n9p83efnxbs2');
  });

  it('names every enabled host, so the CSP handoff cannot drift', () => {
    // The self-hosted provider is enabled and contributes NOTHING here. A
    // native <video> on this origin is covered by `default-src 'self'`, and
    // widening frame-src for it would hand out framing rights nothing needs.
    assert.deepEqual(LEARN_FRAME_SRC_HOSTS, ['https://video.karti.ai']);
  });
});

// ------------------------------------------------------------- self-hosted

describe('self-hosted media', () => {
  it('resolves a /media/learn path to a native video, not an iframe', () => {
    const resolved = resolveLearnEmbed('/media/learn/pig-tour.7f3a91c2.mp4');
    assert.equal(resolved.ok, true);
    assert.equal(resolved.ok && resolved.provider, 'pig');
    assert.equal(resolved.ok && resolved.externalId, 'pig-tour.7f3a91c2.mp4');
    assert.deepEqual(resolved.ok && resolved.embed, {
      kind: 'video',
      src: '/media/learn/pig-tour.7f3a91c2.mp4',
      // The poster is derived from the VIDEO's content hash, so a re-render
      // moves both names together and a thumbnail cannot outlive its clip.
      poster: '/media/learn/pig-tour.7f3a91c2.jpg',
    });
    // The flat field stays in step for callers written before the union.
    assert.equal(resolved.ok && resolved.embedUrl, '/media/learn/pig-tour.7f3a91c2.mp4');
  });

  it('still resolves a remote provider to an iframe', () => {
    const resolved = resolveLearnEmbed('https://video.karti.ai/s/0n6n9p83efnxbs2');
    assert.deepEqual(resolved.ok && resolved.embed, {
      kind: 'iframe',
      src: 'https://video.karti.ai/embed/0n6n9p83efnxbs2',
    });
  });

  it('refuses every path that would read a file we did not mean to serve', () => {
    // Traversal in each encoding that has ever worked somewhere, a scheme, a
    // host, a protocol-relative URL that a naive `startsWith('/')` would treat
    // as a path, an extension we do not serve, and a bare dotfile.
    const hostile = [
      '/media/learn/../../etc/passwd',
      '/media/learn/..%2f..%2fetc%2fpasswd',
      '/media/learn/../secrets.mp4',
      '/media/learn/..',
      '/media/learn/sub/dir/video.mp4',
      '/media/learn/',
      '/etc/passwd',
      '/media/other/video.mp4',
      '//evil.example/media/learn/video.mp4',
      'file:///media/learn/video.mp4',
      'https://primeintellectgrowth.com/media/learn/pig-tour.7f3a91c2.mp4',
      '/media/learn/.env',
      '/media/learn/video.mp4.sh',
      '/media/learn/video mp4.mp4',
      '/media/learn/video.mp4?v=2',
      '/media/learn/video.mp4#t=10',
      '/media/learn/-leading-dash.mp4',
      `/media/learn/${'a'.repeat(130)}.mp4`,
    ];
    for (const candidate of hostile) {
      assert.equal(resolveLearnEmbed(candidate).ok, false, `should reject: ${candidate}`);
    }
  });

  it('re-validates a stored filename rather than trusting the database', () => {
    // Same argument as the Cap case: persistence is not validation. A row
    // written by a future path that skipped the resolver must not become a
    // file read.
    assert.equal(learnEmbed('pig', '../../etc/passwd'), null);
    assert.equal(learnEmbed('pig', 'pig-tour.mp4.sh'), null);
    assert.deepEqual(learnEmbed('pig', 'pig-tour.7f3a91c2.mp4'), {
      kind: 'video',
      src: '/media/learn/pig-tour.7f3a91c2.mp4',
      // The poster is derived from the VIDEO's content hash, so a re-render
      // moves both names together and a thumbnail cannot outlive its clip.
      poster: '/media/learn/pig-tour.7f3a91c2.jpg',
    });
  });

  it('accepts a self-hosted path at the create schema, on the platform track', () => {
    const parsed = learnResourceCreateSchema.safeParse({
      track: LEARN_CODE_TRACK,
      title: 'A tour of PIG in five minutes',
      url: '/media/learn/pig-tour.7f3a91c2.mp4',
      visibility: 'code',
    });
    assert.equal(parsed.success, true);
  });
});

// ------------------------------------------------------------- the two rules

describe('code visibility', () => {
  it('permits code visibility on the platform track only', () => {
    assert.equal(learnVisibilityPermitted('platform', 'code'), true);
    assert.equal(learnVisibilityPermitted('supply', 'code'), false);
    assert.equal(learnVisibilityPermitted('demand', 'code'), false);
    // Members-only is legal everywhere, including on the platform track.
    for (const track of ['supply', 'demand', 'platform'] as const) {
      assert.equal(learnVisibilityPermitted(track, 'members'), true);
    }
  });

  it('refuses a code-visible concept resource at the write schema', () => {
    const rejected = learnResourceCreateSchema.safeParse({
      track: 'supply',
      title: 'How capacity is priced',
      url: 'https://video.karti.ai/s/0n6n9p83efnxbs2',
      visibility: 'code',
    });
    assert.equal(rejected.success, false);

    const accepted = learnResourceCreateSchema.safeParse({
      track: LEARN_CODE_TRACK,
      title: 'Your first hour in PIG',
      url: 'https://video.karti.ai/s/0n6n9p83efnxbs2',
      visibility: 'code',
    });
    assert.equal(accepted.success, true);
  });
});

// ----------------------------------------------------------------- the token

describe('learn token', () => {
  it('verifies a token it minted, and refuses one minted under another code', () => {
    const token = mintLearnToken(ACCESS_CODE, Date.now() + LEARN_TOKEN_TTL_MS);
    assert.equal(verifyLearnToken(ACCESS_CODE, token).valid, true);

    // Rotation is total precisely because the signing key is derived from the
    // code — there is no revocation list to forget to write to.
    const afterRotation = verifyLearnToken('anothercode', token);
    assert.equal(afterRotation.valid, false);
    assert.equal(afterRotation.valid === false && afterRotation.reason, 'mismatch');
  });

  it('refuses an expired token, a forged signature and a rewritten expiry', () => {
    const expiry = Date.now() + LEARN_TOKEN_TTL_MS;
    const token = mintLearnToken(ACCESS_CODE, expiry);

    assert.equal(verifyLearnToken(ACCESS_CODE, token, expiry + 1).valid, false);
    assert.equal(verifyLearnToken(ACCESS_CODE, `${token}x`).valid, false);
    assert.equal(verifyLearnToken(ACCESS_CODE, 'learn_v1.99999999999999.aaaa').valid, false);
    // The expiry is signed, so extending it invalidates the token rather than
    // extending the session.
    const [, , signature] = token.slice('learn_'.length).split('.');
    assert.equal(verifyLearnToken(ACCESS_CODE, `learn_v1.${expiry + 60_000}.${signature}`).valid, false);
    assert.equal(verifyLearnToken(ACCESS_CODE, undefined).valid, false);
    assert.equal(verifyLearnToken(null, token).valid, false);
  });
});

// ----------------------------------------------------------- the whole point

/**
 * A learn token must be worthless everywhere except one handler.
 *
 * This runs against the real `createApp`, not a stub, because the property
 * being asserted is about composition: what the authenticator does with a
 * bearer token it does not recognise, on routes this feature never mentions.
 * A fake would assert my own assumptions back at me.
 *
 * No database is touched — every path here fails in the auth middleware,
 * before a handler runs — so the stub below is a placeholder that would throw
 * loudly if anything ever reached it. That is deliberate: if a future change
 * lets a learn token past the middleware, this test fails with a database
 * error rather than passing quietly.
 */
describe('a learn token is not a credential for anything else', () => {
  const config = loadConfig({
    NODE_ENV: 'production',
    DATABASE_URL: 'postgres://unused:unused@127.0.0.1:1/unused',
    PIG_PUBLIC_URL: 'https://pig-learn-test.invalid',
    SUPABASE_URL: 'https://identity-learn-test.invalid',
    SUPABASE_ANON_KEY: 'learn-test-anon-key',
    SUPABASE_SERVICE_KEY: '',
    PIG_ADMIN_EMAILS: '',
    PIGGY_ENABLED: 'false',
  });

  const db = new Proxy(
    {},
    {
      get() {
        throw new Error('A learn token reached the database. It must never resolve a principal.');
      },
    },
  ) as unknown as Database;

  const authProvider = {
    name: 'learn-test-stub',
    async verifyAccessToken(): Promise<{ subject: string; email: string }> {
      // A learn token is not a JWT. If this is ever called with one, the
      // authenticator has started treating it as an identity assertion.
      throw new Error('Not a valid identity token.');
    },
  };

  const app = createApp(config, db, authProvider);
  const token = mintLearnToken(ACCESS_CODE, Date.now() + LEARN_TOKEN_TTL_MS);

  // The routes a leak would be worth having. `/api/dashboard` is the one
  // scripts/deploy.sh probes before it will finish a release.
  for (const path of ['/api/dashboard', '/api/accounts', '/api/contracts']) {
    it(`answers 401 on ${path} for a valid learn token`, async () => {
      const response = await app.request(`https://pig-learn-test.invalid${path}`, {
        headers: { authorization: `Bearer ${token}` },
      });
      assert.equal(response.status, 401, `${path} must refuse a learn token`);
    });
  }

  it('answers 401 on those routes with no credential at all, unchanged', async () => {
    // The deploy gate asserts exactly this. Adding a public path must not move
    // it, so it is pinned next to the token case rather than trusted.
    const response = await app.request('https://pig-learn-test.invalid/api/dashboard');
    assert.equal(response.status, 401);
  });

  it('is not a learn token once it is dressed as a PIG API key', () => {
    // `pig_` is the one prefix that reaches a database lookup, so the two
    // token vocabularies must not overlap in either direction. Asserted on the
    // verifier rather than through the app because the API-key branch needs a
    // real database to answer 401 `invalid_key`, and the e2e suite covers that
    // path with one.
    const dressed = `pig_${token}`;
    assert.equal(verifyLearnToken(ACCESS_CODE, dressed).valid, false);
    assert.equal(dressed.startsWith('learn_'), false);
  });
});

// ----------------------------------------------------------- the rate limiter

describe('attempt limiter', () => {
  it('allows the quota, refuses past it, and reopens after the window', () => {
    const limiter = createAttemptLimiter({ limit: 3, windowMs: 60_000 });
    const start = 1_000_000;
    for (let attempt = 0; attempt < 3; attempt += 1) {
      assert.equal(limiter.check('10.0.0.9', start).allowed, true);
    }
    const refused = limiter.check('10.0.0.9', start);
    assert.equal(refused.allowed, false);
    assert.ok(refused.retryAfterSeconds > 0);

    // A window that never reopens is a self-inflicted outage, not security.
    assert.equal(limiter.check('10.0.0.9', start + 60_001).allowed, true);
    // Buckets are per key.
    assert.equal(limiter.check('10.0.0.10', start).allowed, true);
  });

  it('buckets on the last forwarded hop, not the first', () => {
    // Caddy APPENDS the peer address, so the first entry is whatever the
    // client sent. Keying on it hands anyone unlimited buckets and the limiter
    // becomes decorative.
    assert.equal(rateLimitKey('203.0.113.7, 10.0.0.2'), '10.0.0.2');
    assert.equal(rateLimitKey('10.0.0.2'), '10.0.0.2');
    assert.equal(rateLimitKey(undefined), 'unknown');
  });
});

// ------------------------------------------------- the public round trip

/**
 * A self-hosted row must survive the whole read path as a `video`.
 *
 * Asserted through `app.request` on the real public route rather than on the
 * serialiser, because the failure this guards against is composition: the
 * route enumerates its columns, drops rows it cannot rebuild an embed for, and
 * is the one read a code-holder performs. A row that resolves fine in
 * isolation and is silently dropped by `renderable()` would leave the Learn
 * page empty with nothing in the log.
 */
describe('a self-hosted row through /api/learn/public', () => {
  const config = loadConfig({
    NODE_ENV: 'production',
    DATABASE_URL: 'postgres://unused:unused@127.0.0.1:1/unused',
    PIG_PUBLIC_URL: 'https://pig-learn-test.invalid',
    SUPABASE_URL: 'https://identity-learn-test.invalid',
    SUPABASE_ANON_KEY: 'learn-test-anon-key',
    SUPABASE_SERVICE_KEY: '',
    PIG_ADMIN_EMAILS: '',
    PIGGY_ENABLED: 'false',
  });

  const rows = [
    {
      id: '00000000-0000-4000-8000-000000000001',
      track: 'platform' as const,
      title: 'A tour of PIG in five minutes',
      summary: 'What the product is for.',
      provider: 'pig' as const,
      externalId: 'pig-tour.7f3a91c2.mp4',
      visibility: 'code' as const,
      durationSeconds: 300,
      sortOrder: 1,
      publishedAt: new Date('2026-08-01T00:00:00.000Z'),
    },
    {
      id: '00000000-0000-4000-8000-000000000002',
      track: 'platform' as const,
      title: 'The Cap-hosted one, still an iframe',
      summary: null,
      provider: 'cap' as const,
      externalId: '0n6n9p83efnxbs2',
      visibility: 'code' as const,
      durationSeconds: 520,
      sortOrder: 2,
      publishedAt: new Date('2026-08-02T00:00:00.000Z'),
    },
  ];

  /** Answers the settings lookup and the resource read, and nothing else. */
  const db = {
    select: () => ({
      from: (table: unknown) => ({
        where: () => ({
          limit: async () =>
            table === platformSettings ? [{ code: ACCESS_CODE }] : [],
          orderBy: async () => (table === learnResources ? rows : []),
        }),
      }),
    }),
  } as unknown as Database;

  const authProvider = {
    name: 'learn-test-stub',
    async verifyAccessToken(): Promise<{ subject: string; email: string }> {
      throw new Error('Not a valid identity token.');
    },
  };

  it('returns kind:"video" for the self-hosted row and kind:"iframe" for the remote one', async () => {
    const app = createApp(config, db, authProvider);
    const token = mintLearnToken(ACCESS_CODE, Date.now() + LEARN_TOKEN_TTL_MS);
    const response = await app.request('https://pig-learn-test.invalid/api/learn/public', {
      headers: { authorization: `Bearer ${token}` },
    });
    assert.equal(response.status, 200);

    const body = (await response.json()) as {
      resources: { title: string; embed: { kind: string; src: string }; embedUrl: string }[];
    };
    assert.equal(body.resources.length, 2, 'neither row may be dropped');

    const [hosted, remote] = body.resources;
    assert.deepEqual(hosted?.embed, {
      kind: 'video',
      src: '/media/learn/pig-tour.7f3a91c2.mp4',
      // The poster is derived from the VIDEO's content hash, so a re-render
      // moves both names together and a thumbnail cannot outlive its clip.
      poster: '/media/learn/pig-tour.7f3a91c2.jpg',
    });
    // Same-origin and relative, so the page needs no CSP host for it at all.
    assert.equal(hosted?.embedUrl, '/media/learn/pig-tour.7f3a91c2.mp4');
    assert.equal(remote?.embed.kind, 'iframe');
    assert.equal(remote?.embed.src, 'https://video.karti.ai/embed/0n6n9p83efnxbs2');
  });

  it('serves that src from the ASSEMBLED app, not just from the route factory', async () => {
    /*
     * The assertion that would have caught the media route shipping unmounted.
     *
     * `describe('the media route')` below exercises createMediaRoutes() as a
     * standalone Hono app, which verifies the handler and proves nothing about
     * whether createApp wires it in. It did not: every layer landed — the
     * migration, the seed, both feeds, the bind mount, the docs — except the
     * one that serves the bytes, and the whole suite stayed green. Requests to
     * /media/learn/* fell through to server.ts's SPA fallback and returned
     * HTTP 200 text/html, so the player showed a black box with working
     * controls and no error.
     *
     * Assert against the composed application, and specifically on the
     * content-type: the failure mode is a 200, not a 404.
     */
    // The route reads its root from the environment at request time, so point
    // it at a directory that really holds the file the feed advertises.
    const root = mkdtempSync(join(tmpdir(), 'pig-media-mounted-'));
    writeFileSync(join(root, 'pig-tour.7f3a91c2.mp4'), Buffer.alloc(2048, 7));
    const previous = process.env[LEARN_MEDIA_DIR_ENV];
    process.env[LEARN_MEDIA_DIR_ENV] = root;

    let response: Response;
    try {
      const app = createApp(config, db, authProvider);
      response = await app.request(
        'https://pig-learn-test.invalid/media/learn/pig-tour.7f3a91c2.mp4',
      );
    } finally {
      if (previous === undefined) delete process.env[LEARN_MEDIA_DIR_ENV];
      else process.env[LEARN_MEDIA_DIR_ENV] = previous;
    }

    assert.notEqual(response.status, 404, 'the media route is not mounted in createApp');
    assert.equal(
      response.headers.get('content-type'),
      'video/mp4',
      'the SPA fallback answered instead of the media route',
    );
  });
});

// --------------------------------------------------------- serving the file

describe('the media route', () => {
  const root = mkdtempSync(join(tmpdir(), 'pig-media-'));
  // 1 KiB of distinguishable bytes: an assertion on a range is only meaningful
  // if the wrong offset produces different content.
  const body = Buffer.from(Array.from({ length: 1024 }, (_, i) => i % 251));
  writeFileSync(join(root, 'pig-tour.7f3a91c2.mp4'), body);
  const app = createMediaRoutes({ root });
  const url = (path: string) => `https://pig.invalid${path}`;

  it('serves the whole file with a seekable header set', async () => {
    const response = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'));
    assert.equal(response.status, 200);
    assert.equal(response.headers.get('content-type'), 'video/mp4');
    // Without this a browser will not attempt a range request at all, and the
    // scrubber becomes decorative.
    assert.equal(response.headers.get('accept-ranges'), 'bytes');
    assert.equal(response.headers.get('content-length'), '1024');
    assert.deepEqual(Buffer.from(await response.arrayBuffer()), body);
  });

  it('answers a range with 206 and exactly those bytes', async () => {
    const response = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'), {
      headers: { range: 'bytes=100-199' },
    });
    assert.equal(response.status, 206);
    assert.equal(response.headers.get('content-range'), 'bytes 100-199/1024');
    assert.equal(response.headers.get('content-length'), '100');
    assert.deepEqual(Buffer.from(await response.arrayBuffer()), body.subarray(100, 200));
  });

  it('answers an open-ended and a suffix range', async () => {
    const open = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'), {
      headers: { range: 'bytes=1000-' },
    });
    assert.equal(open.headers.get('content-range'), 'bytes 1000-1023/1024');

    // How a player finds an MP4 moov atom at the end of the file. Getting this
    // branch wrong is why some videos never start rather than never seek.
    const suffix = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'), {
      headers: { range: 'bytes=-24' },
    });
    assert.equal(suffix.headers.get('content-range'), 'bytes 1000-1023/1024');
    assert.deepEqual(Buffer.from(await suffix.arrayBuffer()), body.subarray(1000));
  });

  it('refuses a range past the end rather than restarting the file', () => {
    // 200-with-the-whole-file here splices byte 0 into the middle of the
    // player's buffer, which corrupts playback instead of failing it.
    assert.equal(parseByteRange('bytes=2000-', 1024), 'unsatisfiable');
    assert.equal(parseByteRange('bytes=500-400', 1024), 'unsatisfiable');
    // A multi-range request is ignored, which the spec permits.
    assert.equal(parseByteRange('bytes=0-10,20-30', 1024), null);
    assert.equal(parseByteRange(undefined, 1024), null);
  });

  it('answers 416 with the real size so a client can recover', async () => {
    const response = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'), {
      headers: { range: 'bytes=5000-' },
    });
    assert.equal(response.status, 416);
    assert.equal(response.headers.get('content-range'), 'bytes */1024');
  });

  it('answers HEAD without a body, so a player can probe cheaply', async () => {
    const response = await app.request(url('/media/learn/pig-tour.7f3a91c2.mp4'), {
      method: 'HEAD',
    });
    assert.equal(response.status, 200);
    assert.equal(response.headers.get('content-length'), '1024');
    assert.equal((await response.text()).length, 0);
  });

  it('reads nothing outside the media directory', async () => {
    // The route sees these as filenames; each must 404 rather than resolve.
    for (const path of [
      '/media/learn/..%2f..%2fpackage.json',
      '/media/learn/%2e%2e%2fpackage.json',
      '/media/learn/pig-tour.7f3a91c2.mp4.sh',
      '/media/learn/absent.7f3a91c2.mp4',
    ]) {
      const response = await app.request(url(path));
      assert.equal(response.status, 404, `should not serve: ${path}`);
    }
  });
});

describe('duration formatting', () => {
  it('crosses the hour without renaming the minutes', () => {
    assert.equal(formatLearnDuration(272), '4:32');
    assert.equal(formatLearnDuration(3_852), '1:04:12');
    assert.equal(formatLearnDuration(60), '1:00');
    assert.equal(formatLearnDuration(null), null);
    assert.equal(formatLearnDuration(-1), null);
  });
});
