// Membership-based write gates on space sub-routes (calendar + settings).
//
// Stage 1 grants members read access via buildVisibilityWhere; stage 2 widens
// the write gates so an editor+ member can manage the shared calendar and an
// owner-role member can edit space settings. A viewer member is read-only; a
// non-member gets 404 from the read-gate (getSpace) and never reaches the write
// gate.

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 write gates by member role (calendar + settings)', () => {
  let dir = '';
  let repo: Repository;
  let ownerId = '';
  let editorId = '';
  let viewerId = '';
  let strangerId = '';
  let spaceId = '';

  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: join(dir, 'workspaces'),
      authActive,
    };
    app.use('/api/local/spaces', createSpaceApi(deps));
    return app;
  }

  beforeEach(async () => {
    dir = mkdtempSync(join(tmpdir(), 'space-wgate-'));
    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;
    viewerId = repo.createUser({ email: 'viewer@x.com', name: 'viewer', 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: '案件', ownerId, visibility: 'private' });
    spaceId = space.id;
    await repo.addSpaceMember({ spaceId, userId: editorId, role: 'editor', invitedBy: ownerId });
    await repo.addSpaceMember({ spaceId, userId: viewerId, role: 'viewer', invitedBy: ownerId });
  });

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

  const owner = () => user(ownerId);
  const editor = () => user(editorId);
  const viewer = () => user(viewerId);
  const stranger = () => user(strangerId);

  // ── Calendar event creation ────────────────────────────────────────────────

  it('editor member CAN create a calendar event', async () => {
    const res = await request(appAs(editor()))
      .post(`/api/local/spaces/${spaceId}/calendar/events`)
      .send({ date: '2026-06-10', title: 'editor event' });
    expect(res.status).toBe(201);
    expect(res.body.title).toBe('editor event');
  });

  it('viewer member CANNOT create a calendar event → 403', async () => {
    const res = await request(appAs(viewer()))
      .post(`/api/local/spaces/${spaceId}/calendar/events`)
      .send({ date: '2026-06-10', title: 'nope' });
    expect(res.status).toBe(403);
  });

  it('non-member CANNOT create a calendar event → 404 (read-gate)', async () => {
    const res = await request(appAs(stranger()))
      .post(`/api/local/spaces/${spaceId}/calendar/events`)
      .send({ date: '2026-06-10', title: 'leak' });
    expect(res.status).toBe(404);
  });

  it('editor member CAN PATCH + DELETE a calendar event', async () => {
    const ev = await repo.createCalendarEvent({ spaceId, date: '2026-06-11', title: 'orig' });
    const patched = await request(appAs(editor()))
      .patch(`/api/local/spaces/${spaceId}/calendar/events/${ev.id}`)
      .send({ title: 'updated' });
    expect(patched.status).toBe(200);
    expect(patched.body.title).toBe('updated');
    const del = await request(appAs(editor())).delete(`/api/local/spaces/${spaceId}/calendar/events/${ev.id}`);
    expect(del.status).toBe(204);
  });

  it('viewer member CANNOT PATCH a calendar event → 403', async () => {
    const ev = await repo.createCalendarEvent({ spaceId, date: '2026-06-11', title: 'orig' });
    const res = await request(appAs(viewer()))
      .patch(`/api/local/spaces/${spaceId}/calendar/events/${ev.id}`)
      .send({ title: 'hijack' });
    expect(res.status).toBe(403);
  });

  it('viewer + editor + stranger can all GET the calendar (read), non-member 404', async () => {
    expect((await request(appAs(viewer())).get(`/api/local/spaces/${spaceId}/calendar?month=2026-06`)).status).toBe(200);
    expect((await request(appAs(editor())).get(`/api/local/spaces/${spaceId}/calendar?month=2026-06`)).status).toBe(200);
    expect((await request(appAs(stranger())).get(`/api/local/spaces/${spaceId}/calendar?month=2026-06`)).status).toBe(404);
  });

  // ── Space settings PATCH + archive ─────────────────────────────────────────

  it('owner can PATCH brand_color/settings', async () => {
    const res = await request(appAs(owner()))
      .patch(`/api/local/spaces/${spaceId}`)
      .send({ brandColor: '#ff0000', title: 'renamed' });
    expect(res.status).toBe(200);
    expect(res.body.brandColor).toBe('#ff0000');
  });

  it('member-with-owner-role CAN PATCH brand_color/settings', async () => {
    const moId = repo.createUser({ email: 'mo@x.com', name: 'mo', role: 'user', status: 'active' }).id;
    await repo.addSpaceMember({ spaceId, userId: moId, role: 'owner', invitedBy: ownerId });
    const res = await request(appAs(user(moId)))
      .patch(`/api/local/spaces/${spaceId}`)
      .send({ brandColor: '#00ff00' });
    expect(res.status).toBe(200);
    expect(res.body.brandColor).toBe('#00ff00');
  });

  it('editor member CANNOT PATCH settings → 403 (manage requires owner-role)', async () => {
    const res = await request(appAs(editor()))
      .patch(`/api/local/spaces/${spaceId}`)
      .send({ brandColor: '#abcdef' });
    expect(res.status).toBe(403);
  });

  it('viewer member CANNOT PATCH settings → 403', async () => {
    const res = await request(appAs(viewer()))
      .patch(`/api/local/spaces/${spaceId}`)
      .send({ title: 'no' });
    expect(res.status).toBe(403);
  });

  it('non-member PATCH settings → 404 (read-gate, never leaks)', async () => {
    const res = await request(appAs(stranger()))
      .patch(`/api/local/spaces/${spaceId}`)
      .send({ title: 'leak' });
    expect(res.status).toBe(404);
  });

  it('editor member CANNOT archive → 403; member-owner CAN', async () => {
    expect((await request(appAs(editor())).post(`/api/local/spaces/${spaceId}/archive`)).status).toBe(403);
    const moId = repo.createUser({ email: 'mo2@x.com', name: 'mo2', role: 'user', status: 'active' }).id;
    await repo.addSpaceMember({ spaceId, userId: moId, role: 'owner', invitedBy: ownerId });
    expect((await request(appAs(user(moId))).post(`/api/local/spaces/${spaceId}/archive`)).status).toBe(200);
  });

  // ── File upload gate ───────────────────────────────────────────────────────

  it('editor member CAN upload a file; viewer CANNOT (403); non-member 404', async () => {
    const payload = { files: [{ name: 'note.txt', contentBase64: Buffer.from('hi').toString('base64') }] };
    expect((await request(appAs(editor())).post(`/api/local/spaces/${spaceId}/files/upload`).send(payload)).status).toBe(200);
    expect((await request(appAs(viewer())).post(`/api/local/spaces/${spaceId}/files/upload`).send(payload)).status).toBe(403);
    expect((await request(appAs(stranger())).post(`/api/local/spaces/${spaceId}/files/upload`).send(payload)).status).toBe(404);
  });
});
