import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { encrypt, decrypt, loadKeyFromEnv, isKeyConfigured, initMcpKeyFromFile } from './crypto.js';
import { statSync, rmSync, writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';

describe('mcp/crypto', () => {
  const validKey = 'a'.repeat(64); // 32 bytes hex

  beforeEach(() => {
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('isKeyConfigured returns false when env var is missing', () => {
    expect(isKeyConfigured()).toBe(false);
  });

  it('isKeyConfigured returns true when env var is a valid 64-char hex', () => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    expect(isKeyConfigured()).toBe(true);
  });

  it('isKeyConfigured returns false when env var is wrong length', () => {
    process.env.MCP_ENCRYPTION_KEY = 'abc';
    expect(isKeyConfigured()).toBe(false);
  });

  it('encrypt + decrypt roundtrips a string', () => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    const key = loadKeyFromEnv();
    const cipher = encrypt('hello world', key);
    expect(cipher).toBeInstanceOf(Buffer);
    const plain = decrypt(cipher, key);
    expect(plain).toBe('hello world');
  });

  it('produces different ciphertexts for the same plaintext (random IV)', () => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    const key = loadKeyFromEnv();
    const a = encrypt('same', key);
    const b = encrypt('same', key);
    expect(Buffer.compare(a, b)).not.toBe(0);
  });

  it('decrypt throws on tampered ciphertext (GCM auth tag)', () => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    const key = loadKeyFromEnv();
    const cipher = encrypt('secret', key);
    cipher[cipher.length - 1] ^= 0xff; // flip last byte
    expect(() => decrypt(cipher, key)).toThrow();
  });

  it('loadKeyFromEnv throws when key is unset', () => {
    expect(() => loadKeyFromEnv()).toThrow(/MCP_ENCRYPTION_KEY/);
  });
});

describe('initMcpKeyFromFile', () => {
  let tmpDir: string;
  let keyPath: string;

  beforeEach(() => {
    delete process.env.MCP_ENCRYPTION_KEY;
    tmpDir = join(tmpdir(), `mcp-crypto-test-${randomUUID()}`);
    keyPath = join(tmpDir, 'secrets', 'mcp.key');
  });

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

  it('generates a new 32-byte key file when file does not exist', () => {
    initMcpKeyFromFile(keyPath);
    const stat = statSync(keyPath);
    expect(stat.size).toBe(32);
    // mode check: 0o100600 = regular file with 0600 permissions
    expect(stat.mode & 0o777).toBe(0o600);
  });

  it('sets MCP_ENCRYPTION_KEY env var (64-char hex) after generating', () => {
    initMcpKeyFromFile(keyPath);
    const raw = process.env.MCP_ENCRYPTION_KEY;
    expect(raw).toMatch(/^[0-9a-f]{64}$/);
  });

  it('reads an existing key file and sets env var', () => {
    // Pre-create the file with a known 32-byte key
    mkdirSync(join(tmpDir, 'secrets'), { recursive: true });
    const knownKey = Buffer.alloc(32, 0xab);
    writeFileSync(keyPath, knownKey, { mode: 0o600 });

    initMcpKeyFromFile(keyPath);

    expect(process.env.MCP_ENCRYPTION_KEY).toBe('ab'.repeat(32));
  });

  it('calling twice uses the same key (idempotent)', () => {
    initMcpKeyFromFile(keyPath);
    const first = process.env.MCP_ENCRYPTION_KEY;
    delete process.env.MCP_ENCRYPTION_KEY;
    initMcpKeyFromFile(keyPath);
    const second = process.env.MCP_ENCRYPTION_KEY;
    expect(first).toBe(second);
  });

  it('throws if existing file is wrong size', () => {
    mkdirSync(join(tmpDir, 'secrets'), { recursive: true });
    writeFileSync(keyPath, Buffer.alloc(16, 0xff), { mode: 0o600 });
    expect(() => initMcpKeyFromFile(keyPath)).toThrow(/not 32 bytes/);
  });
});
