/**
 * Tests for Azure Key Vault secret resolution.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { resolveKeyVaultSecrets, LYSNR_SECRETS } from '../keyvault.js';
import type { SecretMapping } from '../keyvault.js';

// Mock Azure SDK dynamic imports to prevent test timeouts
const { mockGetSecret } = vi.hoisted(() => {
  const mockGetSecret = vi.fn();
  return { mockGetSecret };
});

vi.mock('@azure/identity', () => ({
  DefaultAzureCredential: vi.fn(),
}));

vi.mock('@azure/keyvault-secrets', () => ({
  SecretClient: vi.fn().mockImplementation(() => ({
    getSecret: mockGetSecret,
  })),
}));

describe('resolveKeyVaultSecrets', () => {
  const originalEnv = { ...process.env };

  beforeEach(() => {
    // Clean env vars used in tests
    delete process.env.AZURE_KEYVAULT_URL;
    delete process.env.TEST_SECRET_A;
    delete process.env.TEST_SECRET_B;
    mockGetSecret.mockReset();
  });

  afterEach(() => {
    process.env = { ...originalEnv };
    vi.clearAllMocks();
  });

  it('skips entirely when AZURE_KEYVAULT_URL is not set', async () => {
    const secrets: SecretMapping[] = [{ kvName: 'test-secret', envVar: 'TEST_SECRET_A' }];

    await resolveKeyVaultSecrets(secrets);

    // Should not have set the env var (no KV to resolve from)
    expect(process.env.TEST_SECRET_A).toBeUndefined();
  });

  it('skips secrets that already exist in env', async () => {
    process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net';
    process.env.TEST_SECRET_A = 'already-set';

    const secrets: SecretMapping[] = [{ kvName: 'test-secret-a', envVar: 'TEST_SECRET_A' }];

    // Should not attempt KV call since all secrets are present
    await resolveKeyVaultSecrets(secrets);

    expect(process.env.TEST_SECRET_A).toBe('already-set');
  });

  it('accepts custom vaultUrl via opts', async () => {
    mockGetSecret.mockResolvedValue({ value: 'resolved-value' });

    const secrets: SecretMapping[] = [{ kvName: 'test-secret', envVar: 'TEST_SECRET_A' }];

    await resolveKeyVaultSecrets(secrets, { vaultUrl: 'https://kv-test.vault.azure.net' });

    expect(process.env.TEST_SECRET_A).toBe('resolved-value');
    expect(mockGetSecret).toHaveBeenCalledWith('test-secret');
  });

  it('handles empty secrets array', async () => {
    process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net';

    await expect(resolveKeyVaultSecrets([])).resolves.not.toThrow();
  });

  it('resolves multiple missing secrets from Key Vault', async () => {
    process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net';
    mockGetSecret
      .mockResolvedValueOnce({ value: 'secret-a-val' })
      .mockResolvedValueOnce({ value: 'secret-b-val' });

    const secrets: SecretMapping[] = [
      { kvName: 'secret-a', envVar: 'TEST_SECRET_A' },
      { kvName: 'secret-b', envVar: 'TEST_SECRET_B' },
    ];

    await resolveKeyVaultSecrets(secrets);

    expect(process.env.TEST_SECRET_A).toBe('secret-a-val');
    expect(process.env.TEST_SECRET_B).toBe('secret-b-val');
  });

  it('warns but does not throw when getSecret fails', async () => {
    process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net';
    mockGetSecret.mockRejectedValue(new Error('SecretNotFound'));

    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const secrets: SecretMapping[] = [{ kvName: 'bad-secret', envVar: 'TEST_SECRET_A' }];

    await resolveKeyVaultSecrets(secrets);

    expect(process.env.TEST_SECRET_A).toBeUndefined();
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('1/1 secrets failed'));

    warnSpy.mockRestore();
  });

  it('filters to only missing secrets — skips already-present', async () => {
    process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net';
    process.env.TEST_SECRET_A = 'present';
    mockGetSecret.mockResolvedValue({ value: 'from-kv' });

    const secrets: SecretMapping[] = [
      { kvName: 'secret-a', envVar: 'TEST_SECRET_A' },
      { kvName: 'secret-b', envVar: 'TEST_SECRET_B' },
    ];

    await resolveKeyVaultSecrets(secrets);

    // TEST_SECRET_A should remain unchanged (already present)
    expect(process.env.TEST_SECRET_A).toBe('present');
    // TEST_SECRET_B should be resolved from KV
    expect(process.env.TEST_SECRET_B).toBe('from-kv');
    // getSecret should only be called for the missing secret
    expect(mockGetSecret).toHaveBeenCalledTimes(1);
    expect(mockGetSecret).toHaveBeenCalledWith('secret-b');
  });
});

describe('LYSNR_SECRETS', () => {
  it('exports all expected secret mappings', () => {
    const expectedKeys = [
      'COSMOS_KEY',
      'COSMOS_ENDPOINT',
      'JWT_SECRET',
      'STRIPE_SECRET_KEY',
      'STRIPE_WEBHOOK_SECRET',
      'BILLING_INTERNAL_KEY',
      'AZURE_BLOB_CONNECTION_STRING',
      'AZURE_BLOB_ACCOUNT_KEY',
      'GEMINI_API_KEY',
      'SEED_SECRET',
      'AZURE_SPEECH_KEY',
      'AZURE_OPENAI_KEY',
      'AZURE_OPENAI_ENDPOINT',
    ];

    for (const key of expectedKeys) {
      expect(LYSNR_SECRETS).toHaveProperty(key);
    }
  });

  it('each mapping has kvName and envVar', () => {
    for (const [key, mapping] of Object.entries(LYSNR_SECRETS)) {
      expect(mapping.kvName).toBeDefined();
      expect(typeof mapping.kvName).toBe('string');
      expect(mapping.kvName.length).toBeGreaterThan(0);

      expect(mapping.envVar).toBeDefined();
      expect(typeof mapping.envVar).toBe('string');
      expect(mapping.envVar).toBe(key);
    }
  });

  it('kvNames follow lysnr-* naming convention', () => {
    for (const mapping of Object.values(LYSNR_SECRETS)) {
      expect(mapping.kvName).toMatch(/^lysnr-/);
    }
  });

  it('envVars are UPPER_SNAKE_CASE', () => {
    for (const mapping of Object.values(LYSNR_SECRETS)) {
      expect(mapping.envVar).toMatch(/^[A-Z][A-Z0-9_]*$/);
    }
  });
});
