/**
 * Unit tests for the SSH tool dispatcher (engine/tools/ssh.ts).
 *
 * Strategy: bring up an in-memory SQLite + real repos (connection / audit /
 * abuse / grants) so the audit + abuse + access decisions are exercised end-to-
 * end, but stub the session primitives (sshExec / sshUpload / sshDownload) so
 * no real SSH server is needed. The Phase 3 ssh-session tests already cover
 * the session module; here we only verify the 12-step orchestration.
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { promises as fsp } from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { runMigrations } from '../../db/migrate.js';
import { createConnectionRepo, type CreateConnectionInput, type SshConnectionRepo } from '../../ssh/connection-repo.js';
import { createGrantsRepo, type SshGrantsRepo } from '../../ssh/grants-repo.js';
import { createAuditRepo, type SshAuditRepo } from '../../ssh/audit-repo.js';
import { createAbuseRepo, type SshAbuseRepo } from '../../ssh/abuse-repo.js';
import { createAccessResolver } from '../../ssh/access.js';
import { SshSessionError } from '../../ssh/session.js';
import { SSH_DEFAULTS, type SshRuntimeConfig } from '../../ssh/config.js';
import type { MaintenanceController } from '../../ssh/maintenance.js';
import type { ExecArgs, UploadArgs, DownloadArgs, SessionHooks } from '../../ssh/session.js';
import {
  setSshSubsystem,
  type SshSubsystem,
  TOOL_DEFS,
} from './ssh.js';
import { executeTool } from './index.js';
import type { ToolContext } from './core.js';

const VALID_KEY = 'a'.repeat(64);

function bootstrapDb(): Database.Database {
  process.env.MCP_ENCRYPTION_KEY = VALID_KEY;
  const db = new Database(':memory:');
  db.pragma('foreign_keys = ON');
  db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY, role TEXT);`);
  db.exec(`CREATE TABLE jobs (id TEXT PRIMARY KEY, wait_reason TEXT);`);
  db.exec(`CREATE TABLE local_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT);`);
  runMigrations(db);
  db.prepare(`INSERT INTO users (id, role) VALUES (?, ?), (?, ?), (?, ?)`).run(
    'alice', 'member',
    'bob', 'member',
    'admin1', 'admin',
  );
  return db;
}

function makeConfig(overrides: Partial<SshRuntimeConfig> = {}): SshRuntimeConfig {
  return { ...SSH_DEFAULTS, ...overrides };
}

function makeMaintenance(active = false): MaintenanceController {
  return {
    isActive: () => active,
    snapshot: () => ({ active, reason: active ? 'test' : undefined }),
    enter: () => undefined,
    exit: () => undefined,
  };
}

interface Stubs {
  execImpl?: (args: ExecArgs, hooks: SessionHooks) => Promise<ReturnType<SshSubsystem['sshExec']> extends Promise<infer R> ? R : never>;
  uploadImpl?: (args: UploadArgs, hooks: SessionHooks) => Promise<ReturnType<SshSubsystem['sshUpload']> extends Promise<infer R> ? R : never>;
  downloadImpl?: (args: DownloadArgs, hooks: SessionHooks) => Promise<ReturnType<SshSubsystem['sshDownload']> extends Promise<infer R> ? R : never>;
}

function makeSubsystem(opts: {
  db: Database.Database;
  config?: SshRuntimeConfig;
  maintenance?: MaintenanceController;
  stubs?: Stubs;
  userAccess?: Record<string, { isAdmin: boolean; orgIds: string[] }>;
}): { sub: SshSubsystem; repos: { conn: SshConnectionRepo; audit: SshAuditRepo; abuse: SshAbuseRepo; grants: SshGrantsRepo } } {
  const conn = createConnectionRepo(opts.db);
  const grants = createGrantsRepo(opts.db);
  const audit = createAuditRepo(opts.db);
  const abuse = createAbuseRepo(opts.db, {
    windowMinutes: 10,
    failureThreshold: 5,
    lockMinutes: 30,
  });
  const access = createAccessResolver(grants, { adminBypassesGrants: true });
  const sub: SshSubsystem = {
    connectionRepo: conn,
    auditRepo: audit,
    abuseRepo: abuse,
    accessResolver: access,
    decryptKeyMaterial: (_ownerId, blob) => Buffer.from(blob), // identity (we stub session anyway)
    decryptPassphrase: (_ownerId, blob) => (blob ? Buffer.from(blob) : null),
    getUserAccess: (userId) => opts.userAccess?.[userId] ?? { isAdmin: false, orgIds: [] },
    sshExec: opts.stubs?.execImpl
      ? opts.stubs.execImpl
      : async () => ({
          outputJson: JSON.stringify({ stdout: 'ok', exit_code: 0, untrusted: true }),
          exitCode: 0,
          durationMs: 12,
          hostFingerprint: 'SHA256:fake',
        }),
    sshUpload: opts.stubs?.uploadImpl
      ? opts.stubs.uploadImpl
      : async () => ({ bytes: 4, durationMs: 5, hostFingerprint: 'SHA256:fake' }),
    sshDownload: opts.stubs?.downloadImpl
      ? opts.stubs.downloadImpl
      : async () => ({ bytes: 4, durationMs: 5, hostFingerprint: 'SHA256:fake' }),
    maintenance: opts.maintenance ?? makeMaintenance(false),
    config: opts.config ?? makeConfig(),
    // Phase 3 (SSH Console) deps — these tests don't exercise console tools,
    // so a dummy registry + unreachable openShellChannel keep the interface
    // satisfied without booting the headless terminal.
    sessionRegistry: {
      register: () => undefined,
      get: () => null,
      listAll: () => [],
      listForConnection: () => [],
      closeForTask: async () => undefined,
      enforceCap: () => [],
      sweep: async () => undefined,
      startSweepTimer: () => undefined,
      stopSweepTimer: () => undefined,
      shutdown: async () => undefined,
    } as unknown as SshSubsystem['sessionRegistry'],
    openShellChannel: async () => {
      throw new Error('openShellChannel not stubbed in this test');
    },
  };
  return { sub, repos: { conn, audit, abuse, grants } };
}

function baseConnInput(overrides: Partial<CreateConnectionInput> = {}): CreateConnectionInput {
  return {
    ownerId: 'alice',
    label: 'prod-srv',
    host: 'srv.example.com',
    port: 22,
    username: 'deploy',
    privateKeyEnc: Buffer.from('encrypted-pem'),
    keyFingerprint: 'SHA256:fp',
    remotePathPrefix: '/srv/agent',
    ...overrides,
  };
}

async function ctxWithWorkspace(opts: {
  workspace?: string;
  userId?: string;
  pieceName?: string;
  allowed?: string[];
  jobId?: string;
}): Promise<ToolContext> {
  const workspace = opts.workspace ?? (await fsp.mkdtemp(path.join(os.tmpdir(), 'ssh-tool-')));
  return {
    workspacePath: workspace,
    editAllowed: true,
    userId: opts.userId ?? 'alice',
    ownerId: opts.userId ?? 'alice',
    pieceName: opts.pieceName ?? 'ops',
    allowedSshConnections: opts.allowed,
    jobId: opts.jobId ?? 'job1',
  };
}

describe('engine/tools/ssh — subsystem gating', () => {
  beforeEach(() => setSshSubsystem(null));
  afterEach(() => setSshSubsystem(null));

  it('rejects with "not initialised" when subsystem is null', async () => {
    const ctx = await ctxWithWorkspace({ allowed: ['*'] });
    const r = await executeTool('SshExec', { connection_id: 'x', command: 'whoami' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/not initialised/);
  });

  it('rejects with "in maintenance" when maintenance is active', async () => {
    const db = bootstrapDb();
    const { sub } = makeSubsystem({ db, maintenance: makeMaintenance(true) });
    setSshSubsystem(sub);
    const ctx = await ctxWithWorkspace({ allowed: ['*'] });
    const r = await executeTool('SshExec', { connection_id: 'x', command: 'whoami' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/in maintenance/);
  });
});

describe('engine/tools/ssh — preflight (piece + access + state)', () => {
  let db: Database.Database;
  let sub: SshSubsystem;
  let repos: ReturnType<typeof makeSubsystem>['repos'];
  let connId: string;

  beforeEach(() => {
    db = bootstrapDb();
    ({ sub, repos } = makeSubsystem({ db }));
    setSshSubsystem(sub);
    const created = repos.conn.create(
      baseConnInput({
        ownerId: 'alice',
        commandDenyPatterns: '^rm -rf /\\b',
      }),
    );
    // Verify the host key so SshExec passes step 6
    db.prepare(
      `UPDATE ssh_connections SET host_key_b64=?, host_key_fingerprint=?, host_key_verified_at=? WHERE id=?`,
    ).run('hostb64', 'SHA256:host', new Date().toISOString(), created.id);
    connId = created.id;
  });

  afterEach(() => setSshSubsystem(null));

  it('rejects when allowed_ssh_connections is undefined', async () => {
    const ctx = await ctxWithWorkspace({}); // allowed: undefined
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/does not declare allowed_ssh_connections/);
  });

  it('rejects when connection_id is not in allowed list', async () => {
    const ctx = await ctxWithWorkspace({ allowed: ['00000000-other'] });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/not in this piece's allowed_ssh_connections/);
    const rows = repos.audit.listForConnection(connId, 10);
    expect(rows[0].outcome).toBe('denied');
    expect(rows[0].detail).toMatchObject({ reason: 'piece_not_allowed' });
  });

  it('honours wildcard "*" in allowed list', async () => {
    const ctx = await ctxWithWorkspace({ allowed: ['*'] });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(false);
  });

  it('rejects when connection does not exist', async () => {
    const ctx = await ctxWithWorkspace({ allowed: ['ffffffff-aaaa-bbbb-cccc-dddddddddddd'] });
    const r = await executeTool(
      'SshExec',
      { connection_id: 'ffffffff-aaaa-bbbb-cccc-dddddddddddd', command: 'ls' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/does not exist/);
    const rows = repos.audit.listPending().concat([]);
    const denied = db.prepare(`SELECT * FROM ssh_audit_log WHERE outcome = 'denied' ORDER BY id DESC LIMIT 1`).get() as { detail: string };
    expect(JSON.parse(denied.detail)).toMatchObject({ reason: 'unknown_connection' });
  });

  it('rejects non-owner without a grant', async () => {
    const ctx = await ctxWithWorkspace({ userId: 'bob', allowed: [connId] });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/access denied/);
    const denied = db.prepare(`SELECT * FROM ssh_audit_log WHERE outcome = 'denied' ORDER BY id DESC LIMIT 1`).get() as { detail: string };
    expect(JSON.parse(denied.detail)).toMatchObject({ reason: 'no_grant' });
  });

  it('rejects when admin disabled (access resolver catches before state check)', async () => {
    repos.conn.disableByAdmin(connId, 'security review', 'admin1');
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(true);
    // The access resolver short-circuits on `enabled=false` with reason='disabled' (step 5)
    // before our explicit disabled_by_admin state check (step 6) runs. Both paths are correct;
    // the resolver path is what fires.
    expect(r.output).toMatch(/access denied \(disabled\)/);
  });

  it('rejects when abuse-locked', async () => {
    // Saturate the abuse counter to trip the lock.
    for (let i = 0; i < 5; i++) {
      repos.abuse.checkAndRecordFailure({
        connectionId: connId,
        ownerId: 'alice',
        userId: 'alice',
        host: 'srv.example.com',
        username: 'deploy',
      });
    }
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/temporarily locked/);
  });

  it('admin user is allowed without an explicit grant (adminBypassesGrants=true)', async () => {
    const ctxAdmin = await ctxWithWorkspace({ userId: 'admin1', allowed: [connId] });
    // Stub getUserAccess to mark admin1 as admin
    setSshSubsystem({
      ...sub,
      getUserAccess: (uid) => (uid === 'admin1' ? { isAdmin: true, orgIds: [] } : { isAdmin: false, orgIds: [] }),
    });
    const r = await executeTool('SshExec', { connection_id: connId, command: 'ls' }, ctxAdmin);
    expect(r.isError).toBe(false);
  });
});

describe('engine/tools/ssh SshExec', () => {
  let db: Database.Database;
  let sub: SshSubsystem;
  let repos: ReturnType<typeof makeSubsystem>['repos'];
  let connId: string;

  beforeEach(() => {
    db = bootstrapDb();
    ({ sub, repos } = makeSubsystem({ db }));
    setSshSubsystem(sub);
    const created = repos.conn.create(
      baseConnInput({
        commandDenyPatterns: '^rm -rf /\\b\n^dd\\s',
        commandAllowPatterns: '^ls\\b\n^echo\\s\n^whoami\\b',
      }),
    );
    db.prepare(
      `UPDATE ssh_connections SET host_key_b64=?, host_key_fingerprint=?, host_key_verified_at=? WHERE id=?`,
    ).run('hostb64', 'SHA256:host', new Date().toISOString(), created.id);
    connId = created.id;
  });

  afterEach(() => setSshSubsystem(null));

  it('rejects a command blocked by built-in deny list', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'rm -rf /' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/rejected by built-in deny pattern/);
  });

  it('rejects a command outside the allow list', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'curl http://x' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/not_in_allowlist/);
  });

  it('succeeds on an allowed command and records audit success', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'whoami' },
      ctx,
    );
    expect(r.isError).toBe(false);
    const parsed = JSON.parse(r.output) as { stdout: string; exit_code: number };
    expect(parsed.exit_code).toBe(0);
    const rows = repos.audit.listForConnection(connId, 10);
    expect(rows[0].outcome).toBe('success');
    expect(rows[0].detail).toMatchObject({ exit_code: 0 });
  });

  it('records audit "failed" and increments abuse counter on auth_failed', async () => {
    const failed = makeSubsystem({
      db,
      stubs: {
        execImpl: async () => {
          throw new SshSessionError('auth_failed', 'bad key');
        },
      },
    });
    // Reuse the same db & repos so we observe state mutations
    setSshSubsystem({ ...sub, sshExec: failed.sub.sshExec });
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'whoami' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/SSH authentication failed/);
    const rows = repos.audit.listForConnection(connId, 10);
    expect(rows[0].outcome).toBe('failed');
    expect(rows[0].detail).toMatchObject({ error: 'auth_failed' });
    // Abuse counter should have ticked
    const scope = repos.abuse.getByScopeKey(`conn:${connId}`);
    expect(scope?.failure_count).toBe(1);
  });

  it('clears abuse counter on success', async () => {
    // Pre-seed a failure
    repos.abuse.checkAndRecordFailure({
      connectionId: connId,
      ownerId: 'alice',
      userId: 'alice',
      host: 'srv.example.com',
      username: 'deploy',
    });
    expect(repos.abuse.getByScopeKey(`conn:${connId}`)?.failure_count).toBe(1);

    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'whoami' },
      ctx,
    );
    expect(r.isError).toBe(false);
    expect(repos.abuse.getByScopeKey(`conn:${connId}`)).toBeNull();
  });

  it('surfaces TOFU first_observe with token to the LLM', async () => {
    setSshSubsystem({
      ...sub,
      sshExec: async (_args, hooks) => {
        const ret = await hooks.onFirstObserve({
          connectionId: connId,
          b64: 'observed-key-b64',
          fingerprint: 'SHA256:newhost',
        });
        const err = new SshSessionError('host_key_first_observe', 'new host', {
          fingerprint: 'SHA256:newhost',
          token: ret?.token ?? 'no-token',
        });
        throw err;
      },
    });
    const ctx = await ctxWithWorkspace({ allowed: [connId] });
    const r = await executeTool(
      'SshExec',
      { connection_id: connId, command: 'whoami' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/Host key first-observe/);
    // Pending key was persisted on the connection
    const conn = repos.conn.resolveConnection(connId);
    expect(conn?.hostKeyPending).toBe(true);
    expect(conn?.hostKeyPendingFingerprint).toBe('SHA256:newhost');
  });
});

describe('engine/tools/ssh SshUpload', () => {
  let db: Database.Database;
  let sub: SshSubsystem;
  let repos: ReturnType<typeof makeSubsystem>['repos'];
  let connId: string;
  let workspace: string;

  beforeEach(async () => {
    db = bootstrapDb();
    ({ sub, repos } = makeSubsystem({ db }));
    setSshSubsystem(sub);
    const created = repos.conn.create(baseConnInput({ remotePathPrefix: '/srv/agent' }));
    db.prepare(
      `UPDATE ssh_connections SET host_key_b64=?, host_key_fingerprint=?, host_key_verified_at=? WHERE id=?`,
    ).run('hostb64', 'SHA256:host', new Date().toISOString(), created.id);
    connId = created.id;
    workspace = await fsp.mkdtemp(path.join(os.tmpdir(), 'ssh-upload-'));
    await fsp.writeFile(path.join(workspace, 'hello.txt'), 'hi');
  });

  afterEach(() => setSshSubsystem(null));

  it('rejects when host key is unverified', async () => {
    // Clear the host_key_verified_at to simulate an un-verified connection
    db.prepare(`UPDATE ssh_connections SET host_key_verified_at=NULL WHERE id=?`).run(connId);
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshUpload',
      { connection_id: connId, local_path: 'hello.txt', remote_path: '/srv/agent/h.txt' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/is not user-verified/);
  });

  it('rejects when local path escapes the workspace', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshUpload',
      { connection_id: connId, local_path: '/etc/passwd', remote_path: '/srv/agent/h.txt' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/local path rejected/);
  });

  it('rejects when remote path is outside the prefix', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshUpload',
      { connection_id: connId, local_path: 'hello.txt', remote_path: '/etc/passwd' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/remote path rejected/);
  });

  it('succeeds and records audit success', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshUpload',
      { connection_id: connId, local_path: 'hello.txt', remote_path: '/srv/agent/h.txt' },
      ctx,
    );
    expect(r.isError).toBe(false);
    const body = JSON.parse(r.output) as { ok: boolean; bytes: number; remote: string };
    expect(body.ok).toBe(true);
    expect(body.remote).toBe('/srv/agent/h.txt');
    const rows = repos.audit.listForConnection(connId, 10);
    expect(rows[0].outcome).toBe('success');
  });
});

describe('engine/tools/ssh SshDownload', () => {
  let db: Database.Database;
  let sub: SshSubsystem;
  let repos: ReturnType<typeof makeSubsystem>['repos'];
  let connId: string;
  let workspace: string;

  beforeEach(async () => {
    db = bootstrapDb();
    ({ sub, repos } = makeSubsystem({
      db,
      stubs: {
        downloadImpl: async (args) => {
          // Simulate session writing the file so a subsequent validateLocalPath
          // wouldn't be needed (but for now we just resolve happily).
          return { bytes: 7, durationMs: 8, hostFingerprint: 'SHA256:fake' };
        },
      },
    }));
    setSshSubsystem(sub);
    const created = repos.conn.create(baseConnInput({ remotePathPrefix: '/srv/agent' }));
    db.prepare(
      `UPDATE ssh_connections SET host_key_b64=?, host_key_fingerprint=?, host_key_verified_at=? WHERE id=?`,
    ).run('hostb64', 'SHA256:host', new Date().toISOString(), created.id);
    connId = created.id;
    workspace = await fsp.mkdtemp(path.join(os.tmpdir(), 'ssh-download-'));
  });

  afterEach(() => setSshSubsystem(null));

  it('succeeds on a fresh local target path', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshDownload',
      { connection_id: connId, remote_path: '/srv/agent/data.bin', local_path: 'output/data.bin' },
      ctx,
    );
    expect(r.isError).toBe(false);
    const body = JSON.parse(r.output) as { ok: boolean; bytes: number };
    expect(body.ok).toBe(true);
    expect(body.bytes).toBe(7);
    const rows = repos.audit.listForConnection(connId, 10);
    expect(rows[0].outcome).toBe('success');
  });

  it('rejects when local path escapes workspace', async () => {
    const ctx = await ctxWithWorkspace({ allowed: [connId], workspace });
    const r = await executeTool(
      'SshDownload',
      { connection_id: connId, remote_path: '/srv/agent/x', local_path: '/tmp/escape.bin' },
      ctx,
    );
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/local path rejected/);
  });
});

describe('engine/tools/ssh TOOL_DEFS', () => {
  it('exposes the four SSH tools with required parameters', () => {
    expect(Object.keys(TOOL_DEFS).sort()).toEqual([
      'SshDownload', 'SshExec', 'SshListConnections', 'SshUpload',
    ]);
    expect(TOOL_DEFS.SshExec.function.parameters.required).toEqual(['connection_id', 'command']);
    expect(TOOL_DEFS.SshUpload.function.parameters.required).toEqual(['connection_id', 'local_path', 'remote_path']);
    expect(TOOL_DEFS.SshDownload.function.parameters.required).toEqual(['connection_id', 'remote_path', 'local_path']);
    expect(TOOL_DEFS.SshListConnections.function.parameters.required).toEqual([]);
  });
});

describe('engine/tools/ssh SshListConnections', () => {
  let db: Database.Database;
  let sub: SshSubsystem;
  let repos: { conn: SshConnectionRepo; audit: SshAuditRepo; abuse: SshAbuseRepo; grants: SshGrantsRepo };

  beforeEach(() => {
    db = bootstrapDb();
    ({ sub, repos } = makeSubsystem({ db }));
    setSshSubsystem(sub);
  });
  afterEach(() => {
    setSshSubsystem(null);
    db.close();
  });

  it('rejects when piece does not declare allowed_ssh_connections', async () => {
    const ctx = await ctxWithWorkspace({ allowed: undefined });
    const r = await executeTool('SshListConnections', {}, ctx);
    expect(r.isError).toBe(true);
    expect(r.output).toMatch(/does not declare allowed_ssh_connections/);
  });

  it('returns empty array when no connections exist', async () => {
    const ctx = await ctxWithWorkspace({ allowed: ['*'] });
    const r = await executeTool('SshListConnections', {}, ctx);
    expect(r.isError).toBe(false);
    expect(JSON.parse(r.output)).toEqual({ connections: [] });
  });

  it('wildcard returns owner+granted connections only (others without grant filtered)', async () => {
    const a = repos.conn.create(baseConnInput({ ownerId: 'alice', label: 'alice-srv' }));
    // bob-owned: alice has no grant → access denied → filtered out.
    repos.conn.create(baseConnInput({ ownerId: 'bob', label: 'bob-srv' }));
    const ctx = await ctxWithWorkspace({ allowed: ['*'], userId: 'alice' });
    const r = await executeTool('SshListConnections', {}, ctx);
    expect(r.isError).toBe(false);
    const parsed = JSON.parse(r.output);
    expect(parsed.connections.map((c: any) => c.label)).toEqual(['alice-srv']);
    expect(parsed.connections[0]).toMatchObject({
      id: a.id,
      label: 'alice-srv',
      host: 'srv.example.com',
      port: 22,
      username: 'deploy',
      host_key_verified: false,
      host_key_pending: false,
    });
  });

  it('explicit UUID list filters out non-matching connections', async () => {
    const a = repos.conn.create(baseConnInput({ ownerId: 'alice', label: 'a' }));
    const c = repos.conn.create(baseConnInput({ ownerId: 'alice', label: 'c' }));
    const ctx = await ctxWithWorkspace({ allowed: [a.id], userId: 'alice' });
    const r = await executeTool('SshListConnections', {}, ctx);
    const parsed = JSON.parse(r.output);
    expect(parsed.connections.map((x: any) => x.id)).toEqual([a.id]);
    expect(parsed.connections.map((x: any) => x.id)).not.toContain(c.id);
  });

  it('writes an audit row with action ssh.list_connections', async () => {
    repos.conn.create(baseConnInput({ ownerId: 'alice', label: 'x' }));
    const ctx = await ctxWithWorkspace({ allowed: ['*'], userId: 'alice' });
    await executeTool('SshListConnections', {}, ctx);
    const row = db
      .prepare(
        "SELECT action, outcome FROM ssh_audit_log WHERE action = 'ssh.list_connections' ORDER BY id DESC LIMIT 1",
      )
      .get() as { action: string; outcome: string } | undefined;
    expect(row).toBeDefined();
    expect(row!.outcome).toBe('success');
  });
});
