// スペース間資格情報コピー API のテスト。
//
// 両スペースの管理権を持つ actor のみが candidates 一覧取得と transfer 実行を
// 許可される。片方のみ管理者、または非メンバーは 403。
// 成功時に audit_log が 1 行増える。

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 { runMigrations } from '../db/migrate.js';
import { createSpaceApi, type SpaceApiDeps } from './space-api.js';
import { createRegistry } from '../mcp/registry.js';
import { createConnectionRepo } from '../ssh/connection-repo.js';
import { encryptPrivateKey, bootstrapSystemDek } from '../ssh/crypto.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 credential-transfer API', () => {
  let dir = '';
  let repo: Repository;
  let worktreeDir = '';

  // ユーザー
  let bothOwnerId = '';   // A と B 両方の owner
  let onlyBOwnerId = '';  // B のみ owner（A は非メンバー）
  let onlyAOwnerId = '';  // A のみ owner（B は非メンバー）
  let strangerId = '';    // どちらも非メンバー

  // スペース
  let spaceAId = '';
  let spaceBId = '';

  // MCP サーバー ID（スペース A に seeded）
  let mcpServerId = '';
  // SSH 接続 ID（スペース A に seeded）
  let sshConnId = '';

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

  beforeEach(async () => {
    process.env.MCP_ENCRYPTION_KEY = '0'.repeat(64);
    dir = mkdtempSync(join(tmpdir(), 'space-transfer-'));
    worktreeDir = join(dir, 'workspaces');
    repo = new Repository(join(dir, 'db.sqlite'));
    // auth_kind 等 migrate.ts で追加されるカラムを適用
    runMigrations(repo.getDb());

    // システム DEK を初期化（SSH 暗号化に必要）
    bootstrapSystemDek(repo.getDb());

    // ユーザー作成
    bothOwnerId = repo.createUser({ email: 'both@x.com', name: 'both', role: 'user', status: 'active' }).id;
    onlyBOwnerId = repo.createUser({ email: 'onlyb@x.com', name: 'onlyb', role: 'user', status: 'active' }).id;
    onlyAOwnerId = repo.createUser({ email: 'onlya@x.com', name: 'onlya', role: 'user', status: 'active' }).id;
    strangerId = repo.createUser({ email: 'stranger@x.com', name: 'stranger', role: 'user', status: 'active' }).id;

    // スペース A（bothOwner と onlyAOwner が owner）
    const spaceA = await repo.createSpace({ kind: 'case', title: 'Space A', ownerId: bothOwnerId, visibility: 'private' });
    spaceAId = spaceA.id;
    await repo.addSpaceMember({ spaceId: spaceAId, userId: onlyAOwnerId, role: 'owner', invitedBy: bothOwnerId });

    // スペース B（bothOwner と onlyBOwner が owner）
    const spaceB = await repo.createSpace({ kind: 'case', title: 'Space B', ownerId: bothOwnerId, visibility: 'private' });
    spaceBId = spaceB.id;
    await repo.addSpaceMember({ spaceId: spaceBId, userId: onlyBOwnerId, role: 'owner', invitedBy: bothOwnerId });

    // MCP サーバーをスペース A に追加
    const db = repo.getDb();
    const reg = createRegistry(db);
    reg.upsert({
      id: 'mcp-test-1',
      name: 'TestServer',
      url: 'https://test.example',
      authKind: 'api_key',
      staticToken: 'secret-token',
      ownerId: null,
      spaceId: spaceAId,
    });
    mcpServerId = 'mcp-test-1';

    // SSH 接続をスペース A に追加
    const connRepo = createConnectionRepo(db);
    const pem = '-----BEGIN OPENSSH PRIVATE KEY-----\nFAKEKEY\n-----END OPENSSH PRIVATE KEY-----';
    const { blob, keyVersion } = encryptPrivateKey(db, null, Buffer.from(pem), spaceAId);
    const sshConn = connRepo.create({
      ownerId: null,
      spaceId: spaceAId,
      label: 'prod-server',
      host: 'host.example',
      port: 22,
      username: 'deploy',
      privateKeyEnc: blob,
      passphraseEnc: null,
      keyVersion,
      keyFingerprint: null,
      remotePathPrefix: '/srv',
      allowRemoteUnrestricted: false,
      allowPrivateAddresses: false,
      commandDenyPatterns: null,
      commandAllowPatterns: null,
    });
    sshConnId = sshConn.id;
  });

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

  // ─── GET /candidates ─────────────────────────────────────────────

  describe('GET /:targetId/transfer/candidates', () => {
    it('両スペースを管理できる owner は 200 で mcp+ssh 候補一覧を受け取る', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceAId });
      expect(res.status).toBe(200);
      expect(res.body.mcp).toHaveLength(1);
      expect(res.body.mcp[0]).toMatchObject({ type: 'mcp', name: 'TestServer', willSkip: false });
      expect(res.body.ssh).toHaveLength(1);
      expect(res.body.ssh[0]).toMatchObject({ type: 'ssh', name: 'prod-server', willSkip: false });
    });

    it('sourceId がなければ 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`);
      expect(res.status).toBe(400);
    });

    it('sourceId === targetId は 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceBId });
      expect(res.status).toBe(400);
    });

    it('target のみ管理・source 非管理のユーザーは 403', async () => {
      const res = await request(appAs(user(onlyBOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceAId });
      expect(res.status).toBe(403);
    });

    it('source のみ管理・target 非管理のユーザーは 403', async () => {
      const res = await request(appAs(user(onlyAOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceAId });
      expect(res.status).toBe(403);
    });

    it('どちらも非メンバーは 403', async () => {
      const res = await request(appAs(user(strangerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceAId });
      expect(res.status).toBe(403);
    });
  });

  // ─── POST /transfer ──────────────────────────────────────────────

  describe('POST /:targetId/transfer', () => {
    it('両スペースを管理できる owner は 200 で copied を受け取る', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [mcpServerId], sshConnectionIds: [sshConnId] });
      expect(res.status).toBe(200);
      expect(res.body.copied).toHaveLength(2);
      expect(res.body.skipped).toHaveLength(0);
    });

    it('sourceId がなければ 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ mcpServerIds: [], sshConnectionIds: [] });
      expect(res.status).toBe(400);
    });

    it('sourceId === targetId は 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceBId, mcpServerIds: [], sshConnectionIds: [] });
      expect(res.status).toBe(400);
    });

    it('mcpServerIds が配列でなければ 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: 'bad', sshConnectionIds: [] });
      expect(res.status).toBe(400);
    });

    it('sshConnectionIds が配列でなければ 400', async () => {
      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [], sshConnectionIds: 'bad' });
      expect(res.status).toBe(400);
    });

    it('target のみ管理・source 非管理のユーザーは 403', async () => {
      const res = await request(appAs(user(onlyBOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [], sshConnectionIds: [] });
      expect(res.status).toBe(403);
    });

    it('source のみ管理・target 非管理のユーザーは 403', async () => {
      const res = await request(appAs(user(onlyAOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [], sshConnectionIds: [] });
      expect(res.status).toBe(403);
    });

    it('どちらも非メンバーは 403', async () => {
      const res = await request(appAs(user(strangerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [], sshConnectionIds: [] });
      expect(res.status).toBe(403);
    });

    it('POST 成功で audit_log が 1 行増える', async () => {
      const db = repo.getDb();
      const before = (db.prepare('SELECT COUNT(*) AS n FROM audit_log').get() as { n: number }).n;

      const res = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [mcpServerId], sshConnectionIds: [] });
      expect(res.status).toBe(200);

      const after = (db.prepare('SELECT COUNT(*) AS n FROM audit_log').get() as { n: number }).n;
      expect(after).toBe(before + 1);

      // 監査ログの内容確認
      const row = db.prepare("SELECT action, actor, detail FROM audit_log ORDER BY id DESC LIMIT 1").get() as {
        action: string; actor: string; detail: string;
      };
      expect(row.action).toBe('space.credential.transfer');
      expect(row.actor).toBe(bothOwnerId);
      const detail = JSON.parse(row.detail) as Record<string, unknown>;
      expect(detail.sourceId).toBe(spaceAId);
      expect(detail.targetId).toBe(spaceBId);
    });

    it('候補・転送レスポンスにシークレットを出さない (Fix 4)', async () => {
      // GET candidates
      const candRes = await request(appAs(user(bothOwnerId)))
        .get(`/api/local/spaces/${spaceBId}/transfer/candidates`)
        .query({ sourceId: spaceAId });
      expect(candRes.status).toBe(200);
      expect(JSON.stringify(candRes.body)).not.toContain('secret-token');
      expect(JSON.stringify(candRes.body)).not.toContain('FAKEKEY');

      // POST transfer
      const transferRes = await request(appAs(user(bothOwnerId)))
        .post(`/api/local/spaces/${spaceBId}/transfer`)
        .send({ sourceId: spaceAId, mcpServerIds: [mcpServerId], sshConnectionIds: [sshConnId] });
      expect(transferRes.status).toBe(200);
      expect(JSON.stringify(transferRes.body)).not.toContain('secret-token');
      expect(JSON.stringify(transferRes.body)).not.toContain('FAKEKEY');
    });
  });
});
