import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { VapidKeyStore } from './vapid-store.js';

describe('VapidKeyStore', () => {
  let tempDir = '';
  const SUBJECT = 'https://aao.example/';

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-vapid-'));
  });
  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeStore() {
    return new VapidKeyStore(join(tempDir, 'vapid.json'), join(tempDir, 'vapid-history'));
  }

  it('generates a new key file on first load', () => {
    const store = makeStore();
    const k = store.loadOrGenerate(SUBJECT);
    expect(k.keyId).toMatch(/^v1-\d{4}-\d{2}-\d{2}-[a-z0-9]+$/);
    expect(k.publicKey).toBeTruthy();
    expect(k.privateKey).toBeTruthy();
    expect(k.subject).toBe(SUBJECT);
    expect(existsSync(join(tempDir, 'vapid.json'))).toBe(true);
  });

  it('writes the key file with mode 0600', () => {
    const store = makeStore();
    store.loadOrGenerate(SUBJECT);
    const mode = statSync(join(tempDir, 'vapid.json')).mode & 0o777;
    expect(mode).toBe(0o600);
  });

  it('returns the same key on repeated load (idempotent)', () => {
    const store1 = makeStore();
    const a = store1.loadOrGenerate(SUBJECT);
    const store2 = makeStore();
    const b = store2.loadOrGenerate(SUBJECT);
    expect(b.keyId).toBe(a.keyId);
    expect(b.publicKey).toBe(a.publicKey);
    expect(b.privateKey).toBe(a.privateKey);
  });

  it('getKey returns current key by id', () => {
    const store = makeStore();
    const k = store.loadOrGenerate(SUBJECT);
    expect(store.getKey(k.keyId)).toEqual(k);
  });

  it('getKey returns null for unknown keyId', () => {
    const store = makeStore();
    store.loadOrGenerate(SUBJECT);
    expect(store.getKey('nonexistent-key-id')).toBeNull();
  });

  it('rotate moves the old key to history and creates a new current', () => {
    const store = makeStore();
    const oldKey = store.loadOrGenerate(SUBJECT);
    const newKey = store.rotate(SUBJECT);

    expect(newKey.keyId).not.toBe(oldKey.keyId);
    expect(newKey.publicKey).not.toBe(oldKey.publicKey);
    expect(existsSync(join(tempDir, 'vapid.json'))).toBe(true);
    expect(existsSync(join(tempDir, 'vapid-history', `${oldKey.keyId}.json`))).toBe(true);
  });

  it('rotate: old key is still retrievable via getKey from history', () => {
    const store = makeStore();
    const oldKey = store.loadOrGenerate(SUBJECT);
    store.rotate(SUBJECT);
    const recovered = store.getKey(oldKey.keyId);
    expect(recovered).not.toBeNull();
    expect(recovered?.publicKey).toBe(oldKey.publicKey);
    expect(recovered?.privateKey).toBe(oldKey.privateKey);
  });

  it('history file load works across process restart (fresh store instance)', () => {
    const s1 = makeStore();
    const oldKey = s1.loadOrGenerate(SUBJECT);
    s1.rotate(SUBJECT);

    // Simulate restart: a fresh VapidKeyStore reads the same on-disk state.
    const s2 = makeStore();
    s2.loadOrGenerate(SUBJECT);
    const recovered = s2.getKey(oldKey.keyId);
    expect(recovered?.publicKey).toBe(oldKey.publicKey);
  });

  it('throws on malformed key file', () => {
    writeFileSync(join(tempDir, 'vapid.json'), '{"keyId":"only-id"}', { mode: 0o600 });
    const store = makeStore();
    expect(() => store.loadOrGenerate(SUBJECT)).toThrow(/malformed/);
  });

  it('getCurrent throws if loadOrGenerate was never called', () => {
    const store = makeStore();
    expect(() => store.getCurrent()).toThrow(/loadOrGenerate/);
  });

  it('keyId reflects the date prefix', () => {
    const store = makeStore();
    const k = store.loadOrGenerate(SUBJECT);
    const today = new Date().toISOString().slice(0, 10);
    expect(k.keyId).toContain(today);
  });

  it('readKeyFile preserves the original key material verbatim', () => {
    const store = makeStore();
    const k = store.loadOrGenerate(SUBJECT);
    const raw = JSON.parse(readFileSync(join(tempDir, 'vapid.json'), 'utf-8'));
    expect(raw.publicKey).toBe(k.publicKey);
    expect(raw.privateKey).toBe(k.privateKey);
    expect(raw.subject).toBe(k.subject);
  });
});
