import { afterEach, beforeEach, describe, expect, it } 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';

/**
 * ROOT-CAUSE security test: 個人スペース（kind='personal'）は所有者本人にしか
 * 見えない（admin も例外でない）。これがないと admin の listSpaces/getSpace が
 * 他ユーザーの個人スペースを返し、UI が「最初に見つかった personal」を自分の
 * ワークスペースと誤認する（誤った members-owner / AGENTS.md / MCP / 設定で動く）。
 *
 * 同時に、case スペースの可視性は従来通り（admin は全件、メンバーは可視、
 * 非メンバーは不可視）であることを退行防止として固定する。
 */

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

function viewer(id: string, role: 'admin' | 'user' = 'user', orgIds: string[] = []): Express.User {
  return {
    id,
    email: `${id}@x.com`,
    name: id,
    avatarUrl: null,
    role,
    status: 'active',
    orgIds,
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
  } as Express.User;
}

describe('personal space visibility (owner-only, even for admin)', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { repo.close(); rmSync(dir, { recursive: true, force: true }); });

  it('admin does NOT see another user\'s personal space via listSpaces', async () => {
    const alice = repo.createUser({ email: 'a@x.com', name: 'alice', role: 'user', status: 'active' });
    const admin = repo.createUser({ email: 'adm@x.com', name: 'adm', role: 'admin', status: 'active' });
    const alicePersonal = await repo.ensurePersonalSpace(alice.id, 'alice');
    const adminPersonal = await repo.ensurePersonalSpace(admin.id, 'adm');

    const asAdmin = await repo.listSpaces({ viewer: viewer(admin.id, 'admin') });
    const ids = asAdmin.map(s => s.id);
    // admin は自分の個人スペースだけ見える。alice の個人スペースは混ざらない。
    expect(ids).toContain(adminPersonal.id);
    expect(ids).not.toContain(alicePersonal.id);
  });

  it('admin gets null on getSpace for another user\'s personal space', async () => {
    const alice = repo.createUser({ email: 'a2@x.com', name: 'alice', role: 'user', status: 'active' });
    const admin = repo.createUser({ email: 'adm2@x.com', name: 'adm', role: 'admin', status: 'active' });
    const alicePersonal = await repo.ensurePersonalSpace(alice.id, 'alice');

    const got = await repo.getSpace(alicePersonal.id, { viewer: viewer(admin.id, 'admin') });
    expect(got).toBeNull();
  });

  it('owner sees their own personal space (list + get)', async () => {
    const alice = repo.createUser({ email: 'a3@x.com', name: 'alice', role: 'user', status: 'active' });
    const alicePersonal = await repo.ensurePersonalSpace(alice.id, 'alice');

    const list = await repo.listSpaces({ viewer: viewer(alice.id) });
    expect(list.map(s => s.id)).toContain(alicePersonal.id);
    const got = await repo.getSpace(alicePersonal.id, { viewer: viewer(alice.id) });
    expect(got?.id).toBe(alicePersonal.id);
  });

  it('a non-owner user does NOT see another user\'s personal space', async () => {
    const alice = repo.createUser({ email: 'a4@x.com', name: 'alice', role: 'user', status: 'active' });
    const bob = repo.createUser({ email: 'b4@x.com', name: 'bob', role: 'user', status: 'active' });
    const alicePersonal = await repo.ensurePersonalSpace(alice.id, 'alice');

    const list = await repo.listSpaces({ viewer: viewer(bob.id) });
    expect(list.map(s => s.id)).not.toContain(alicePersonal.id);
    expect(await repo.getSpace(alicePersonal.id, { viewer: viewer(bob.id) })).toBeNull();
  });

  it('no-viewer (no-auth) path still returns personal spaces (synthetic local owner)', async () => {
    const local = repo.createUser({ email: 'local@x.com', name: 'local', role: 'user', status: 'active' });
    const personal = await repo.ensurePersonalSpace(local.id, 'local');
    const list = await repo.listSpaces();
    expect(list.map(s => s.id)).toContain(personal.id);
    expect((await repo.getSpace(personal.id))?.id).toBe(personal.id);
  });
});

describe('case space visibility unchanged (regression guard)', () => {
  let repo: Repository;
  let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { repo.close(); rmSync(dir, { recursive: true, force: true }); });

  it('admin sees every case space; members see theirs; strangers do not', async () => {
    const owner = repo.createUser({ email: 'o@x.com', name: 'o', role: 'user', status: 'active' });
    const member = repo.createUser({ email: 'm@x.com', name: 'm', role: 'user', status: 'active' });
    const stranger = repo.createUser({ email: 's@x.com', name: 's', role: 'user', status: 'active' });
    const admin = repo.createUser({ email: 'adm3@x.com', name: 'adm', role: 'admin', status: 'active' });

    const space = await repo.createSpace({ kind: 'case', title: '案件A', ownerId: owner.id, visibility: 'private' });
    await repo.addSpaceMember({ spaceId: space.id, userId: member.id, role: 'editor' });

    // admin: 全件可視
    expect((await repo.listSpaces({ viewer: viewer(admin.id, 'admin') })).map(s => s.id)).toContain(space.id);
    expect((await repo.getSpace(space.id, { viewer: viewer(admin.id, 'admin') }))?.id).toBe(space.id);

    // owner: 可視
    expect((await repo.getSpace(space.id, { viewer: viewer(owner.id) }))?.id).toBe(space.id);

    // member: 可視
    expect((await repo.listSpaces({ viewer: viewer(member.id) })).map(s => s.id)).toContain(space.id);
    expect((await repo.getSpace(space.id, { viewer: viewer(member.id) }))?.id).toBe(space.id);

    // stranger: 不可視（404 相当 = null）
    expect((await repo.listSpaces({ viewer: viewer(stranger.id) })).map(s => s.id)).not.toContain(space.id);
    expect(await repo.getSpace(space.id, { viewer: viewer(stranger.id) })).toBeNull();
  });
});
