// Integration: a task created inside case space A, when its effective space is
// resolved, must see ONLY space A's MCP servers and SSH connections — never
// space B's, and never the admin-global (space-less) set. This wires the real
// Repository DB to the real McpRegistry and SshConnectionRepo (same DB) so the
// full path is exercised, not just the unit-level list functions.
//
// Unit coverage for the list/access primitives already exists
// (src/mcp/registry.test.ts, src/ssh/access.test.ts); this guards the seam
// where a space-tagged task hands its space_id to those primitives.

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 { Repository } from './repository.js';
import { runMigrations } from './migrate.js';
import { createRegistry } from '../mcp/registry.js';
import { createConnectionRepo } from '../ssh/connection-repo.js';

describe('per-space MCP/SSH resource isolation (integration)', () => {
  let dir = '';
  let repo: Repository;
  let ownerId = '';
  const validKey = 'a'.repeat(64);

  beforeEach(() => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    dir = mkdtempSync(join(tmpdir(), 'space-res-iso-'));
    repo = new Repository(join(dir, `${randomUUID()}.db`));
    // Bring mcp_servers / ssh_connections up to the registry/connection-repo's
    // expected shape (production runs migrate.ts after schema.sql). Idempotent.
    runMigrations(repo.getDb());
    ownerId = repo.createUser({ email: 'o@x.com', name: 'o', role: 'user', status: 'active' }).id;
  });

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

  it('a space-A task sees only space-A MCP servers (not space-B, not global)', async () => {
    const spaceA = await repo.createSpace({ kind: 'case', title: '案件A', ownerId });
    const spaceB = await repo.createSpace({ kind: 'case', title: '案件B', ownerId });
    const taskA = await repo.createLocalTask({ title: 't', body: 'b', ownerId, spaceId: spaceA.id });

    const reg = createRegistry(repo.getDb());
    reg.upsert({ id: 'srv-a', name: 'A', url: 'https://a.example/mcp', authKind: 'api_key', ownerId, staticToken: 'tok', spaceId: spaceA.id });
    reg.upsert({ id: 'srv-b', name: 'B', url: 'https://b.example/mcp', authKind: 'api_key', ownerId, staticToken: 'tok', spaceId: spaceB.id });
    reg.upsert({ id: 'srv-g', name: 'G', url: 'https://g.example/mcp', authKind: 'oauth', ownerId: null, oauthClientId: 'g', oauthClientSecret: 'gs' });

    // The task's effective space is its space_id (set), as the worker resolves it.
    const loaded = await repo.getLocalTask(taskA.id);
    const visible = reg.listEnabledForSpace(loaded!.spaceId).map(s => s.id);

    expect(visible).toEqual(['srv-a']);
    expect(visible).not.toContain('srv-b');
    expect(visible).not.toContain('srv-g'); // admin-global must NOT leak in
  });

  it('a space-A task sees only space-A SSH connections (not space-B, not global)', async () => {
    const spaceA = await repo.createSpace({ kind: 'case', title: '案件A', ownerId });
    const spaceB = await repo.createSpace({ kind: 'case', title: '案件B', ownerId });
    const taskA = await repo.createLocalTask({ title: 't', body: 'b', ownerId, spaceId: spaceA.id });

    const conns = createConnectionRepo(repo.getDb());
    const base = {
      label: 'srv', host: 'h', port: 22, username: 'u',
      privateKeyEnc: Buffer.from([1]), keyFingerprint: 'SHA256:x', remotePathPrefix: '/home/u',
    };
    const connA = conns.create({ ownerId, spaceId: spaceA.id, ...base });
    const connB = conns.create({ ownerId, spaceId: spaceB.id, ...base });
    const connGlobal = conns.create({ ownerId, spaceId: null, ...base });

    const loaded = await repo.getLocalTask(taskA.id);
    const inSpaceA = conns.listBySpace(loaded!.spaceId!).map(c => c.id);

    expect(inSpaceA).toContain(connA.id);
    expect(inSpaceA).not.toContain(connB.id);     // sibling space hidden
    expect(inSpaceA).not.toContain(connGlobal.id); // space-less/global hidden
    expect(inSpaceA).toHaveLength(1);
  });

  it('a personal (space-less) task does NOT see any case-space resources', async () => {
    const spaceA = await repo.createSpace({ kind: 'case', title: '案件A', ownerId });
    const personalTask = await repo.createLocalTask({ title: 't', body: 'b', ownerId });

    const reg = createRegistry(repo.getDb());
    reg.upsert({ id: 'srv-a', name: 'A', url: 'https://a.example/mcp', authKind: 'api_key', ownerId, staticToken: 'tok', spaceId: spaceA.id });
    reg.upsert({ id: 'srv-g', name: 'G', url: 'https://g.example/mcp', authKind: 'oauth', ownerId: null, oauthClientId: 'g', oauthClientSecret: 'gs' });

    const loaded = await repo.getLocalTask(personalTask.id);
    expect(loaded!.spaceId).toBeNull();
    // A space-less task uses the legacy/global scope (space_id IS NULL) only.
    const visible = reg.listEnabledForSpace(loaded!.spaceId).map(s => s.id);
    expect(visible).toEqual(['srv-g']);
    expect(visible).not.toContain('srv-a');
  });
});
