/**
 * Unit tests for VaultClient.
 * Mocks the node-vault library to avoid real Vault connections.
 */

import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { VaultClient, createVaultClientFromEnv } from '../../../src/vault/VaultClient.js';
import { CredentialError } from '../../../src/utils/errors.js';

// ─── Mock node-vault ────────────────────────────────────────────────────────

const mockWrite = jest.fn<() => Promise<unknown>>();
const mockRead = jest.fn<() => Promise<unknown>>();
const mockDelete = jest.fn<() => Promise<unknown>>();

jest.mock('node-vault', () => {
  return jest.fn(() => ({
    write: mockWrite,
    read: mockRead,
    delete: mockDelete,
  }));
});

// ─── Helpers ────────────────────────────────────────────────────────────────

const AGENT_ID = 'agent-uuid-1234';
const CRED_ID = 'cred-uuid-5678';
const PLAIN_SECRET = 'super-secret-value';

function makeClient(): VaultClient {
  return new VaultClient('http://127.0.0.1:8200', 'test-token', 'secret');
}

// ─── Tests ──────────────────────────────────────────────────────────────────

describe('VaultClient', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  // ── writeSecret ────────────────────────────────────────────────────────────

  describe('writeSecret', () => {
    it('writes the secret to the correct KV v2 path and returns the path', async () => {
      mockWrite.mockResolvedValue({});
      const client = makeClient();
      const path = await client.writeSecret(AGENT_ID, CRED_ID, PLAIN_SECRET);

      expect(mockWrite).toHaveBeenCalledWith(
        `secret/data/agentidp/agents/${AGENT_ID}/credentials/${CRED_ID}`,
        { data: { clientSecret: PLAIN_SECRET } },
      );
      expect(path).toBe(`secret/data/agentidp/agents/${AGENT_ID}/credentials/${CRED_ID}`);
    });

    it('throws CredentialError when Vault write fails', async () => {
      mockWrite.mockRejectedValue(new Error('connection refused'));
      const client = makeClient();

      await expect(client.writeSecret(AGENT_ID, CRED_ID, PLAIN_SECRET))
        .rejects.toThrow(CredentialError);
    });

    it('CredentialError on write failure has code VAULT_WRITE_ERROR', async () => {
      mockWrite.mockRejectedValue(new Error('forbidden'));
      const client = makeClient();

      await expect(client.writeSecret(AGENT_ID, CRED_ID, PLAIN_SECRET))
        .rejects.toMatchObject({ code: 'VAULT_WRITE_ERROR' });
    });
  });

  // ── readSecret ─────────────────────────────────────────────────────────────

  describe('readSecret', () => {
    it('reads and returns the stored secret', async () => {
      mockRead.mockResolvedValue({
        data: { data: { clientSecret: PLAIN_SECRET }, metadata: {} },
      });
      const client = makeClient();
      const secret = await client.readSecret(AGENT_ID, CRED_ID);

      expect(mockRead).toHaveBeenCalledWith(
        `secret/data/agentidp/agents/${AGENT_ID}/credentials/${CRED_ID}`,
      );
      expect(secret).toBe(PLAIN_SECRET);
    });

    it('throws CredentialError when secret field is missing', async () => {
      mockRead.mockResolvedValue({ data: { data: {}, metadata: {} } });
      const client = makeClient();

      await expect(client.readSecret(AGENT_ID, CRED_ID))
        .rejects.toMatchObject({ code: 'VAULT_SECRET_MISSING' });
    });

    it('throws CredentialError when Vault read fails', async () => {
      mockRead.mockRejectedValue(new Error('404 not found'));
      const client = makeClient();

      await expect(client.readSecret(AGENT_ID, CRED_ID))
        .rejects.toMatchObject({ code: 'VAULT_READ_ERROR' });
    });
  });

  // ── verifySecret ───────────────────────────────────────────────────────────

  describe('verifySecret', () => {
    it('returns true when candidate matches stored secret', async () => {
      mockRead.mockResolvedValue({
        data: { data: { clientSecret: PLAIN_SECRET }, metadata: {} },
      });
      const client = makeClient();
      const result = await client.verifySecret(AGENT_ID, CRED_ID, PLAIN_SECRET);
      expect(result).toBe(true);
    });

    it('returns false when candidate does not match stored secret', async () => {
      mockRead.mockResolvedValue({
        data: { data: { clientSecret: PLAIN_SECRET }, metadata: {} },
      });
      const client = makeClient();
      const result = await client.verifySecret(AGENT_ID, CRED_ID, 'wrong-secret');
      expect(result).toBe(false);
    });

    it('returns false when Vault read fails (does not throw)', async () => {
      mockRead.mockRejectedValue(new Error('vault sealed'));
      const client = makeClient();
      const result = await client.verifySecret(AGENT_ID, CRED_ID, PLAIN_SECRET);
      expect(result).toBe(false);
    });

    it('returns false when lengths differ (constant-time)', async () => {
      mockRead.mockResolvedValue({
        data: { data: { clientSecret: PLAIN_SECRET }, metadata: {} },
      });
      const client = makeClient();
      const result = await client.verifySecret(AGENT_ID, CRED_ID, 'short');
      expect(result).toBe(false);
    });
  });

  // ── deleteSecret ───────────────────────────────────────────────────────────

  describe('deleteSecret', () => {
    it('calls delete on the metadata path', async () => {
      mockDelete.mockResolvedValue({});
      const client = makeClient();
      await client.deleteSecret(AGENT_ID, CRED_ID);

      expect(mockDelete).toHaveBeenCalledWith(
        `secret/metadata/agentidp/agents/${AGENT_ID}/credentials/${CRED_ID}`,
      );
    });

    it('throws CredentialError when Vault delete fails', async () => {
      mockDelete.mockRejectedValue(new Error('permission denied'));
      const client = makeClient();

      await expect(client.deleteSecret(AGENT_ID, CRED_ID))
        .rejects.toMatchObject({ code: 'VAULT_DELETE_ERROR' });
    });
  });
});

// ─── createVaultClientFromEnv ─────────────────────────────────────────────────

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

  beforeEach(() => {
    process.env = { ...originalEnv };
  });

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

  it('returns null when VAULT_ADDR is not set', () => {
    delete process.env['VAULT_ADDR'];
    delete process.env['VAULT_TOKEN'];
    expect(createVaultClientFromEnv()).toBeNull();
  });

  it('returns null when VAULT_TOKEN is not set', () => {
    process.env['VAULT_ADDR'] = 'http://127.0.0.1:8200';
    delete process.env['VAULT_TOKEN'];
    expect(createVaultClientFromEnv()).toBeNull();
  });

  it('returns a VaultClient when both VAULT_ADDR and VAULT_TOKEN are set', () => {
    process.env['VAULT_ADDR'] = 'http://127.0.0.1:8200';
    process.env['VAULT_TOKEN'] = 'test-token';
    const client = createVaultClientFromEnv();
    expect(client).toBeInstanceOf(VaultClient);
  });

  it('uses default mount "secret" when VAULT_MOUNT is not set', () => {
    process.env['VAULT_ADDR'] = 'http://127.0.0.1:8200';
    process.env['VAULT_TOKEN'] = 'test-token';
    delete process.env['VAULT_MOUNT'];
    // VaultClient instance created — mount is internal, just verify no throw
    expect(() => createVaultClientFromEnv()).not.toThrow();
  });
});
