/**
 * APIS-019 — Shared-subtask wildcard traversal hardening.
 *
 * Public token route: GET /api/shared/:token/subtasks/:jobId/files/*
 * Containment relies on a separator-bounded prefix check:
 *
 *   const base = resolve(job.worktreePath);
 *   const resolved = resolve(base, req.params[0]);
 *   if (resolved !== base && !resolved.startsWith(base + sep)) -> 403
 *
 * share-api.test.ts already asserts one `..` escape, cross-task containment,
 * and a sibling prefix-collision at the WORKTREE level. This file targets the
 * WILDCARD path segment (`req.params[0]`) directly with a battery of escape
 * payloads — encoded `..`, deep `..`, absolute-path injection, and a sibling
 * prefix-collision reached THROUGH the wildcard — asserting each is rejected
 * (403/404) and the out-of-bounds secret is never served.
 *
 * Public route: no auth middleware mounted (token is the only credential).
 */
import { afterEach, describe, expect, it } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import express from 'express';
import request from 'supertest';
import { Repository, localTaskRepoName } from '../db/repository.js';
import { mountShareApi } from './share-api.js';

const SECRET = 'TOP-SECRET-DO-NOT-LEAK';

/**
 * Seed a shared task whose subtask worktree sits under the task workspace,
 * plus a sibling directory `<taskWs>/subtasks/1-evil` that shares a string
 * prefix with the legit subtask worktree `<taskWs>/subtasks/1`.
 */
async function seed(repo: Repository, tempDir: string) {
  const task = await repo.createLocalTask({ title: 'parent', body: 'b' });
  const taskWs = join(tempDir, 'ws');
  mkdirSync(taskWs, { recursive: true });
  await repo.updateLocalTask(task.id, { workspacePath: taskWs });

  const repoName = localTaskRepoName(task.id);
  const parent = await repo.createJob({ repo: repoName, issueNumber: task.id, instruction: 'parent' });
  const sub = await repo.createJob({ repo: repoName, issueNumber: 1, instruction: 'sub', parentJobId: parent.id });

  const subWs = join(taskWs, 'subtasks', '1');
  mkdirSync(join(subWs, 'output'), { recursive: true });
  writeFileSync(join(subWs, 'output', 'sub-result.md'), '# legit sub');
  await repo.updateJob(sub.id, { worktreePath: subWs });

  // Secret OUTSIDE the subtask worktree but inside the task workspace.
  writeFileSync(join(taskWs, 'secret.txt'), SECRET);
  // Sibling whose path is a string-prefix of the subtask worktree (1 vs 1-evil).
  const siblingDir = join(taskWs, 'subtasks', '1-evil');
  mkdirSync(siblingDir, { recursive: true });
  writeFileSync(join(siblingDir, 'leak.txt'), SECRET);

  // Secret well outside the whole task tree.
  writeFileSync(join(tempDir, 'host-secret.txt'), SECRET);

  const token = (await repo.shareLocalTask(task.id)) as string;
  return { token, subJobId: sub.id, taskWs };
}

function setup() {
  const tempDir = mkdtempSync(join(tmpdir(), 'maestro-share-wildcard-'));
  const repo = new Repository(join(tempDir, 'test.db'));
  const app = express();
  app.use(express.json());
  // Public share route — intentionally NO auth middleware.
  mountShareApi(app, repo);
  return { app, repo, tempDir };
}

describe('APIS-019 shared-subtask wildcard traversal', () => {
  let tempDir = '';
  let repo: Repository | null = null;

  afterEach(() => {
    if (repo) { repo.close(); repo = null; }
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });

  it('serves a legitimate in-bounds file (control)', async () => {
    const ctx = setup();
    tempDir = ctx.tempDir; repo = ctx.repo;
    const { token, subJobId } = await seed(ctx.repo, tempDir);
    const res = await request(ctx.app).get(`/api/shared/${token}/subtasks/${subJobId}/files/output/sub-result.md`);
    expect(res.status).toBe(200);
    expect(res.text).toContain('# legit sub');
  });

  // Each payload must be rejected (403 containment, or 404 not-found) and must
  // NOT return the secret in the body.
  const payloads: Array<{ label: string; seg: string }> = [
    { label: 'encoded single-level ..', seg: '..%2Fsecret.txt' },
    { label: 'encoded deep ..', seg: '..%2F..%2Fhost-secret.txt' },
    { label: 'mixed encoded + literal ..', seg: 'output%2F..%2F..%2Fsecret.txt' },
    { label: 'sibling prefix-collision via wildcard', seg: '..%2F1-evil%2Fleak.txt' },
    { label: 'double-encoded ..', seg: '..%252Fsecret.txt' },
    { label: 'trailing traversal back into parent', seg: 'output%2F..%2F..%2F..%2Fhost-secret.txt' },
  ];

  for (const { label, seg } of payloads) {
    it(`rejects traversal payload: ${label}`, async () => {
      const ctx = setup();
      tempDir = ctx.tempDir; repo = ctx.repo;
      const { token, subJobId } = await seed(ctx.repo, tempDir);
      const res = await request(ctx.app).get(`/api/shared/${token}/subtasks/${subJobId}/files/${seg}`);
      // Acceptable rejections: 403 (containment) or 404 (resolved-but-not-found).
      expect([403, 404], `status for ${label}`).toContain(res.status);
      // The secret must never be served.
      expect(res.text ?? '', `body for ${label}`).not.toContain(SECRET);
    });
  }

  it('an absolute-path wildcard segment cannot escape the worktree', async () => {
    const ctx = setup();
    tempDir = ctx.tempDir; repo = ctx.repo;
    const { token, subJobId } = await seed(ctx.repo, tempDir);
    // resolve(base, '/etc/passwd') => '/etc/passwd' (absolute wins), which is
    // outside base → must be rejected, not served.
    const res = await request(ctx.app).get(`/api/shared/${token}/subtasks/${subJobId}/files/%2Fetc%2Fpasswd`);
    expect([403, 404]).toContain(res.status);
    expect(res.text ?? '').not.toContain('root:');
  });

  it('unknown token on the wildcard route → 404 (not a 500/leak)', async () => {
    const ctx = setup();
    tempDir = ctx.tempDir; repo = ctx.repo;
    await seed(ctx.repo, tempDir);
    const res = await request(ctx.app).get('/api/shared/bogus-token/subtasks/999/files/output/sub-result.md');
    expect(res.status).toBe(404);
  });
});
