import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Repository } from './repository.js';

function freshRepo(): { repo: Repository; dir: string } {
  const dir = mkdtempSync(join(tmpdir(), 'spaces-test-'));
  const repo = new Repository(join(dir, `${randomUUID()}.db`));
  return { repo, dir };
}

describe('spaces schema', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('creates the spaces table with the expected columns', () => {
    // @ts-expect-error reach into the private db handle for a schema assertion
    const cols = repo.db.prepare("PRAGMA table_info('spaces')").all() as Array<{ name: string }>;
    const names = cols.map(c => c.name).sort();
    expect(names).toEqual(
      ['a2a_skills', 'brand_color', 'created_at', 'description', 'id', 'kind', 'owner_id',
       'python_packages', 'status', 'title', 'tool_policy', 'updated_at', 'visibility', 'visibility_scope_org_id', 'workspace_dir'].sort(),
    );
  });

  it('adds space_id and workspace_mode columns to local_tasks', () => {
    // @ts-expect-error private db handle
    const cols = repo.db.prepare("PRAGMA table_info('local_tasks')").all() as Array<{ name: string }>;
    const names = cols.map(c => c.name);
    expect(names).toContain('space_id');
    expect(names).toContain('workspace_mode');
  });
});

describe('createSpace / getSpace', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('creates a case space and reads it back with mapped camelCase fields', async () => {
    const space = await repo.createSpace({
      kind: 'case',
      title: '案件A',
      ownerId: 'user-1',
      visibility: 'private',
      workspaceDir: '/wt/space/abc',
      brandColor: '#0d9488',
    });
    expect(space.id).toMatch(/[0-9a-f-]{36}/);
    expect(space.kind).toBe('case');
    expect(space.title).toBe('案件A');
    expect(space.ownerId).toBe('user-1');
    expect(space.visibility).toBe('private');
    expect(space.status).toBe('open');
    expect(space.brandColor).toBe('#0d9488');
    expect(space.workspaceDir).toBe('/wt/space/abc');

    const loaded = await repo.getSpace(space.id);
    expect(loaded?.title).toBe('案件A');
  });
});

describe('listSpaces / updateSpace / archiveSpace', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  const admin = { id: 'admin-1', role: 'admin', orgIds: [] } as unknown as Express.User;
  const u1 = { id: 'user-1', role: 'user', orgIds: ['org-x'] } as unknown as Express.User;
  const u2 = { id: 'user-2', role: 'user', orgIds: [] } as unknown as Express.User;

  it('filters by visibility for non-admin viewers', async () => {
    await repo.createSpace({ kind: 'case', title: 'priv1', ownerId: 'user-1', visibility: 'private' });
    await repo.createSpace({ kind: 'case', title: 'orgX', ownerId: 'user-2', visibility: 'org', visibilityScopeOrgId: 'org-x' });
    await repo.createSpace({ kind: 'case', title: 'pub', ownerId: 'user-2', visibility: 'public' });

    const seenByU1 = (await repo.listSpaces({ viewer: u1 })).map(s => s.title).sort();
    expect(seenByU1).toEqual(['orgX', 'priv1', 'pub']); // own private + org-x + public

    const seenByU2 = (await repo.listSpaces({ viewer: u2 })).map(s => s.title).sort();
    expect(seenByU2).toEqual(['orgX', 'pub']); // own org+public, NOT user-1 private

    const seenByAdmin = (await repo.listSpaces({ viewer: admin })).length;
    expect(seenByAdmin).toBe(3); // admin sees all
  });

  it('updates mutable fields and archives', async () => {
    const s = await repo.createSpace({ kind: 'case', title: 'old', ownerId: 'user-1' });
    const updated = await repo.updateSpace(s.id, { title: 'new', description: 'd', brandColor: '#123456' });
    expect(updated?.title).toBe('new');
    expect(updated?.description).toBe('d');
    expect(updated?.brandColor).toBe('#123456');

    await repo.archiveSpace(s.id);
    expect((await repo.getSpace(s.id))?.status).toBe('archived');
  });
});

describe('ensurePersonalSpace', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('creates exactly one private personal space per user and is idempotent', async () => {
    const first = await repo.ensurePersonalSpace('user-1', 'Alice');
    expect(first.kind).toBe('personal');
    expect(first.visibility).toBe('private');
    expect(first.ownerId).toBe('user-1');

    const second = await repo.ensurePersonalSpace('user-1', 'Alice');
    expect(second.id).toBe(first.id); // same row, not a duplicate

    const personalCount = (await repo.listSpaces({ viewer: { id: 'user-1', role: 'user', orgIds: [] } as unknown as Express.User }))
      .filter(s => s.kind === 'personal').length;
    expect(personalCount).toBe(1);
  });

  it('hardDeleteSpace never removes a personal space (undeletable invariant backstop)', async () => {
    const personal = await repo.ensurePersonalSpace('user-1', 'Alice');
    const caseSpace = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'user-1' });

    await repo.hardDeleteSpace(personal.id); // must be a no-op
    expect(await repo.getSpace(personal.id)).not.toBeNull();

    await repo.hardDeleteSpace(caseSpace.id); // case spaces are deletable
    expect(await repo.getSpace(caseSpace.id)).toBeNull();
  });

  it('hardDeleteSpace crypto-shreds the case space DEK but never a personal one', async () => {
    const personal = await repo.ensurePersonalSpace('user-1', 'Alice');
    const caseSpace = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'user-1' });
    const db = repo.getDb();
    // Seed both space credential DEKs for each (raw insert — the shred is pure SQL).
    const seed = (sid: string) => {
      db.prepare('INSERT INTO space_ssh_deks (space_id, encrypted_dek, key_version) VALUES (?, ?, 1)')
        .run(sid, Buffer.from('wrapped-ssh-dek'));
      db.prepare('INSERT INTO space_browser_deks (space_id, encrypted_dek) VALUES (?, ?)')
        .run(sid, Buffer.from('wrapped-browser-dek'));
    };
    seed(personal.id);
    seed(caseSpace.id);
    const sshDek = (sid: string) => db.prepare('SELECT 1 FROM space_ssh_deks WHERE space_id = ?').get(sid);
    const browserDek = (sid: string) => db.prepare('SELECT 1 FROM space_browser_deks WHERE space_id = ?').get(sid);

    // Personal id → no-op (space + both DEKs survive).
    await repo.hardDeleteSpace(personal.id);
    expect(sshDek(personal.id)).toBeTruthy();
    expect(browserDek(personal.id)).toBeTruthy();

    // Case id → space gone AND both its DEKs shredded.
    await repo.hardDeleteSpace(caseSpace.id);
    expect(sshDek(caseSpace.id)).toBeUndefined();
    expect(browserDek(caseSpace.id)).toBeUndefined();
    // The personal DEKs are untouched by the case deletion.
    expect(sshDek(personal.id)).toBeTruthy();
    expect(browserDek(personal.id)).toBeTruthy();
  });
});
