/**
 * APIS-038 — Space tool-policy sensitive-tool write round-trip (PR #653 regression class).
 *
 * The PR #653 save-loss bug was caused by the policy write path walking only the
 * CATEGORY system while the separate `sensitiveTools` channel (SENSITIVE_TOOLS,
 * e.g. Bash) was delivered via a different field. A regression here silently
 * grants or denies Bash/SSH/browser. The existing space-api.tool-policy.test.ts
 * covers the manager/member/stranger authz matrix and a single Bash+ssh PUT;
 * this file deepens the DUAL-CHANNEL persistence guarantee:
 *
 *   - a sensitive TOOL (Bash, separate `sensitiveTools` array) AND
 *   - sensitive CATEGORIES (ssh, browser, the `categories` array)
 *
 * must BOTH round-trip through PUT → persist → GET independently and together,
 * and a non-manager must never be able to write either channel.
 *
 * Auth pattern: authActive=true + middleware injecting req.user (Passport sim).
 * Roles: manager (space owner), member (viewer-role), stranger (no relation).
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import express from 'express';
import request from 'supertest';
import { Repository } from '../db/repository.js';
import { createSpaceApi } from './space-api.js';
import { SENSITIVE_CATEGORIES, SENSITIVE_TOOLS } from '../engine/tools/tool-categories.js';

function freshSetup() {
  const dir = mkdtempSync(join(tmpdir(), 'maestro-toolpolicy-sens-'));
  const repo = new Repository(join(dir, `${randomUUID()}.db`));
  return { repo, dir };
}

function makeApp(repo: Repository, currentUser: Express.User) {
  const app = express();
  app.use((req: any, _res: any, next: any) => {
    req.user = currentUser;
    next();
  });
  app.use(
    '/',
    createSpaceApi({
      repo,
      dataRoot: tmpdir(),
      worktreeDir: tmpdir(),
      authActive: true,
    }),
  );
  return app;
}

async function seed(repo: Repository) {
  const manager = repo.createUser({ email: 'mgr@x.com', name: 'Mgr', role: 'user', status: 'active' });
  const member = repo.createUser({ email: 'mem@x.com', name: 'Mem', role: 'user', status: 'active' });
  const stranger = repo.createUser({ email: 'str@x.com', name: 'Str', role: 'user', status: 'active' });
  // Public so member/stranger can GET (viewer-level read of the policy).
  const space = await repo.createSpace({ kind: 'case', title: 'TP Space', ownerId: manager.id, visibility: 'public' });
  await repo.addSpaceMember({ spaceId: space.id, userId: member.id, role: 'viewer', invitedBy: manager.id });
  return { manager, member, stranger, space };
}

function asUser(u: { id: string; role: 'admin' | 'user' }): Express.User {
  return { id: u.id, role: u.role, orgIds: [] } as unknown as Express.User;
}

/** Pull a sensitive TOOL entry (separate channel). Asserts the fixture is meaningful. */
function bashName(): string {
  const name = [...SENSITIVE_TOOLS][0];
  expect(name).toBeDefined();
  return name as string;
}

/** Pull the sensitive CATEGORY names (category channel). */
function sensitiveCategoryNames(): string[] {
  return [...SENSITIVE_CATEGORIES];
}

describe('APIS-038 tool-policy sensitive dual-channel round-trip', () => {
  let repo: Repository;
  let dir: string;
  let fx: Awaited<ReturnType<typeof seed>>;

  beforeEach(async () => {
    ({ repo, dir } = freshSetup());
    fx = await seed(repo);
  });

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

  it('manager enabling ONLY a sensitive TOOL (Bash) persists via the sensitiveTools channel', async () => {
    const app = makeApp(repo, asUser(fx.manager));
    const bash = bashName();

    const put = await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [bash] });
    expect(put.status).toBe(200);

    // The persisted JSON keeps the tool in enabledSensitive.
    expect(put.body.policy.enabledSensitive).toContain(bash);

    // Re-read from a fresh GET (proves it round-tripped through the DB, not echo).
    const get = await request(app).get(`/${fx.space.id}/tool-policy`);
    expect(get.status).toBe(200);

    // Channel 1: sensitiveTools array shows Bash enabled=true.
    const bashEntry = get.body.sensitiveTools.find((t: any) => t.name === bash);
    expect(bashEntry).toBeDefined();
    expect(bashEntry.enabled).toBe(true);

    // Channel 2 (categories) untouched: every sensitive category stays OFF.
    for (const catName of sensitiveCategoryNames()) {
      const cat = get.body.categories.find((c: any) => c.name === catName);
      expect(cat, catName).toBeDefined();
      expect(cat.enabled, `${catName} should stay OFF`).toBe(false);
    }
  });

  it('manager enabling ONLY sensitive CATEGORIES persists via the categories channel (Bash stays OFF)', async () => {
    const app = makeApp(repo, asUser(fx.manager));
    const cats = sensitiveCategoryNames();
    const bash = bashName();

    const put = await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: cats });
    expect(put.status).toBe(200);

    const get = await request(app).get(`/${fx.space.id}/tool-policy`);
    expect(get.status).toBe(200);

    // Channel 2: every sensitive category now enabled.
    for (const catName of cats) {
      const cat = get.body.categories.find((c: any) => c.name === catName);
      expect(cat, catName).toBeDefined();
      expect(cat.enabled, `${catName} should be ON`).toBe(true);
    }

    // Channel 1 untouched: Bash stays OFF (the separate-channel bug class).
    const bashEntry = get.body.sensitiveTools.find((t: any) => t.name === bash);
    expect(bashEntry.enabled).toBe(false);
  });

  it('manager enabling BOTH channels together persists both (no cross-channel clobber)', async () => {
    const app = makeApp(repo, asUser(fx.manager));
    const cats = sensitiveCategoryNames();
    const bash = bashName();

    const put = await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [bash, ...cats] });
    expect(put.status).toBe(200);

    const get = await request(app).get(`/${fx.space.id}/tool-policy`);

    // Tool channel enabled.
    expect(get.body.sensitiveTools.find((t: any) => t.name === bash).enabled).toBe(true);
    // Category channel enabled.
    for (const catName of cats) {
      expect(get.body.categories.find((c: any) => c.name === catName).enabled, catName).toBe(true);
    }
  });

  it('toggling a sensitive tool OFF again clears it from BOTH the persisted JSON and the GET view', async () => {
    const app = makeApp(repo, asUser(fx.manager));
    const bash = bashName();

    // Turn it on.
    await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [bash] });

    // Turn it off (empty enabledSensitive).
    const off = await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [] });
    expect(off.status).toBe(200);
    // Empty arrays are dropped from the clean policy.
    expect(off.body.policy.enabledSensitive).toBeUndefined();

    const get = await request(app).get(`/${fx.space.id}/tool-policy`);
    expect(get.body.sensitiveTools.find((t: any) => t.name === bash).enabled).toBe(false);
  });

  it('member (viewer) cannot write the sensitive-tool channel — 403, no persistence', async () => {
    const bash = bashName();
    const memberApp = makeApp(repo, asUser(fx.member));
    const res = await request(memberApp)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [bash] });
    expect(res.status).toBe(403);

    // Manager re-reads: Bash must still be OFF (the rejected write left no trace).
    const managerApp = makeApp(repo, asUser(fx.manager));
    const get = await request(managerApp).get(`/${fx.space.id}/tool-policy`);
    expect(get.body.sensitiveTools.find((t: any) => t.name === bash).enabled).toBe(false);
  });

  it('stranger on a public space cannot write the sensitive channel — 403', async () => {
    const bash = bashName();
    const app = makeApp(repo, asUser(fx.stranger));
    const res = await request(app)
      .put(`/${fx.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: [bash] });
    expect(res.status).toBe(403);
  });
});
