/**
 * Multi-user integration tests for GET/PUT /api/local/spaces/:id/tool-policy.
 *
 * Auth pattern: authActive=true + middleware that sets req.user = <seeded user>.
 * Roles: manager (space owner), member (viewer-role member), stranger (no relation).
 *
 * Space visibility is 'public' so GET succeeds for viewer and member without
 * needing org membership — consistent with how other space-api viewer routes work.
 */
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';

// ─── Test fixtures ──────────────────────────────────────────────────────────

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

function makeApp(repo: Repository, currentUser: Express.User) {
  const app = express();
  // Inject the current user (simulating Passport session middleware).
  app.use((req: any, _res: any, next: any) => {
    req.user = currentUser;
    next();
  });
  app.use(
    '/',
    createSpaceApi({
      repo,
      dataRoot: tmpdir(),
      worktreeDir: tmpdir(),
      authActive: true,  // enables viewerOf to read req.user
    }),
  );
  return app;
}

// ─── Seed helpers ───────────────────────────────────────────────────────────

async function seed(repo: Repository) {
  const manager = repo.createUser({
    email: 'manager@example.com',
    name: 'Manager',
    role: 'user',
    status: 'active',
  });
  const member = repo.createUser({
    email: 'member@example.com',
    name: 'Member',
    role: 'user',
    status: 'active',
  });
  const stranger = repo.createUser({
    email: 'stranger@example.com',
    name: 'Stranger',
    role: 'user',
    status: 'active',
  });

  // Public space so member and stranger can GET (viewer reads the policy)
  const space = await repo.createSpace({
    kind: 'case',
    title: 'Test Space',
    ownerId: manager.id,
    visibility: 'public',
  });

  // Add member as viewer-role member
  await repo.addSpaceMember({
    spaceId: space.id,
    userId: member.id,
    role: 'viewer',
    invitedBy: manager.id,
  });

  return { manager, member, stranger, space };
}

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

// ─── Tests ──────────────────────────────────────────────────────────────────

describe('GET /api/local/spaces/:id/tool-policy', () => {
  let repo: Repository;
  let dir: string;
  let fixtures: Awaited<ReturnType<typeof seed>>;

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

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

  it('manager (owner) receives 200 with categories + policy shape', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.manager));
    const res = await request(app).get(`/${fixtures.space.id}/tool-policy`);
    expect(res.status).toBe(200);

    // policy shape
    expect(res.body).toHaveProperty('policy');
    expect(res.body.policy).toEqual({});  // null policy → empty object

    // categories array
    expect(res.body).toHaveProperty('categories');
    expect(Array.isArray(res.body.categories)).toBe(true);
    expect(res.body.categories.length).toBeGreaterThan(0);

    for (const cat of res.body.categories) {
      expect(cat).toHaveProperty('name');
      expect(cat).toHaveProperty('sensitive');
      expect(cat).toHaveProperty('enabled');
      expect(typeof cat.name).toBe('string');
      expect(typeof cat.sensitive).toBe('boolean');
      expect(typeof cat.enabled).toBe('boolean');

      // safe categories default ON; sensitive categories default OFF
      if (SENSITIVE_CATEGORIES.has(cat.name)) {
        expect(cat.sensitive).toBe(true);
        expect(cat.enabled).toBe(false);
      } else {
        expect(cat.sensitive).toBe(false);
        expect(cat.enabled).toBe(true);
      }
    }

    // sensitiveTools array
    expect(res.body).toHaveProperty('sensitiveTools');
    expect(Array.isArray(res.body.sensitiveTools)).toBe(true);
    for (const t of res.body.sensitiveTools) {
      expect(SENSITIVE_TOOLS.has(t.name)).toBe(true);
      expect(t.enabled).toBe(false);  // default OFF
    }
  });

  it('member (viewer-role) receives 200', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.member));
    const res = await request(app).get(`/${fixtures.space.id}/tool-policy`);
    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('policy');
    expect(res.body).toHaveProperty('categories');
  });

  it('stranger cannot see a private space (404)', async () => {
    // Create a private space and verify stranger gets 404
    const priv = await repo.createSpace({
      kind: 'case',
      title: 'Private',
      ownerId: fixtures.manager.id,
      visibility: 'private',
    });
    const app = makeApp(repo, asExpressUser(fixtures.stranger));
    const res = await request(app).get(`/${priv.id}/tool-policy`);
    expect(res.status).toBe(404);
  });

  it('public space: stranger receives 200 (viewer-level read)', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.stranger));
    const res = await request(app).get(`/${fixtures.space.id}/tool-policy`);
    expect(res.status).toBe(200);
  });
});

describe('PUT /api/local/spaces/:id/tool-policy', () => {
  let repo: Repository;
  let dir: string;
  let fixtures: Awaited<ReturnType<typeof seed>>;

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

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

  it('manager (owner) can PUT and GET reflects the change', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.manager));
    const body = { disabledSafe: ['core'], enabledSensitive: ['Bash', 'ssh'] };

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

    // Returned shape same as GET
    expect(put.body).toHaveProperty('policy');
    expect(put.body.policy.disabledSafe).toEqual(['core']);
    expect(put.body.policy.enabledSensitive).toContain('Bash');
    expect(put.body.policy.enabledSensitive).toContain('ssh');

    // Subsequent GET reflects persisted change
    const get = await request(app).get(`/${fixtures.space.id}/tool-policy`);
    expect(get.status).toBe(200);
    expect(get.body.policy.disabledSafe).toEqual(['core']);
    expect(get.body.policy.enabledSensitive).toContain('Bash');

    // Bash should now show enabled=true in sensitiveTools
    const bashEntry = get.body.sensitiveTools.find((t: any) => t.name === 'Bash');
    expect(bashEntry).toBeDefined();
    expect(bashEntry.enabled).toBe(true);

    // ssh category should now show enabled=true
    const sshCat = get.body.categories.find((c: any) => c.name === 'ssh');
    expect(sshCat).toBeDefined();
    expect(sshCat.enabled).toBe(true);
  });

  it('member (viewer-role member) receives 403 on PUT', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.member));
    const res = await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: ['Bash'] });
    expect(res.status).toBe(403);
  });

  it('stranger on a private space receives 404 on PUT', async () => {
    const priv = await repo.createSpace({
      kind: 'case',
      title: 'Private',
      ownerId: fixtures.manager.id,
      visibility: 'private',
    });
    const app = makeApp(repo, asExpressUser(fixtures.stranger));
    const res = await request(app)
      .put(`/${priv.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: ['Bash'] });
    expect(res.status).toBe(404);
  });

  it('stranger on a public space receives 403 on PUT', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.stranger));
    const res = await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: ['Bash'] });
    expect(res.status).toBe(403);
  });

  it('PUT with unknown category/tool name returns 400', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.manager));
    const res = await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: ['Bash', 'nonexistent-tool-xyz'] });
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/unknown/i);
  });

  it('PUT with non-array fields returns 400', async () => {
    const app = makeApp(repo, asExpressUser(fixtures.manager));
    const res = await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ disabledSafe: 'not-an-array' });
    expect(res.status).toBe(400);
  });

  it('PUT with empty body clears the policy', async () => {
    // First set something
    const app = makeApp(repo, asExpressUser(fixtures.manager));
    await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({ enabledSensitive: ['Bash'] });

    // Then clear
    const res = await request(app)
      .put(`/${fixtures.space.id}/tool-policy`)
      .set('Content-Type', 'application/json')
      .send({});
    expect(res.status).toBe(200);
    expect(res.body.policy).toEqual({});
  });
});
