import { describe, expect, it } from 'vitest';
import {
  buildVisibilityWhere,
  canUserSeeTask,
  canManageSpace,
  canEditInSpace,
} from './visibility.js';

function makeUser(overrides: Partial<Express.User> = {}): Express.User {
  return {
    id: 'user-1', email: 'u@x.com', name: 'u', avatarUrl: null,
    role: 'user', status: 'active',
    orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
    ...overrides,
  };
}

describe('buildVisibilityWhere', () => {
  it('admin sees everything (1=1)', () => {
    const w = buildVisibilityWhere(makeUser({ role: 'admin' }), 'lt');
    expect(w.clause).toBe('1=1');
    expect(w.params).toEqual([]);
  });

  it('user with no orgs: owner or public only', () => {
    const w = buildVisibilityWhere(makeUser(), 'lt');
    expect(w.clause).toContain('lt.owner_id = ?');
    expect(w.clause).toContain("lt.visibility = 'public'");
    expect(w.clause).toContain('IN (NULL)'); // empty orgs → never matches
    expect(w.params).toEqual(['user-1']);
  });

  it('user with orgs: owner or public or same-org', () => {
    const w = buildVisibilityWhere(makeUser({ orgIds: ['10', '20'] }), 'lt');
    expect(w.clause).toMatch(/lt\.visibility_scope_org_id IN \(\?,\?\)/);
    expect(w.params).toEqual(['user-1', '10', '20']);
  });

  it('respects custom table alias', () => {
    const w = buildVisibilityWhere(makeUser(), 'j');
    expect(w.clause).toContain('j.owner_id');
    expect(w.clause).not.toContain('lt.');
  });

  // ─── 後方互換: opts 省略時はバイト一致で現状動作 ───
  it('backward-compat: omitting opts is byte-identical to the legacy 2-arg form (no orgs)', () => {
    const noOpts = buildVisibilityWhere(makeUser(), 'lt');
    expect(noOpts.clause).not.toContain('space_members');
    expect(noOpts.params).toEqual(['user-1']);
    // 既存の節構造（owner / public / org）のみ
    expect(noOpts.clause).toBe(
      "( lt.owner_id = ? OR lt.visibility = 'public' OR (lt.visibility = 'org' AND lt.visibility_scope_org_id IN (NULL)) )",
    );
  });

  it('backward-compat: omitting opts byte-identical with orgs', () => {
    const noOpts = buildVisibilityWhere(makeUser({ orgIds: ['10', '20'] }), 'lt');
    expect(noOpts.clause).not.toContain('space_members');
    expect(noOpts.params).toEqual(['user-1', '10', '20']);
  });

  // ─── 新 opts: spaceColumn のメンバーシップ OR ブランチ（メンバー + 根オーナー） ───
  it('opts.spaceColumn (non-admin) appends membership+owner OR-branch with user.id LAST (twice) in params', () => {
    const w = buildVisibilityWhere(makeUser({ orgIds: ['10'] }), 'lt', { spaceColumn: 'space_id' });
    // メンバーシップ（space_members）と根オーナー（spaces.owner_id）の両方を OR で通す。
    expect(w.clause).toContain(
      'OR (lt.space_id IN (SELECT space_id FROM space_members WHERE user_id = ?) OR lt.space_id IN (SELECT id FROM spaces WHERE owner_id = ?))',
    );
    // 既存 params の後ろに membership.user_id → owner.user_id の順で 2 回足される（順序が重要）
    expect(w.params).toEqual(['user-1', '10', 'user-1', 'user-1']);
  });

  it('opts.spaceColumn supports the spaces self-alias (id column), includes owner branch', () => {
    const w = buildVisibilityWhere(makeUser(), 's', { spaceColumn: 'id' });
    expect(w.clause).toContain(
      'OR (s.id IN (SELECT space_id FROM space_members WHERE user_id = ?) OR s.id IN (SELECT id FROM spaces WHERE owner_id = ?))',
    );
    expect(w.params).toEqual(['user-1', 'user-1', 'user-1']);
  });

  it('opts.spaceColumn is IGNORED for admin (still 1=1, no membership subquery)', () => {
    const w = buildVisibilityWhere(makeUser({ role: 'admin' }), 'lt', { spaceColumn: 'space_id' });
    expect(w.clause).toBe('1=1');
    expect(w.params).toEqual([]);
    expect(w.clause).not.toContain('space_members');
  });

  it('opts present but spaceColumn unset → no membership branch (identical to omitting opts)', () => {
    const w = buildVisibilityWhere(makeUser(), 'lt', {});
    expect(w.clause).not.toContain('space_members');
    expect(w.params).toEqual(['user-1']);
  });
});

describe('canManageSpace', () => {
  const admin = makeUser({ id: 'admin-x', role: 'admin' });
  const owner = makeUser({ id: 'owner-1' });
  const other = makeUser({ id: 'other-1' });
  const space = { ownerId: 'owner-1' };

  it('admin can always manage', () => {
    expect(canManageSpace(admin, space)).toBe(true);
    expect(canManageSpace(admin, space, null)).toBe(true);
  });
  it('root owner (space.ownerId) can manage regardless of member role', () => {
    expect(canManageSpace(owner, space)).toBe(true);
    expect(canManageSpace(owner, space, null)).toBe(true);
  });
  it("member with role 'owner' can manage", () => {
    expect(canManageSpace(other, space, 'owner')).toBe(true);
  });
  it("editor / viewer / non-member cannot manage", () => {
    expect(canManageSpace(other, space, 'editor')).toBe(false);
    expect(canManageSpace(other, space, 'viewer')).toBe(false);
    expect(canManageSpace(other, space, null)).toBe(false);
    expect(canManageSpace(other, space)).toBe(false);
  });
  it('null space.ownerId does not match a null-id-ish user', () => {
    expect(canManageSpace(other, { ownerId: null })).toBe(false);
  });
});

describe('canEditInSpace', () => {
  const admin = makeUser({ id: 'admin-x', role: 'admin' });
  const owner = makeUser({ id: 'owner-1' });
  const other = makeUser({ id: 'other-1' });
  const space = { ownerId: 'owner-1' };

  it('admin / root owner / member-owner can edit', () => {
    expect(canEditInSpace(admin, space)).toBe(true);
    expect(canEditInSpace(owner, space)).toBe(true);
    expect(canEditInSpace(other, space, 'owner')).toBe(true);
  });
  it("editor can edit", () => {
    expect(canEditInSpace(other, space, 'editor')).toBe(true);
  });
  it("viewer cannot edit (read-only)", () => {
    expect(canEditInSpace(other, space, 'viewer')).toBe(false);
  });
  it("non-member cannot edit", () => {
    expect(canEditInSpace(other, space, null)).toBe(false);
    expect(canEditInSpace(other, space)).toBe(false);
  });
});

describe('canUserSeeTask', () => {
  const adminUser = makeUser({ role: 'admin' });
  const aliceNoOrg = makeUser({ id: 'alice' });
  const bobOrg10 = makeUser({ id: 'bob', orgIds: ['10'] });

  it('admin sees private tasks of others', () => {
    const t = { ownerId: 'someone-else', visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(adminUser, t)).toBe(true);
  });

  it('owner sees own private task', () => {
    const t = { ownerId: 'alice', visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(true);
  });

  it('non-owner cannot see another user\'s private task', () => {
    const t = { ownerId: 'someone-else', visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(false);
  });

  it('public task is visible to anyone', () => {
    const t = { ownerId: 'someone-else', visibility: 'public' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(true);
  });

  it('org task: same org member can see', () => {
    const t = { ownerId: 'someone-else', visibility: 'org' as const, visibilityScopeOrgId: '10' };
    expect(canUserSeeTask(bobOrg10, t)).toBe(true);
  });

  it('org task: different org member cannot see', () => {
    const t = { ownerId: 'someone-else', visibility: 'org' as const, visibilityScopeOrgId: '99' };
    expect(canUserSeeTask(bobOrg10, t)).toBe(false);
  });

  it('org task with null scope: only owner can see', () => {
    const t = { ownerId: 'alice', visibility: 'org' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(true);
    expect(canUserSeeTask(bobOrg10, t)).toBe(false);
  });

  it('owner with null ownerId: not matched (null !== null check skipped)', () => {
    const t = { ownerId: null, visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(false);
  });

  it('private space task: spaceAccessible=true grants a non-owner member view', () => {
    const t = { ownerId: 'someone-else', visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t, true)).toBe(true);
  });

  it('private space task: spaceAccessible=false → still 404 for non-member', () => {
    const t = { ownerId: 'someone-else', visibility: 'private' as const, visibilityScopeOrgId: null };
    expect(canUserSeeTask(aliceNoOrg, t, false)).toBe(false);
    expect(canUserSeeTask(aliceNoOrg, t)).toBe(false);
  });
});
