import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { encryptStateBlob } from './sessions.js';
import { ensureProfileEncryptDek, decryptProfileState, type ProfileDekDeps } from './profile-dek.js';

// In-memory DEK store (mirrors BrowserSessionRepo's get/set User/Space DEK).
function fakeRepo() {
  const u = new Map<string, Buffer>();
  const s = new Map<string, Buffer>();
  return {
    getUserDek: (id: string) => u.get(id) ?? null,
    setUserDek: (id: string, b: Buffer) => void u.set(id, b),
    getSpaceDek: (id: string) => s.get(id) ?? null,
    setSpaceDek: (id: string, b: Buffer) => void s.set(id, b),
  };
}

describe('profile-dek (Space-as-Principal P2)', () => {
  let dir: string;
  let deps: ProfileDekDeps;
  let repo: ReturnType<typeof fakeRepo>;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'profile-dek-'));
    repo = fakeRepo();
    deps = { masterKeyPath: join(dir, 'master.key'), sessRepo: repo };
  });
  afterEach(() => rmSync(dir, { recursive: true, force: true }));

  function save(ownerId: string, spaceId: string | null, state: string): Buffer {
    const dek = ensureProfileEncryptDek(deps, ownerId, spaceId);
    try {
      return encryptStateBlob(dek, state);
    } finally {
      dek.fill(0);
    }
  }

  it('encrypts a space-owned profile under the SPACE DEK (not the owner DEK)', () => {
    const blob = save('alice', 'space-1', 'STATE');
    const out = decryptProfileState(deps, { ownerId: 'alice', spaceId: 'space-1', encryptedStateBlob: blob });
    expect(out).toBe('STATE');
    expect(repo.getSpaceDek('space-1')).not.toBeNull();
    expect(repo.getUserDek('alice')).toBeNull(); // owner DEK never created for a space profile
  });

  it('encrypts a personal profile (spaceId null) under the owner DEK', () => {
    const blob = save('alice', null, 'PERSONAL');
    const out = decryptProfileState(deps, { ownerId: 'alice', spaceId: null, encryptedStateBlob: blob });
    expect(out).toBe('PERSONAL');
    expect(repo.getUserDek('alice')).not.toBeNull();
    expect(repo.getSpaceDek('space-1')).toBeNull();
  });

  it('falls back to the owner DEK when the space DEK exists but the blob predates migration', () => {
    // Blob written under the owner DEK (legacy), then the profile became space-owned.
    const legacy = save('alice', null, 'LEGACY');
    // Create a space DEK so the space attempt actually runs (and misses).
    ensureProfileEncryptDek(deps, 'someone', 'space-1').fill(0);
    const out = decryptProfileState(deps, { ownerId: 'alice', spaceId: 'space-1', encryptedStateBlob: legacy });
    expect(out).toBe('LEGACY');
  });

  it('a space-encrypted blob is NOT decryptable by the owner DEK alone', () => {
    const blob = save('alice', 'space-1', 'SECRET');
    // Force a (different) owner DEK to exist, then decrypt as a personal profile.
    save('alice', null, 'x');
    expect(() =>
      decryptProfileState(deps, { ownerId: 'alice', spaceId: null, encryptedStateBlob: blob }),
    ).toThrow();
  });

  it('throws when no DEK material exists at all', () => {
    expect(() =>
      decryptProfileState(deps, { ownerId: 'nobody', spaceId: null, encryptedStateBlob: Buffer.from('zz') }),
    ).toThrow();
  });
});
