import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations } from '../db/migrate.js';
import { createAuditRepo } from './audit-repo.js';
import { reconcileStaleSshAudit } from './recovery.js';

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

function bootstrapDb(): Database.Database {
  process.env.MCP_ENCRYPTION_KEY = validKey;
  const db = new Database(':memory:');
  db.pragma('foreign_keys = ON');
  db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY);`);
  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);
  return db;
}

describe('ssh/audit-repo', () => {
  let db: Database.Database;
  beforeEach(() => {
    db = bootstrapDb();
  });
  afterEach(() => {
    db.close();
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('begin inserts a pending row and returns its id', () => {
    const repo = createAuditRepo(db);
    const id = repo.begin({
      action: 'ssh.exec',
      connectionId: 'conn-1',
      ownerId: 'alice',
      actingUserId: 'alice',
      detail: { command: 'git pull' },
    });
    expect(id).toBeGreaterThan(0);
    const row = repo.getById(id);
    expect(row?.outcome).toBe('pending');
    expect(row?.action).toBe('ssh.exec');
    expect(row?.detail).toEqual({ command: 'git pull' });
    expect(row?.completedAt).toBeNull();
  });

  it('complete transitions pending to terminal outcome and merges detail', () => {
    const repo = createAuditRepo(db);
    const id = repo.begin({
      action: 'ssh.exec',
      connectionId: 'c',
      detail: { command: 'ls' },
    });
    const ok = repo.complete(id, 'success', { exit_code: 0, stdout_bytes: 100 });
    expect(ok).toBe(true);
    const row = repo.getById(id);
    expect(row?.outcome).toBe('success');
    expect(row?.completedAt).not.toBeNull();
    expect(row?.detail).toEqual({ command: 'ls', exit_code: 0, stdout_bytes: 100 });
  });

  it('complete is idempotent (returns false on already-completed row)', () => {
    const repo = createAuditRepo(db);
    const id = repo.begin({ action: 'ssh.exec' });
    expect(repo.complete(id, 'success')).toBe(true);
    expect(repo.complete(id, 'failed')).toBe(false);
    // Outcome should remain 'success'
    expect(repo.getById(id)?.outcome).toBe('success');
  });

  it('beginAndComplete writes a terminal row directly', () => {
    const repo = createAuditRepo(db);
    const id = repo.beginAndComplete(
      { action: 'ssh.connection.disable', connectionId: 'c', reason: 'admin disabled' },
      'success',
    );
    const row = repo.getById(id);
    expect(row?.outcome).toBe('success');
    expect(row?.reason).toBe('admin disabled');
  });

  it('listForConnection returns rows newest first, limited', () => {
    const repo = createAuditRepo(db);
    repo.begin({ action: 'ssh.exec', connectionId: 'c1', startedAt: '2026-05-01T10:00:00Z' });
    repo.begin({ action: 'ssh.exec', connectionId: 'c1', startedAt: '2026-05-01T11:00:00Z' });
    repo.begin({ action: 'ssh.exec', connectionId: 'c2', startedAt: '2026-05-01T12:00:00Z' });
    const rows = repo.listForConnection('c1');
    expect(rows.length).toBe(2);
    expect(rows[0].startedAt).toBe('2026-05-01T11:00:00Z');
    expect(rows[1].startedAt).toBe('2026-05-01T10:00:00Z');
  });

  it('listForOwner returns rows for that owner', () => {
    const repo = createAuditRepo(db);
    repo.begin({ action: 'ssh.exec', ownerId: 'alice' });
    repo.begin({ action: 'ssh.exec', ownerId: 'bob' });
    repo.begin({ action: 'ssh.exec', ownerId: 'alice' });
    expect(repo.listForOwner('alice')).toHaveLength(2);
    expect(repo.listForOwner('bob')).toHaveLength(1);
  });

  it('listPending only returns pending rows', () => {
    const repo = createAuditRepo(db);
    const a = repo.begin({ action: 'ssh.exec' });
    const b = repo.begin({ action: 'ssh.upload' });
    repo.complete(a, 'success');
    const pending = repo.listPending();
    expect(pending).toHaveLength(1);
    expect(pending[0].id).toBe(b);
  });
});

describe('ssh/recovery', () => {
  let db: Database.Database;
  beforeEach(() => {
    db = bootstrapDb();
  });
  afterEach(() => {
    db.close();
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('reconcileStaleSshAudit marks all pending rows aborted', () => {
    const repo = createAuditRepo(db);
    const a = repo.begin({ action: 'ssh.exec' });
    const b = repo.begin({ action: 'ssh.upload' });
    const c = repo.begin({ action: 'ssh.download' });
    repo.complete(b, 'success'); // already done

    const result = reconcileStaleSshAudit(db);
    expect(result.reconciledCount).toBe(2);
    expect(result.ids.sort()).toEqual([a, c].sort());
    expect(repo.getById(a)?.outcome).toBe('aborted');
    expect(repo.getById(c)?.outcome).toBe('aborted');
    expect(repo.getById(b)?.outcome).toBe('success');
    expect(repo.getById(a)?.detail).toMatchObject({ stale_reason: 'orchestrator_restart' });
  });

  it('reconcileStaleSshAudit is a no-op when no pending rows', () => {
    const result = reconcileStaleSshAudit(db);
    expect(result.reconciledCount).toBe(0);
    expect(result.ids).toEqual([]);
  });
});
