/**
 * Tests for resolveConsoleTaskSpace — the membership gate that stops a
 * non-member (who can merely SEE a public/org task) from opening or
 * re-attaching an SSH console on that task's workspace connections.
 */
import { describe, it, expect } from 'vitest';
import { resolveConsoleTaskSpace, CONSOLE_SPACE_DENY, type ConsoleSpaceResolverRepo } from './console-space-resolver.js';

function mkRepo(opts: {
  spaces?: Record<string, { ownerId: string | null }>;
  members?: Record<string, Record<string, string>>; // spaceId -> userId -> role
  personalSpaceId?: string;
  personalThrows?: boolean;
}): ConsoleSpaceResolverRepo {
  return {
    ensurePersonalSpace: async (ownerId: string) => {
      if (opts.personalThrows) throw new Error('boom');
      return { id: opts.personalSpaceId ?? `personal-${ownerId}` };
    },
    getSpace: async (spaceId: string) => opts.spaces?.[spaceId] ?? null,
    getSpaceMemberRole: (spaceId: string, userId: string) =>
      opts.members?.[spaceId]?.[userId] ?? null,
  };
}

describe('resolveConsoleTaskSpace', () => {
  it('case space: owner gets the space id', async () => {
    const repo = mkRepo({ spaces: { 'space-A': { ownerId: 'alice' } } });
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: 'space-A' }, { id: 'alice' });
    expect(r).toBe('space-A');
  });

  it('case space: a non-owner MEMBER gets the space id', async () => {
    const repo = mkRepo({
      spaces: { 'space-A': { ownerId: 'alice' } },
      members: { 'space-A': { carol: 'member' } },
    });
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: 'space-A' }, { id: 'carol' });
    expect(r).toBe('space-A');
  });

  it('case space: a non-member who can SEE the task gets the DENY sentinel (IDOR guard)', async () => {
    const repo = mkRepo({ spaces: { 'space-A': { ownerId: 'alice' } } });
    // bob is neither owner nor member — even though task visibility let him here.
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: 'space-A' }, { id: 'bob' });
    // MUST NOT be null: null would fall to the legacy space-less path and let
    // bob reach a space_id=NULL connection he owns/is-granted from this task.
    expect(r).toBe(CONSOLE_SPACE_DENY);
    expect(r).not.toBeNull();
  });

  it('personal WS: NULL space + owner resolves to the owner personal space', async () => {
    const repo = mkRepo({
      personalSpaceId: 'space-alice-personal',
      spaces: { 'space-alice-personal': { ownerId: 'alice' } },
    });
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: null }, { id: 'alice' });
    expect(r).toBe('space-alice-personal');
  });

  it('personal WS: a viewer who is not the owner gets the DENY sentinel', async () => {
    const repo = mkRepo({
      personalSpaceId: 'space-alice-personal',
      spaces: { 'space-alice-personal': { ownerId: 'alice' } },
    });
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: null }, { id: 'bob' });
    expect(r).toBe(CONSOLE_SPACE_DENY);
  });

  it('no-owner (no-auth legacy) task stays no-space (null) — legacy path is the ONLY null case', async () => {
    const repo = mkRepo({});
    const r = await resolveConsoleTaskSpace(repo, { ownerId: null, spaceId: null }, { id: 'local' });
    expect(r).toBeNull();
  });

  it('hard-denies (sentinel, not null) when personal-space resolution throws', async () => {
    const repo = mkRepo({ personalThrows: true });
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: null }, { id: 'alice' });
    // Not null: a failed resolve of a space-scoped task must not fall to legacy.
    expect(r).toBe(CONSOLE_SPACE_DENY);
  });

  it('admin is NOT a shortcut: an admin non-member gets the DENY sentinel', async () => {
    const repo = mkRepo({ spaces: { 'space-A': { ownerId: 'alice' } } });
    // No role passed here — the helper takes only { id }; admin-ness must not
    // leak in. A separate owner/member row is the ONLY way in.
    const r = await resolveConsoleTaskSpace(repo, { ownerId: 'alice', spaceId: 'space-A' }, { id: 'admin-1' });
    expect(r).toBe(CONSOLE_SPACE_DENY);
  });
});
