import { RoleEnum } from '../roles/roles.enum';
import { StatusEnum } from '../statuses/statuses.enum';
import { UsersService } from './users.service';

describe('UsersService directory', () => {
  const teamA = { id: 10, name: 'Alpha', alias: 'alpha' };
  const teamB = { id: 20, name: 'Bravo', alias: 'bravo' };
  const playerRole = { id: 1, name: 'Player' };

  const users = [
    user(1, 'Riley', 'Reader', 'reader@example.com'),
    user(2, 'Emma', 'Shared', 'emma@example.com'),
    user(3, 'Iva', 'Inactive', 'iva@example.com', StatusEnum.inactive),
    user(4, 'Otis', 'Outside', 'otis@example.com'),
    user(5, 'Morgan', 'Multiple', 'morgan@example.com'),
    user(
      6,
      'Ada',
      'Admin',
      'admin@example.com',
      StatusEnum.active,
      RoleEnum.admin,
    ),
  ];

  const players = [
    assignment(101, users[0], teamA),
    assignment(201, users[1], teamA),
    assignment(301, users[2], teamA, false),
    assignment(401, users[3], teamB),
    assignment(501, users[4], teamA),
    assignment(502, users[4], teamB),
  ];

  let userRows: ReturnType<typeof directoryUserRow>[];
  let assignmentRows: ReturnType<typeof directoryAssignmentRow>[];
  let total: number;
  let rejectActiveSharedTeamPredicate: boolean;
  const usersRepository = {
    find: jest.fn(() => {
      throw new Error('directory queries must use a safe database projection');
    }),
    createQueryBuilder: jest.fn(() => createDirectoryUsersQuery()),
  };
  const playersRepository = {
    find: jest.fn(() => {
      throw new Error('directory queries must use a safe database projection');
    }),
    createQueryBuilder: jest.fn((alias: string) =>
      alias === 'requesterPlayer'
        ? createSharedTeamsQuery()
        : createDirectoryAssignmentsQuery(),
    ),
  };
  let service: UsersService;

  beforeEach(() => {
    jest.clearAllMocks();
    rejectActiveSharedTeamPredicate = false;
    setDirectoryResult(
      [users[0], users[1], users[2], users[4]],
      [players[0], players[1], players[2], players[4]],
    );
    service = new UsersService(
      usersRepository as any,
      playersRepository as any,
    );
  });

  it('hides users and assignments from teams that the requester does not share', async () => {
    const result = await directoryFor(users[0]);

    expect(result.data.map((entry) => entry.id)).toEqual([1, 2, 3, 5]);
    expect(result.data.find((entry) => entry.id === 4)).toBeUndefined();
    expect(result.data.find((entry) => entry.id === 5).assignments).toEqual([
      assignmentSummary(501, 'Morgan', 'Multiple', true, teamA),
    ]);
  });

  it('redacts email and authentication secrets for a non-admin requester', async () => {
    const result = await directoryFor(users[0]);
    const entry = result.data.find((candidate) => candidate.id === 2);

    expect(entry).toEqual({
      id: 2,
      firstName: 'Emma',
      lastName: 'Shared',
      status: { id: StatusEnum.active, name: 'Active' },
      assignments: [assignmentSummary(201, 'Emma', 'Shared', true, teamA)],
    });
    expect(entry).not.toHaveProperty('email');
    expect(entry).not.toHaveProperty('password');
    expect(entry).not.toHaveProperty('hash');
    expect(entry).not.toHaveProperty('socialId');
  });

  it('keeps inactive users and inactive assignments visible in shared teams', async () => {
    const result = await directoryFor(users[0]);

    expect(result.data.find((entry) => entry.id === 3)).toEqual({
      id: 3,
      firstName: 'Iva',
      lastName: 'Inactive',
      status: { id: StatusEnum.inactive, name: 'Inactive' },
      assignments: [assignmentSummary(301, 'Iva', 'Inactive', false, teamA)],
    });
  });

  it('treats an inactive requester assignment as a shared team membership', async () => {
    rejectActiveSharedTeamPredicate = true;
    setDirectoryResult(
      [users[0], users[1]],
      [
        assignment(101, users[0], teamA, false),
        assignment(201, users[1], teamA),
      ],
    );

    const result = await directoryFor(users[0]);

    expect(result.data.map((entry) => entry.id)).toEqual([1, 2]);
  });

  it('does not hydrate whole user entities for the directory', async () => {
    const result = await directoryFor(users[0]);

    expect(
      result.data.every(
        (entry) =>
          !('password' in entry || 'hash' in entry || 'socialId' in entry),
      ),
    ).toBe(true);
  });

  it('returns every user and assignment with email and role for an admin requester', async () => {
    setDirectoryResult(users, players, true);

    const result = await directoryFor(users[5]);
    const multiple = result.data.find((entry) => entry.id === 5);
    const outsider = result.data.find((entry) => entry.id === 4);

    expect(result.data).toHaveLength(6);
    expect(outsider).toMatchObject({
      email: 'otis@example.com',
      role: { id: RoleEnum.user, name: 'User' },
    });
    expect(multiple.assignments).toEqual([
      assignmentSummary(501, 'Morgan', 'Multiple', true, teamA),
      assignmentSummary(502, 'Morgan', 'Multiple', true, teamB),
    ]);
    expect(multiple).not.toHaveProperty('password');
    expect(multiple).not.toHaveProperty('hash');
    expect(multiple).not.toHaveProperty('socialId');
  });

  it('deduplicates a user with assignments in more than one shared team before pagination', async () => {
    setDirectoryResult(
      [users[2], users[4]],
      [players[2], players[4]],
      false,
      4,
    );

    const result = await directoryFor(users[0], { page: 2, limit: 2 });

    expect(result.data.map((entry) => entry.id)).toEqual([3, 5]);
    expect(result.total).toBe(4);
    expect(result.hasNextPage).toBe(false);
  });

  it('searches visible names case-insensitively without exposing outside-team users', async () => {
    setDirectoryResult([users[4]], [players[4]]);
    const matched = await directoryFor(users[0], { search: 'mOrGaN' });
    setDirectoryResult([], []);
    const hidden = await directoryFor(users[0], { search: 'outside' });

    expect(matched.data.map((entry) => entry.id)).toEqual([5]);
    expect(hidden.data).toEqual([]);
  });

  it('paginates the deduplicated, filtered directory and reports the next page', async () => {
    setDirectoryResult(
      [users[0], users[1]],
      [players[0], players[1]],
      false,
      4,
    );

    const result = await directoryFor(users[0], { page: 1, limit: 2 });

    expect(result).toMatchObject({
      page: 1,
      limit: 2,
      total: 4,
      hasNextPage: true,
    });
    expect(result.data.map((entry) => entry.id)).toEqual([1, 2]);
  });

  function directoryFor(
    requester: typeof users[number],
    query: { page?: number; limit?: number; search?: string } = {},
  ) {
    return (service as any).findDirectory(requester, {
      page: 1,
      limit: 20,
      ...query,
    });
  }

  function user(
    id: number,
    firstName: string,
    lastName: string,
    email: string,
    statusId = StatusEnum.active,
    roleId = RoleEnum.user,
  ) {
    return {
      id,
      firstName,
      lastName,
      email,
      password: `password-${id}`,
      hash: `hash-${id}`,
      socialId: `social-${id}`,
      provider: 'email',
      previousPassword: `previous-password-${id}`,
      status: {
        id: statusId,
        name: statusId === StatusEnum.active ? 'Active' : 'Inactive',
      },
      role: {
        id: roleId,
        name: roleId === RoleEnum.admin ? 'Admin' : 'User',
      },
    };
  }

  function assignment(
    id: number,
    playerUser: typeof users[number],
    team: typeof teamA,
    active = true,
  ) {
    return {
      id,
      firstName: playerUser.firstName,
      lastName: playerUser.lastName,
      active,
      user: playerUser,
      team,
      teamRole: playerRole,
    };
  }

  function assignmentSummary(
    id: number,
    firstName: string,
    lastName: string,
    active: boolean,
    team: typeof teamA,
  ) {
    return {
      id,
      firstName,
      lastName,
      active,
      team,
      teamRole: playerRole,
    };
  }

  function setDirectoryResult(
    directoryUsers: typeof users,
    directoryPlayers: typeof players,
    isAdmin = false,
    directoryTotal = directoryUsers.length,
  ) {
    userRows = directoryUsers.map((directoryUser) =>
      directoryUserRow(directoryUser, isAdmin),
    );
    assignmentRows = directoryPlayers.map(directoryAssignmentRow);
    total = directoryTotal;
  }

  function directoryUserRow(
    directoryUser: typeof users[number],
    isAdmin = false,
  ) {
    return {
      user_id: directoryUser.id,
      first_name: directoryUser.firstName,
      last_name: directoryUser.lastName,
      status_id: directoryUser.status.id,
      status_name: directoryUser.status.name,
      ...(isAdmin
        ? {
            email: directoryUser.email,
            role_id: directoryUser.role.id,
            role_name: directoryUser.role.name,
          }
        : {}),
    };
  }

  function directoryAssignmentRow(directoryPlayer: typeof players[number]) {
    return {
      player_id: directoryPlayer.id,
      user_id: directoryPlayer.user.id,
      first_name: directoryPlayer.firstName,
      last_name: directoryPlayer.lastName,
      active: directoryPlayer.active,
      team_id: directoryPlayer.team.id,
      team_name: directoryPlayer.team.name,
      team_alias: directoryPlayer.team.alias,
      team_role_id: directoryPlayer.teamRole.id,
      team_role_name: directoryPlayer.teamRole.name,
    };
  }

  function createDirectoryUsersQuery() {
    const query: any = {
      leftJoin: () => query,
      innerJoin: () => query,
      setParameters: () => query,
      setParameter: () => query,
      andWhere: () => query,
      distinct: () => query,
      orderBy: () => query,
      offset: () => query,
      limit: () => query,
      select: (selection: string | string[]) => {
        const selected = Array.isArray(selection) ? selection : [selection];
        if (selected.join(' ').match(/password|hash|socialId|provider/i)) {
          throw new Error(
            'directory projection contains an authentication field',
          );
        }
        return query;
      },
      clone: () => createDirectoryCountQuery(),
      getRawMany: () => Promise.resolve(userRows),
    };
    return query;
  }

  function createDirectoryCountQuery() {
    const query: any = {
      select: () => query,
      getRawOne: () => Promise.resolve({ total: String(total) }),
    };
    return query;
  }

  function createSharedTeamsQuery() {
    const query: any = {
      select: () => query,
      where: (predicate: string) => {
        if (
          rejectActiveSharedTeamPredicate &&
          /requesterPlayer\.active/i.test(predicate)
        ) {
          throw new Error(
            'shared-team membership must not filter inactive requester assignments',
          );
        }
        return query;
      },
      getQuery: () =>
        'SELECT requesterPlayer.teamId FROM player requesterPlayer',
      getParameters: () => ({ requesterId: users[0].id }),
    };
    return query;
  }

  function createDirectoryAssignmentsQuery() {
    const query: any = {
      innerJoin: () => query,
      leftJoin: () => query,
      select: () => query,
      where: () => query,
      andWhere: () => query,
      setParameters: () => query,
      orderBy: () => query,
      getRawMany: () => Promise.resolve(assignmentRows),
    };
    return query;
  }
});
