// Shared-workspace WRITE gate for the comment + cancel routes.
//
// Bug: commenting on a co-member's space task returned 404 because the route
// gated with checkTaskOwnership (owner/admin only). A space editor member must
// be able to comment; a space viewer-only member must get a CLEAR 403 (not a
// 404); a non-member must still get 404; owner/admin still work.

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, localTaskRepoName } from '../db/repository.js';
import { mountLocalTasksApi } from './local-tasks-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 Express.User;
}

describe('comment route write gate (shared workspace)', () => {
  let dir = '';
  let repo: Repository;
  let ownerId = '';

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'lt-comment-gate-'));
    repo = new Repository(join(dir, 'db.sqlite'));
    ownerId = repo.createUser({ email: 'o@x.com', name: 'o', role: 'user', status: 'active' }).id;
  });

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

  function appAs(u: Express.User): express.Application {
    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = u;
      next();
    });
    mountLocalTasksApi(app, { repo, worktreeDir: join(dir, 'workspaces') });
    return app;
  }

  async function caseSpaceTask() {
    const space = await repo.createSpace({ kind: 'case', title: '案件A', ownerId, visibility: 'private' });
    const task = await repo.createLocalTask({
      title: 't', body: 'b', pieceName: 'manual-writer', ownerId, spaceId: space.id, visibility: 'private',
    });
    return { space, task };
  }

  it('owner can comment (201)', async () => {
    const { task } = await caseSpaceTask();
    const res = await request(appAs(user(ownerId)))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'hello', author: 'o' });
    expect(res.status).toBe(201);
  });

  it('admin can comment (201)', async () => {
    const { task } = await caseSpaceTask();
    const admin = repo.createUser({ email: 'adm@x.com', name: 'adm', role: 'admin', status: 'active' });
    const res = await request(appAs(user(admin.id, 'admin')))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'hi', author: 'adm' });
    expect(res.status).toBe(201);
  });

  it('editor member can comment (201)', async () => {
    const { space, task } = await caseSpaceTask();
    const editor = repo.createUser({ email: 'ed@x.com', name: 'ed', role: 'user', status: 'active' });
    await repo.addSpaceMember({ spaceId: space.id, userId: editor.id, role: 'editor' });
    const res = await request(appAs(user(editor.id)))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'from editor', author: 'ed' });
    expect(res.status).toBe(201);
  });

  it('viewer-only member is denied with a clear 403 (not 404)', async () => {
    const { space, task } = await caseSpaceTask();
    const v = repo.createUser({ email: 'v@x.com', name: 'v', role: 'user', status: 'active' });
    await repo.addSpaceMember({ spaceId: space.id, userId: v.id, role: 'viewer' });
    const res = await request(appAs(user(v.id)))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'should fail', author: 'v' });
    expect(res.status).toBe(403);
    expect(res.body.error).toBe('閲覧のみのため投稿できません');
  });

  it('non-member gets 404', async () => {
    const { task } = await caseSpaceTask();
    const stranger = repo.createUser({ email: 's@x.com', name: 's', role: 'user', status: 'active' });
    const res = await request(appAs(user(stranger.id)))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'nope', author: 's' });
    expect(res.status).toBe(404);
  });

  it('non-space personal task: non-owner still gets 404 (back-compat)', async () => {
    const task = await repo.createLocalTask({ title: 't', body: 'b', ownerId, visibility: 'private' });
    const stranger = repo.createUser({ email: 's2@x.com', name: 's2', role: 'user', status: 'active' });
    const res = await request(appAs(user(stranger.id)))
      .post(`/api/local/tasks/${task.id}/comments`)
      .send({ body: 'nope', author: 's2' });
    expect(res.status).toBe(404);
  });
});

describe('cancel route write gate (shared workspace)', () => {
  let dir = '';
  let repo: Repository;
  let ownerId = '';

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'lt-cancel-gate-'));
    repo = new Repository(join(dir, 'db.sqlite'));
    ownerId = repo.createUser({ email: 'o@x.com', name: 'o', role: 'user', status: 'active' }).id;
  });

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

  function appAs(u: Express.User): express.Application {
    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = u;
      next();
    });
    mountLocalTasksApi(app, { repo, worktreeDir: join(dir, 'workspaces') });
    return app;
  }

  async function runningSpaceTask() {
    const space = await repo.createSpace({ kind: 'case', title: '案件A', ownerId, visibility: 'private' });
    const task = await repo.createLocalTask({
      title: 't', body: 'b', pieceName: 'manual-writer', ownerId, spaceId: space.id, visibility: 'private',
    });
    const job = await repo.createJob({
      repo: localTaskRepoName(task.id), issueNumber: task.id, instruction: 'go',
      pieceName: 'manual-writer', ownerId, spaceId: space.id,
    });
    await repo.updateJob(job.id, { status: 'running' });
    return { space, task };
  }

  it('editor member can cancel a running job (200)', async () => {
    const { space, task } = await runningSpaceTask();
    const editor = repo.createUser({ email: 'ed2@x.com', name: 'ed2', role: 'user', status: 'active' });
    await repo.addSpaceMember({ spaceId: space.id, userId: editor.id, role: 'editor' });
    const res = await request(appAs(user(editor.id))).post(`/api/local/tasks/${task.id}/cancel`);
    expect(res.status).toBe(200);
  });

  it('viewer-only member is denied cancel with 403', async () => {
    const { space, task } = await runningSpaceTask();
    const v = repo.createUser({ email: 'v2@x.com', name: 'v2', role: 'user', status: 'active' });
    await repo.addSpaceMember({ spaceId: space.id, userId: v.id, role: 'viewer' });
    const res = await request(appAs(user(v.id))).post(`/api/local/tasks/${task.id}/cancel`);
    expect(res.status).toBe(403);
    expect(res.body.error).toBe('閲覧のみのため操作できません');
  });

  it('non-member gets 404 on cancel', async () => {
    const { task } = await runningSpaceTask();
    const stranger = repo.createUser({ email: 's3@x.com', name: 's3', role: 'user', status: 'active' });
    const res = await request(appAs(user(stranger.id))).post(`/api/local/tasks/${task.id}/cancel`);
    expect(res.status).toBe(404);
  });
});
