import { describe, it, expect, vi } from 'vitest';
import {
  buildAppMcpdPolicyHcl,
  verifyHealth,
  ensureKvV2,
  writePolicy,
  ensureTokenRole,
  mintRoleToken,
  revokeAccessor,
  lookupSelf,
  testWriteReadDelete,
} from '../src/vault/index.js';

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

describe('buildAppMcpdPolicyHcl', () => {
  it('emits stable HCL for the documented default', () => {
    const hcl = buildAppMcpdPolicyHcl({ mount: 'secret', pathPrefix: 'mcpd', tokenRole: 'app-mcpd-role' });
    expect(hcl).toContain('path "secret/data/mcpd/*"');
    expect(hcl).toContain('path "secret/metadata/mcpd/*"');
    expect(hcl).toContain('path "auth/token/create/app-mcpd-role"');
    expect(hcl).toContain('path "auth/token/revoke-accessor"');
    expect(hcl).toContain('capabilities = ["read"]');
  });

  it('normalises leading/trailing slashes in pathPrefix', () => {
    const hcl = buildAppMcpdPolicyHcl({ mount: 'secret', pathPrefix: '/mcpd/', tokenRole: 'r' });
    expect(hcl).not.toContain('//');
    expect(hcl).toContain('path "secret/data/mcpd/*"');
  });
});

describe('verifyHealth', () => {
  it('returns normalised shape for a healthy unsealed vault', async () => {
    const fetchFn = mockFetch([{ match: /\/v1\/sys\/health$/, status: 200, body: { initialized: true, sealed: false, standby: false, version: '2.5.2' } }]);
    const h = await verifyHealth('http://bao.example:8200', 'root', { fetch: fetchFn as unknown as typeof fetch });
    expect(h).toEqual({ initialized: true, sealed: false, standby: false, version: '2.5.2' });
  });

  it('throws on non-standard 5xx', async () => {
    const fetchFn = vi.fn(async () => new Response('boom', { status: 502 }));
    await expect(verifyHealth('http://x', 'root', { fetch: fetchFn as unknown as typeof fetch })).rejects.toThrow(/HTTP 502/);
  });
});

describe('ensureKvV2', () => {
  it('returns false when mount already exists as kv v2', async () => {
    const fetchFn = mockFetch([
      { match: /GET .*\/v1\/sys\/mounts$/, status: 200, body: { 'secret/': { type: 'kv', options: { version: '2' } } } },
    ]);
    const created = await ensureKvV2('http://x', 'root', 'secret', { fetch: fetchFn as unknown as typeof fetch });
    expect(created).toBe(false);
  });

  it('mounts KV v2 when mount is missing', async () => {
    const fetchFn = mockFetch([
      { match: /GET .*\/v1\/sys\/mounts$/, status: 200, body: {} },
      { match: /POST .*\/v1\/sys\/mounts\/secret$/, status: 200 },
    ]);
    const created = await ensureKvV2('http://x', 'root', 'secret', { fetch: fetchFn as unknown as typeof fetch });
    expect(created).toBe(true);
  });

  it('rejects when mount exists but is kv v1', async () => {
    const fetchFn = mockFetch([
      { match: /GET .*\/v1\/sys\/mounts$/, status: 200, body: { 'secret/': { type: 'kv', options: { version: '1' } } } },
    ]);
    await expect(ensureKvV2('http://x', 'root', 'secret', { fetch: fetchFn as unknown as typeof fetch }))
      .rejects.toThrow(/not v2/);
  });
});

describe('writePolicy', () => {
  it('PUTs the HCL to /v1/sys/policies/acl/<name>', async () => {
    const fetchFn = mockFetch([{ match: /PUT .*\/v1\/sys\/policies\/acl\/app-mcpd$/, status: 200 }]);
    await writePolicy('http://x', 'root', 'app-mcpd', 'path "x" {}', { fetch: fetchFn as unknown as typeof fetch });
    const [, init] = fetchFn.mock.calls[0] as [string, RequestInit];
    expect(init.method).toBe('PUT');
    const body = JSON.parse(init.body as string) as { policy: string };
    expect(body.policy).toContain('path "x"');
  });
});

describe('ensureTokenRole', () => {
  it('POSTs the role config with period + renewable', async () => {
    const fetchFn = mockFetch([{ match: /POST .*\/v1\/auth\/token\/roles\/app-mcpd-role$/, status: 200 }]);
    await ensureTokenRole('http://x', 'root', 'app-mcpd-role', {
      allowedPolicies: ['app-mcpd'],
      period: 720 * 3600,
      renewable: true,
    }, { fetch: fetchFn as unknown as typeof fetch });
    const [, init] = fetchFn.mock.calls[0] as [string, RequestInit];
    const sent = JSON.parse(init.body as string) as Record<string, unknown>;
    expect(sent.allowed_policies).toEqual(['app-mcpd']);
    expect(sent.period).toBe(720 * 3600);
    expect(sent.renewable).toBe(true);
    expect(sent.orphan).toBe(false);
  });
});

describe('mintRoleToken', () => {
  it('parses the auth block into a MintedToken', async () => {
    const fetchFn = mockFetch([{
      match: /POST .*\/v1\/auth\/token\/create\/app-mcpd-role$/,
      status: 200,
      body: { auth: { client_token: 'hvs.CAE.xyz', accessor: 'acc-1', lease_duration: 2592000, renewable: true, policies: ['app-mcpd', 'default'] } },
    }]);
    const m = await mintRoleToken('http://x', 'caller', 'app-mcpd-role', { fetch: fetchFn as unknown as typeof fetch });
    expect(m.clientToken).toBe('hvs.CAE.xyz');
    expect(m.accessor).toBe('acc-1');
    expect(m.leaseDuration).toBe(2592000);
    expect(m.renewable).toBe(true);
    expect(m.policies).toEqual(['app-mcpd', 'default']);
  });

  it('throws when the response is missing auth.client_token', async () => {
    const fetchFn = mockFetch([{ match: /create\/r$/, status: 200, body: { auth: { accessor: 'acc' } } }]);
    await expect(mintRoleToken('http://x', 'caller', 'r', { fetch: fetchFn as unknown as typeof fetch }))
      .rejects.toThrow(/missing auth.client_token/);
  });
});

describe('revokeAccessor', () => {
  it('swallows 400 (already revoked/unknown)', async () => {
    const fetchFn = vi.fn(async () => new Response('{}', { status: 400 }));
    await expect(revokeAccessor('http://x', 'caller', 'acc', { fetch: fetchFn as unknown as typeof fetch }))
      .resolves.toBeUndefined();
  });
});

describe('lookupSelf', () => {
  it('extracts accessor + ttl from data block', async () => {
    const fetchFn = mockFetch([{
      match: /lookup-self$/,
      status: 200,
      body: { data: { accessor: 'acc-7', ttl: 2591000, policies: ['app-mcpd'] } },
    }]);
    const r = await lookupSelf('http://x', 'caller', { fetch: fetchFn as unknown as typeof fetch });
    expect(r).toEqual({ accessor: 'acc-7', ttl: 2591000, policies: ['app-mcpd'] });
  });
});

describe('testWriteReadDelete', () => {
  it('runs write→read→delete and succeeds on round-trip match', async () => {
    const calls: string[] = [];
    const fetchFn = vi.fn(async (url: string | URL, init?: RequestInit) => {
      const u = String(url);
      const m = init?.method ?? 'GET';
      calls.push(`${m} ${u}`);
      if (m === 'POST') return new Response('{}', { status: 200 });
      if (m === 'GET') {
        return new Response(JSON.stringify({ data: { data: { marker: 'mcpctl-smoke' } } }), { status: 200 });
      }
      // DELETE
      return new Response(null, { status: 200 });
    });
    await testWriteReadDelete('http://x', 'caller', 'secret', 'mcpd/smoke', { fetch: fetchFn as unknown as typeof fetch });
    expect(calls).toHaveLength(3);
    expect(calls[0]).toMatch(/POST .*\/v1\/secret\/data\/mcpd\/smoke$/);
    expect(calls[1]).toMatch(/GET .*\/v1\/secret\/data\/mcpd\/smoke$/);
    expect(calls[2]).toMatch(/DELETE .*\/v1\/secret\/metadata\/mcpd\/smoke$/);
  });

  it('throws when read-back marker does not match', async () => {
    const fetchFn = vi.fn(async (_u: string | URL, init?: RequestInit) => {
      if ((init?.method ?? 'GET') === 'GET') {
        return new Response(JSON.stringify({ data: { data: { marker: 'wrong' } } }), { status: 200 });
      }
      return new Response('{}', { status: 200 });
    });
    await expect(testWriteReadDelete('http://x', 'c', 'secret', 'p', { fetch: fetchFn as unknown as typeof fetch }))
      .rejects.toThrow(/didn't match written marker/);
  });
});
