import { describe, it, expect, vi } from 'vitest';
import { runSecretBackendOpenbaoWizard } from '../../src/commands/create-secretbackend-wizard.js';
import type { ApiClient } from '../../src/api-client.js';
import type { ConfigSetupPrompt } from '../../src/commands/config-setup.js';

function mockClient(handlers: Record<string, (body?: unknown) => unknown>): ApiClient {
  const call = (method: 'GET' | 'POST' | 'PUT' | 'DELETE') => async (path: string, body?: unknown) => {
    const handler = handlers[`${method} ${path}`] ?? handlers[path];
    if (handler === undefined) throw new Error(`unmocked ${method} ${path}`);
    return handler(body);
  };
  return {
    get: call('GET'),
    post: call('POST'),
    put: call('PUT'),
    delete: call('DELETE'),
  } as unknown as ApiClient;
}

function vaultFetch(responses: Array<{ match: RegExp; status: number; body?: unknown }>): ReturnType<typeof vi.fn> {
  return vi.fn(async (url: string | URL, init?: RequestInit) => {
    const key = `${init?.method ?? 'GET'} ${String(url)}`;
    const match = responses.find((r) => r.match.test(key) || r.match.test(String(url)));
    if (!match) throw new Error(`unexpected vault fetch: ${key}`);
    const body = match.body !== undefined ? JSON.stringify(match.body) : '';
    return new Response(body, { status: match.status });
  });
}

function scriptedPrompt(answers: {
  input?: Record<string, string>;
  password?: Record<string, string>;
  confirm?: Record<string, boolean>;
}): ConfigSetupPrompt {
  return {
    async input(message, def) {
      return answers.input?.[message] ?? def ?? '';
    },
    async password(message) {
      return answers.password?.[message] ?? '';
    },
    async confirm(message, def) {
      return answers.confirm?.[message] ?? def ?? true;
    },
    select: vi.fn(),
  };
}

describe('runSecretBackendOpenbaoWizard', () => {
  it('walks through provisioning and creates Secret + SecretBackend + triggers initial rotate', async () => {
    const logs: string[] = [];
    const log = (...args: unknown[]) => logs.push(args.map(String).join(' '));

    const vaultResponses = [
      { match: /GET .*\/v1\/sys\/health$/, status: 200, body: { initialized: true, sealed: false, standby: false, version: '2.5.2' } },
      { match: /GET .*\/v1\/sys\/mounts$/, status: 200, body: { 'secret/': { type: 'kv', options: { version: '2' } } } },
      { match: /PUT .*\/v1\/sys\/policies\/acl\/app-mcpd$/, status: 200 },
      { match: /POST .*\/v1\/auth\/token\/roles\/app-mcpd-role$/, status: 200 },
      { match: /POST .*\/v1\/auth\/token\/create\/app-mcpd-role$/, status: 200, body: { auth: { client_token: 'hvs.AAA', accessor: 'acc-first', lease_duration: 2592000, renewable: true } } },
      // smoke test: write / read / delete
      { match: /POST .*\/v1\/secret\/data\/mcpd\/\.__mcpctl_wizard_smoke__$/, status: 200 },
      { match: /GET .*\/v1\/secret\/data\/mcpd\/\.__mcpctl_wizard_smoke__$/, status: 200, body: { data: { data: { marker: 'mcpctl-smoke' } } } },
      { match: /DELETE .*\/v1\/secret\/metadata\/mcpd\/\.__mcpctl_wizard_smoke__$/, status: 200 },
    ];
    const fetchFn = vaultFetch(vaultResponses);

    const created: Record<string, unknown> = {};
    const client = mockClient({
      'POST /api/v1/secrets': (body) => { created.secret = body; return { id: 'sec-new', name: (body as { name: string }).name }; },
      'POST /api/v1/secretbackends': (body) => { created.backend = body; return { id: 'backend-new', name: (body as { name: string }).name }; },
      'POST /api/v1/secretbackends/backend-new/rotate': () => ({ ok: true, tokenMeta: { generatedAt: 'now' } }),
      'POST /api/v1/secretbackends/backend-new/default': () => ({ id: 'backend-new' }),
      'GET /api/v1/secretbackends': () => [{ name: 'default', isDefault: true }],
      'POST /api/v1/secrets/migrate': () => ({ dryRun: true, candidates: [{ id: 's1', name: 'grafana-creds' }, { id: 's2', name: 'unifi-creds' }] }),
    });

    const prompt = scriptedPrompt({
      input: {
        'OpenBao URL': 'http://bao.example:8200',
        'KV v2 mount': 'secret',
        'Path prefix under mount': 'mcpd',
        'Policy name': 'app-mcpd',
        'Token role name': 'app-mcpd-role',
      },
      password: {
        'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 'root.admin.token',
      },
      confirm: {
        "Promote 'bao' to default backend?": true,
      },
    });

    await runSecretBackendOpenbaoWizard(
      { name: 'bao' },
      { client, log, prompt, fetch: fetchFn as unknown as typeof fetch },
    );

    // Admin token used for the provisioning calls (first 5 vault requests)
    const firstCallInit = fetchFn.mock.calls[0]![1] as RequestInit;
    expect((firstCallInit.headers as Record<string, string>)['X-Vault-Token']).toBe('root.admin.token');

    // Secret was created with the minted token value (hvs.AAA), not the setup token
    expect(created.secret).toMatchObject({ name: 'bao-creds', data: { token: 'hvs.AAA' } });

    // SecretBackend created with rotation config
    expect(created.backend).toMatchObject({
      name: 'bao',
      type: 'openbao',
      config: expect.objectContaining({
        url: 'http://bao.example:8200',
        auth: 'token',
        tokenSecretRef: { name: 'bao-creds', key: 'token' },
        rotation: expect.objectContaining({ enabled: true, tokenRole: 'app-mcpd-role' }),
      }),
    });

    // Migration hint mentions both candidate count + the concrete command
    const fullLog = logs.join('\n');
    expect(fullLog).toContain("You have 2 secret(s) on 'default'");
    expect(fullLog).toContain('mcpctl --direct migrate secrets --from default --to bao');

    // Setup token never appears in the log (critical)
    expect(fullLog).not.toContain('root.admin.token');
  });

  it('rejects when setup token is empty', async () => {
    const prompt = scriptedPrompt({
      input: { 'OpenBao URL': 'http://x' },
      password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': '' },
    });
    await expect(runSecretBackendOpenbaoWizard(
      { name: 'bao' },
      { client: mockClient({}), log: () => {}, prompt, fetch: vi.fn() as unknown as typeof fetch },
    )).rejects.toThrow(/setup token is required/);
  });

  it('rejects when vault is sealed', async () => {
    const fetchFn = vaultFetch([
      { match: /\/sys\/health$/, status: 200, body: { initialized: true, sealed: true, standby: false, version: '2.5.2' } },
    ]);
    const prompt = scriptedPrompt({
      input: { 'OpenBao URL': 'http://x' },
      password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 't' },
    });
    await expect(runSecretBackendOpenbaoWizard(
      { name: 'bao' },
      { client: mockClient({}), log: () => {}, prompt, fetch: fetchFn as unknown as typeof fetch },
    )).rejects.toThrow(/not ready/);
  });
});
