// Membership-aware READ gate exercised end-to-end through the HTTP API.
//
// Core regression: now that space tasks are forced visibility='private', a
// space member (or root owner) loading ANOTHER member's chat detail / files /
// logs via the secondary per-request gate (canViewTask) must get 200, not 404.
// A non-member must still get 404. This proves both gaps are closed:
//   gap-1: buildVisibilityWhere now grants the root owner access, and
//   gap-2: canViewTask is membership-aware via repo.userCanViewSpace.
// spec: docs/superpowers/specs/2026-06-19-shared-space-design.md

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { Repository } from '../db/repository.js';
import { mountLocalTasksApi } from './local-tasks-api.js';
import { mountLocalFilesApi } from './local-files-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,
  };
}

describe('space membership READ gate (HTTP, real repo)', () => {
  let dir = '';
  let repo: Repository;
  let memberId = '';
  let strangerId = '';
  let spaceId = '';
  let taskId = 0;

  beforeEach(async () => {
    dir = mkdtempSync(join(tmpdir(), 'lt-space-view-'));
    repo = new Repository(join(dir, 'db.sqlite'));
    const owner = repo.createUser({ email: 'o@x.com', name: 'o', role: 'user', status: 'active' });
    const memberB = repo.createUser({ email: 'b@x.com', name: 'b', role: 'user', status: 'active' });
    const memberA = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const stranger = repo.createUser({ email: 's@x.com', name: 's', role: 'user', status: 'active' });
    memberId = memberA.id;
    strangerId = stranger.id;

    const space = await repo.createSpace({ kind: 'case', title: '案件', ownerId: owner.id, visibility: 'private' });
    spaceId = space.id;
    await repo.addSpaceMember({ spaceId, userId: memberA.id, role: 'viewer' });
    await repo.addSpaceMember({ spaceId, userId: memberB.id, role: 'editor' });

    // Task owned by memberB, private, inside the space, with a real workspace.
    const ws = join(dir, 'ws-task');
    mkdirSync(join(ws, 'logs'), { recursive: true });
    const task = await repo.createLocalTask({
      title: 'b task', body: 'x', ownerId: memberB.id, visibility: 'private',
      spaceId, workspacePath: ws,
    });
    taskId = task.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') });
    mountLocalFilesApi(app, repo);
    return app;
  }

  it('member A (different from task owner B) GETs the chat detail → 200', async () => {
    const res = await request(appAs(user(memberId))).get(`/api/local/tasks/${taskId}`);
    expect(res.status).toBe(200);
    expect(res.body.task.id).toBe(taskId);
  });

  it('member A GETs the Files tab (section=logs) → 200', async () => {
    const res = await request(appAs(user(memberId))).get(`/api/local/tasks/${taskId}/files?section=logs`);
    expect(res.status).toBe(200);
    expect(res.body.basePath).toBe('logs');
  });

  it('stranger (non-member) GET chat detail → 404', async () => {
    const res = await request(appAs(user(strangerId))).get(`/api/local/tasks/${taskId}`);
    expect(res.status).toBe(404);
  });

  it('stranger (non-member) GET Files tab → 404', async () => {
    const res = await request(appAs(user(strangerId))).get(`/api/local/tasks/${taskId}/files?section=logs`);
    expect(res.status).toBe(404);
  });
});
