// 公開アプリ共有リンクの発行/失効/取得エンドポイント（認証付き）のテスト。
//
// 発行/失効/取得は canManageSpace（owner / admin / member.role==='owner'）。
// editor/viewer メンバーや非メンバーは 403。発行は未失効リンクの再利用。
// spec: docs/superpowers/specs/2026-06-23-public-app-share-link-design.md

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { Repository } from '../db/repository.js';
import { createSpaceApi, type SpaceApiDeps } from './space-api.js';

function user(id: string, role: 'user' | 'admin' = 'user', orgIds: string[] = []): Express.User {
  return {
    id,
    email: `${id}@x.com`,
    name: id,
    avatarUrl: null,
    role,
    status: 'active',
    orgIds,
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
  } as unknown as Express.User;
}

describe('space app-share link API', () => {
  let dir = '';
  let repo: Repository;
  let ownerId = '';
  let editorId = '';
  let strangerId = '';
  let spaceId = '';
  let worktreeDir = '';
  const appName = 'dashboard';

  function appAs(u: Express.User | null, authActive = true): express.Application {
    const app = express();
    if (u) {
      app.use((req, _res, next) => {
        (req as unknown as { user: Express.User }).user = u;
        next();
      });
    }
    const deps: SpaceApiDeps = {
      repo,
      dataRoot: join(dir, 'data', 'users'),
      worktreeDir,
      authActive,
    };
    app.use('/api/local/spaces', createSpaceApi(deps));
    return app;
  }

  beforeEach(async () => {
    dir = mkdtempSync(join(tmpdir(), 'space-app-share-'));
    worktreeDir = join(dir, 'workspaces');
    repo = new Repository(join(dir, 'db.sqlite'));
    ownerId = repo.createUser({ email: 'owner@x.com', name: 'owner', role: 'user', status: 'active' }).id;
    editorId = repo.createUser({ email: 'editor@x.com', name: 'editor', role: 'user', status: 'active' }).id;
    strangerId = repo.createUser({ email: 'stranger@x.com', name: 'stranger', role: 'user', status: 'active' }).id;
    const space = await repo.createSpace({ kind: 'case', title: '案件A', ownerId, visibility: 'private' });
    spaceId = space.id;
    await repo.addSpaceMember({ spaceId, userId: editorId, role: 'editor', invitedBy: ownerId });
  });

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

  it('owner がリンクを発行できる（token + shareUrl）', async () => {
    const res = await request(appAs(user(ownerId)))
      .post(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(res.status).toBe(201);
    expect(res.body.token).toBeTruthy();
    expect(res.body.shareUrl).toBe(`/ui/app/${res.body.token}`);
  });

  it('GET は発行前は { token: null }、発行後は現行リンクを返す', async () => {
    const app = appAs(user(ownerId));
    const before = await request(app).get(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(before.status).toBe(200);
    expect(before.body.token).toBeNull();
    const created = (await request(app).post(`/api/local/spaces/${spaceId}/apps/${appName}/share`)).body;
    const after = await request(app).get(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(after.status).toBe(200);
    expect(after.body.token).toBe(created.token);
    expect(after.body.shareUrl).toBe(`/ui/app/${created.token}`);
    expect(after.body.revokedAt).toBeNull();
  });

  it('未失効の再発行は同一トークンを返す', async () => {
    const app = appAs(user(ownerId));
    const first = (await request(app).post(`/api/local/spaces/${spaceId}/apps/${appName}/share`)).body.token;
    const second = (await request(app).post(`/api/local/spaces/${spaceId}/apps/${appName}/share`)).body.token;
    expect(second).toBe(first);
  });

  it('DELETE で失効でき、その後 GET は revokedAt を持つ', async () => {
    const app = appAs(user(ownerId));
    await request(app).post(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    const del = await request(app).delete(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(del.status === 200 || del.status === 204).toBe(true);
    const after = await request(app).get(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(after.body.revokedAt).not.toBeNull();
  });

  it('editor メンバーは発行できない（403）', async () => {
    const res = await request(appAs(user(editorId)))
      .post(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(res.status).toBe(403);
  });

  it('非メンバーはスペースが見えず 404', async () => {
    const res = await request(appAs(user(strangerId)))
      .post(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    // private スペースは非メンバーに getSpace で null → 404
    expect(res.status).toBe(404);
  });

  it('admin は発行できる', async () => {
    const res = await request(appAs(user('admin-1', 'admin')))
      .post(`/api/local/spaces/${spaceId}/apps/${appName}/share`);
    expect(res.status).toBe(201);
    expect(res.body.token).toBeTruthy();
  });
});
