import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Repository } from '../db/repository.js';
import { spaceFilesDir } from '../spaces/paths.js';
import { mountAppShareApi } from './app-share-api.js';

function makeApp(repo: Repository, worktreeDir: string) {
  const app = express();
  // 公開 API は認証ミドルウェアの外側。テストでも認証なしで mount する。
  mountAppShareApi(app, repo, worktreeDir);
  return app;
}

describe('app-share-api（公開・read-only）', () => {
  let repo: Repository;
  let dir: string;
  let worktreeDir: string;
  let spaceId: string;
  let token: string;
  const appName = 'dashboard';

  beforeEach(async () => {
    dir = mkdtempSync(join(tmpdir(), 'app-share-api-'));
    worktreeDir = join(dir, 'wt');
    repo = new Repository(join(dir, `${randomUUID()}.db`));
    const space = await repo.createSpace({ kind: 'case', title: '案件A', ownerId: 'user-1' });
    spaceId = space.id;

    const filesRoot = spaceFilesDir(worktreeDir, spaceId);
    // apps/{appName}/index.html
    mkdirSync(join(filesRoot, 'apps', appName), { recursive: true });
    writeFileSync(join(filesRoot, 'apps', appName, 'index.html'), '<html><body>app</body></html>');
    writeFileSync(join(filesRoot, 'apps', appName, 'data.json'), '{"k":1}');
    // output/ 成果物
    mkdirSync(join(filesRoot, 'output'), { recursive: true });
    writeFileSync(join(filesRoot, 'output', 'report.txt'), 'hello report');
    // logs/（封じ込め外であるべき領域）
    mkdirSync(join(filesRoot, 'logs'), { recursive: true });
    writeFileSync(join(filesRoot, 'logs', 'secret.txt'), 'should not leak');
    // 別アプリ（共有対象外）
    mkdirSync(join(filesRoot, 'apps', 'other-app'), { recursive: true });
    writeFileSync(join(filesRoot, 'apps', 'other-app', 'index.html'), 'other');

    token = repo.createAppShareLink(spaceId, appName, 'user-1').token;
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  it('(a) 有効トークンで GET /api/app-share/:token が 200 で appName を返す', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}`);
    expect(res.status).toBe(200);
    expect(res.body.app.appName).toBe(appName);
    expect(res.body.app.entryPath).toBe(`apps/${appName}/index.html`);
  });

  it('(b) output/ 配下の read は 200', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: 'output/report.txt' });
    expect(res.status).toBe(200);
    expect(res.text).toBe('hello report');
  });

  it('(c) apps/{app}/ 配下の read は 200', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: `apps/${appName}/data.json` });
    expect(res.status).toBe(200);
    expect(res.text).toBe('{"k":1}');
  });

  it('(d) パストラバーサル（../../etc/passwd）は 403', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: '../../../../etc/passwd' });
    expect(res.status).toBe(403);
  });

  it('(e) logs/ など許可ルート外の read は 403', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: 'logs/secret.txt' });
    expect(res.status).toBe(403);
  });

  it('(e2) 他アプリ apps/other-app/ の read は 403（共有対象アプリ外）', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: 'apps/other-app/index.html' });
    expect(res.status).toBe(403);
  });

  it('(f) revoke 後は 404', async () => {
    repo.revokeAppShareLink(spaceId, appName);
    const app = makeApp(repo, worktreeDir);
    const meta = await request(app).get(`/api/app-share/${token}`);
    expect(meta.status).toBe(404);
    const content = await request(app).get(`/api/app-share/${token}/files/content`).query({ path: 'output/report.txt' });
    expect(content.status).toBe(404);
  });

  it('(g) 不正トークンは 404', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get('/api/app-share/nonexistent-token');
    expect(res.status).toBe(404);
  });

  it('(h) raw レスポンスに Referrer-Policy: no-referrer ヘッダが付く', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/raw`).query({ path: `apps/${appName}/data.json` });
    expect(res.status).toBe(200);
    expect(res.headers['referrer-policy']).toBe('no-referrer');
  });

  it('(i) list は許可ルート内のみ。apps/{app} の一覧が取れる', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/list`).query({ dir: `apps/${appName}` });
    expect(res.status).toBe(200);
    const names = (res.body.entries as Array<{ name: string }>).map((e) => e.name).sort();
    expect(names).toEqual(['data.json', 'index.html']);
  });

  it('(j) list で許可ルート外（logs/）は 403', async () => {
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/list`).query({ dir: 'logs' });
    expect(res.status).toBe(403);
  });

  it('(k) symlink 経由の脱出は拒否（403）', async () => {
    // output/ 内から files ルート外の絶対パスへ symlink を張り、それを read しようとする
    const filesRoot = spaceFilesDir(worktreeDir, spaceId);
    const secretOutside = join(dir, 'outside-secret.txt');
    writeFileSync(secretOutside, 'top secret');
    try {
      symlinkSync(secretOutside, join(filesRoot, 'output', 'link.txt'));
    } catch {
      return; // symlink 不可な環境ではスキップ
    }
    const app = makeApp(repo, worktreeDir);
    const res = await request(app).get(`/api/app-share/${token}/files/raw`).query({ path: 'output/link.txt' });
    expect(res.status).toBe(403);
  });

  it('(l) write/delete エンドポイントは存在しない（POST は 404）', async () => {
    const app = makeApp(repo, worktreeDir);
    const post = await request(app).post(`/api/app-share/${token}/files/content`).send({ path: 'output/x.txt', content: 'x' });
    expect(post.status).toBe(404);
    const del = await request(app).delete(`/api/app-share/${token}/files/content`).query({ path: 'output/report.txt' });
    expect(del.status).toBe(404);
  });
});
