// tslint:disable:no-unused-expression
import { faker } from "@faker-js/faker";
import { fail } from "assert";
import * as chai from "chai";
import { omit } from "lodash-es";
import { KeycloakAdminClient } from "../src/client.js";
import type ClientRepresentation from "../src/defs/clientRepresentation.js";
import type FederatedIdentityRepresentation from "../src/defs/federatedIdentityRepresentation.js";
import type GroupRepresentation from "../src/defs/groupRepresentation.js";
import { RequiredActionAlias } from "../src/defs/requiredActionProviderRepresentation.js";
import type RoleRepresentation from "../src/defs/roleRepresentation.js";
import type UserRepresentation from "../src/defs/userRepresentation.js";
import { UnmanagedAttributePolicy } from "../src/defs/userProfileMetadata.js";
import { credentials } from "./constants.js";

const expect = chai.expect;

describe("Users", () => {
  let kcAdminClient: KeycloakAdminClient;
  let currentClient: ClientRepresentation;
  let currentUser: UserRepresentation;
  let currentRole: RoleRepresentation;
  let federatedIdentity: FederatedIdentityRepresentation;

  before(async () => {
    kcAdminClient = new KeycloakAdminClient();
    await kcAdminClient.auth(credentials);

    // Enable unmanaged attributes
    const currentProfileConfig = await kcAdminClient.users.getProfile();
    await kcAdminClient.users.updateProfile({
      ...currentProfileConfig,
      unmanagedAttributePolicy: UnmanagedAttributePolicy.Enabled,
    });

    // initialize user
    const username = faker.internet.userName();
    const user = await kcAdminClient.users.create({
      username,
      email: "test@keycloak.org",
      // enabled required to be true in order to send actions email
      emailVerified: true,
      enabled: true,
      attributes: {
        key: "value",
      },
    });

    expect(user.id).to.be.ok;
    currentUser = (await kcAdminClient.users.findOne({ id: user.id }))!;

    // add smtp to realm
    await kcAdminClient.realms.update(
      { realm: "master" },
      {
        smtpServer: {
          auth: true,
          from: "0830021730-07fb21@inbox.mailtrap.io",
          host: "smtp.mailtrap.io",
          user: process.env.SMTP_USER,
          password: process.env.SMTP_PWD,
        },
      },
    );
  });

  after(async () => {
    const userId = currentUser.id;
    await kcAdminClient.users.del({
      id: userId!,
    });

    const user = await kcAdminClient.users.findOne({
      id: userId!,
    });
    expect(user).to.be.null;
  });

  it("list users", async () => {
    const users = await kcAdminClient.users.find();
    expect(users).to.be.ok;
  });

  it("count users", async () => {
    const numUsers = await kcAdminClient.users.count();
    // admin user + created user in before hook
    expect(numUsers).to.equal(2);
  });

  it("count users with filter", async () => {
    const numUsers = await kcAdminClient.users.count({
      email: "test@keycloak.org",
    });
    expect(numUsers).to.equal(1);
  });

  it.skip("gets the profile", async () => {
    const profile = await kcAdminClient.users.getProfile();
    expect(profile).to.be.ok;
  });

  it.skip("updates the profile", async () => {
    const profile = await kcAdminClient.users.updateProfile({});
    expect(profile).to.be.ok;
  });

  it("find users by custom attributes", async () => {
    // Searching by attributes is only available from Keycloak > 15
    const users = await kcAdminClient.users.find({ q: "key:value" });
    expect(users.length).to.be.equal(1);
    expect(users[0]).to.be.deep.include(currentUser);
  });

  it("find users by builtin attributes", async () => {
    // Searching by attributes is only available from Keycloak > 15
    const users = await kcAdminClient.users.find({
      q: `email:${currentUser.email}`,
    });
    expect(users.length).to.be.equal(1);
    expect(users[0]).to.be.deep.include(currentUser);
  });

  it("get single users", async () => {
    const userId = currentUser.id;
    const user = await kcAdminClient.users.findOne({
      id: userId!,
    });
    expect(user).to.be.deep.include(currentUser);
  });

  it("update single users", async () => {
    const userId = currentUser.id;
    await kcAdminClient.users.update(
      { id: userId! },
      {
        firstName: "william",
        lastName: "chang",
        requiredActions: [RequiredActionAlias.UPDATE_PASSWORD],
        emailVerified: true,
      },
    );

    const user = await kcAdminClient.users.findOne({
      id: userId!,
    });
    expect(user).to.deep.include({
      firstName: "william",
      lastName: "chang",
      requiredActions: [RequiredActionAlias.UPDATE_PASSWORD],
      emailVerified: true,
    });
  });

  /**
   * reset password
   */

  it("should reset user password", async () => {
    const userId = currentUser.id;
    await kcAdminClient.users.resetPassword({
      id: userId!,
      credential: {
        temporary: false,
        type: "password",
        value: "test",
      },
    });
  });

  /**
   * get user credentials
   */
  it("get user credentials", async () => {
    const userId = currentUser.id;
    const result = await kcAdminClient.users.getCredentials({
      id: userId!,
    });

    expect(result.map((c) => c.type)).to.include("password");
  });

  it("get configured user storage credential types", async () => {
    const userId = currentUser.id;
    const result = await kcAdminClient.users.getUserStorageCredentialTypes({
      id: userId!,
    });

    expect(result).to.be.deep.eq([]);
  });

  /**
   * delete user credentials
   */
  it("delete user credentials", async () => {
    const userId = currentUser.id;
    const result = await kcAdminClient.users.getCredentials({
      id: userId!,
    });

    expect(result.map((c) => c.type)).to.include("password");

    const credential = result[0];
    await kcAdminClient.users.deleteCredential({
      id: userId!,
      credentialId: credential.id!,
    });

    const credentialsAfterDelete = await kcAdminClient.users.getCredentials({
      id: userId!,
    });

    expect(credentialsAfterDelete).to.not.deep.include(credential);

    // Add deleted password back
    await kcAdminClient.users.resetPassword({
      id: userId!,
      credential: {
        temporary: false,
        type: "password",
        value: "test",
      },
    });
  });

  /**
   * update a credential label for a user
   */
  it("update a credential label for a user", async () => {
    const userId = currentUser.id;
    const result = await kcAdminClient.users.getCredentials({
      id: userId!,
    });

    expect(result.map((c) => c.type)).to.include("password");

    const credential = result[0];

    await kcAdminClient.users.updateCredentialLabel(
      { id: userId!, credentialId: credential.id! },
      "New user label",
    );

    const credentialsAfterLabelUpdate =
      await kcAdminClient.users.getCredentials({
        id: userId!,
      });

    expect(credentialsAfterLabelUpdate.map((c) => c.userLabel)).to.include(
      "New user label",
    );
  });

  /**
   * Groups
   */
  describe("user groups", () => {
    let currentGroup: GroupRepresentation;
    before(async () => {
      const group = await kcAdminClient.groups.create({
        name: "cool-group",
      });
      expect(group.id).to.be.ok;
      currentGroup = (await kcAdminClient.groups.findOne({ id: group.id }))!;
    });

    after(async () => {
      const groupId = currentGroup.id;
      const groups = await kcAdminClient.groups.find({ max: 100 });
      await Promise.all(
        groups.map((_group: GroupRepresentation) => {
          return kcAdminClient.groups.del({ id: _group.id! });
        }),
      );

      const group = await kcAdminClient.groups.findOne({
        id: groupId!,
      });
      expect(group).to.be.null;
    });

    it("add group", async () => {
      let count = (
        await kcAdminClient.users.countGroups({ id: currentUser.id! })
      ).count;
      expect(count).to.eq(0);
      await kcAdminClient.users.addToGroup({
        groupId: currentGroup.id!,
        id: currentUser.id!,
      });
      count = (await kcAdminClient.users.countGroups({ id: currentUser.id! }))
        .count;
      expect(count).to.eq(1);
    });

    it("count groups", async () => {
      let { count } = await kcAdminClient.users.countGroups({
        id: currentUser.id!,
      });
      expect(count).to.eq(1);

      count = (
        await kcAdminClient.users.countGroups({
          id: currentUser.id!,
          search: "cool-group",
        })
      ).count;
      expect(count).to.eq(1);

      count = (
        await kcAdminClient.users.countGroups({
          id: currentUser.id!,
          search: "fake-group",
        })
      ).count;
      expect(count).to.eq(0);
    });

    it("list groups", async () => {
      const groups = await kcAdminClient.users.listGroups({
        id: currentUser.id!,
      });
      expect(groups).to.be.ok;
      expect(groups.length).to.be.eq(1);
      expect(groups[0].name).to.eq("cool-group");
    });

    it("remove group", async () => {
      const newGroup = await kcAdminClient.groups.create({
        name: "test-group",
      });
      await kcAdminClient.users.addToGroup({
        id: currentUser.id!,
        groupId: newGroup.id,
      });
      let count = (
        await kcAdminClient.users.countGroups({ id: currentUser.id! })
      ).count;
      expect(count).to.eq(2);

      try {
        await kcAdminClient.users.delFromGroup({
          id: currentUser.id!,
          groupId: newGroup.id,
        });
      } catch {
        fail("Didn't expect an error when deleting a valid group id");
      }

      count = (await kcAdminClient.users.countGroups({ id: currentUser.id! }))
        .count;
      expect(count).to.equal(1);

      await kcAdminClient.groups.del({ id: newGroup.id });

      // delete a non-existing group should throw an error
      try {
        await kcAdminClient.users.delFromGroup({
          id: currentUser.id!,
          groupId: "fake-group-id",
        });
        fail(
          "Expected an error when deleting a fake id not assigned to the user",
        );
      } catch (e) {
        expect(e).to.be.ok;
      }
    });
  });

  /**
   * Role mappings
   */
  describe("role-mappings", () => {
    before(async () => {
      // create new role
      const roleName = faker.internet.userName();
      await kcAdminClient.roles.create({
        name: roleName,
      });
      const role = await kcAdminClient.roles.findOneByName({
        name: roleName,
      });
      currentRole = role!;
    });

    after(async () => {
      await kcAdminClient.roles.delByName({ name: currentRole.name! });
    });

    it("add a role to user", async () => {
      // add role-mappings with role id
      await kcAdminClient.users.addRealmRoleMappings({
        id: currentUser.id!,

        // at least id and name should appear
        roles: [
          {
            id: currentRole.id!,
            name: currentRole.name!,
          },
        ],
      });
    });

    it("list available role-mappings for user", async () => {
      const roles = await kcAdminClient.users.listAvailableRealmRoleMappings({
        id: currentUser.id!,
      });

      // admin, create-realm
      // not sure why others like offline_access, uma_authorization not included
      expect(roles.length).to.be.least(2);
    });

    it("list role-mappings of user", async () => {
      const res = await kcAdminClient.users.listRoleMappings({
        id: currentUser.id!,
      });

      expect(res).have.all.keys("realmMappings");
    });

    it("list realm role-mappings of user", async () => {
      const roles = await kcAdminClient.users.listRealmRoleMappings({
        id: currentUser.id!,
      });
      // currentRole will have an empty `attributes`, but role-mappings do not
      expect(roles).to.deep.include(omit(currentRole, "attributes"));
    });

    it("list realm composite role-mappings of user", async () => {
      const roles = await kcAdminClient.users.listCompositeRealmRoleMappings({
        id: currentUser.id!,
      });
      // todo: add data integrity check later
      expect(roles).to.be.ok;
    });

    it("del realm role-mappings from user", async () => {
      await kcAdminClient.users.delRealmRoleMappings({
        id: currentUser.id!,
        roles: [
          {
            id: currentRole.id!,
            name: currentRole.name!,
          },
        ],
      });

      const roles = await kcAdminClient.users.listRealmRoleMappings({
        id: currentUser.id!,
      });
      expect(roles).to.not.deep.include(currentRole);
    });
  });

  /**
   * client Role mappings
   */
  describe("client role-mappings", () => {
    before(async () => {
      // create new client
      const clientId = faker.internet.userName();
      await kcAdminClient.clients.create({
        clientId,
      });

      const clients = await kcAdminClient.clients.find({ clientId });
      expect(clients[0]).to.be.ok;
      currentClient = clients[0];

      // create new client role
      const roleName = faker.internet.userName();
      await kcAdminClient.clients.createRole({
        id: currentClient.id,
        name: roleName,
      });

      // assign to currentRole
      currentRole = await kcAdminClient.clients.findRole({
        id: currentClient.id!,
        roleName,
      });
    });

    after(async () => {
      await kcAdminClient.clients.delRole({
        id: currentClient.id!,
        roleName: currentRole.name!,
      });
      await kcAdminClient.clients.del({ id: currentClient.id! });
    });

    it("add a client role to user", async () => {
      // add role-mappings with role id
      await kcAdminClient.users.addClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,

        // at least id and name should appear
        roles: [
          {
            id: currentRole.id!,
            name: currentRole.name!,
          },
        ],
      });
    });

    it("list available client role-mappings for user", async () => {
      const roles = await kcAdminClient.users.listAvailableClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,
      });

      expect(roles).to.be.empty;
    });

    it("list composite client role-mappings for user", async () => {
      const roles = await kcAdminClient.users.listCompositeClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,
      });

      expect(roles).to.be.ok;
    });

    it("list client role-mappings of user", async () => {
      const roles = await kcAdminClient.users.listClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,
      });

      // currentRole will have an empty `attributes`, but role-mappings do not
      expect(currentRole).to.deep.include(roles[0]);
    });

    it("del client role-mappings from user", async () => {
      const roleName = faker.internet.userName();
      await kcAdminClient.clients.createRole({
        id: currentClient.id,
        name: roleName,
      });
      const role = await kcAdminClient.clients.findRole({
        id: currentClient.id!,
        roleName,
      });

      // delete the created role
      await kcAdminClient.users.delClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,
        roles: [
          {
            id: role.id!,
            name: role.name!,
          },
        ],
      });

      // check if mapping is successfully deleted
      const roles = await kcAdminClient.users.listClientRoleMappings({
        id: currentUser.id!,
        clientUniqueId: currentClient.id!,
      });

      // should only left the one we added in the previous test
      expect(roles.length).to.be.eql(1);
    });
  });

  describe("User sessions", () => {
    before(async () => {
      kcAdminClient = new KeycloakAdminClient();
      await kcAdminClient.auth(credentials);

      // create client
      const clientId = faker.internet.userName();
      await kcAdminClient.clients.create({
        clientId,
        consentRequired: true,
      });

      const clients = await kcAdminClient.clients.find({ clientId });
      expect(clients[0]).to.be.ok;
      currentClient = clients[0];
    });

    after(async () => {
      await kcAdminClient.clients.del({
        id: currentClient.id!,
      });
    });

    it("list user sessions", async () => {
      // @TODO: In order to test it, currentUser has to be logged in
      const userSessions = await kcAdminClient.users.listSessions({
        id: currentUser.id!,
      });

      expect(userSessions).to.be.ok;
    });

    it("list users off-line sessions", async () => {
      // @TODO: In order to test it, currentUser has to be logged in
      const userOfflineSessions = await kcAdminClient.users.listOfflineSessions(
        { id: currentUser.id!, clientId: currentClient.id! },
      );

      expect(userOfflineSessions).to.be.ok;
    });

    it("logout user from all sessions", async () => {
      // @TODO: In order to test it, currentUser has to be logged in
      await kcAdminClient.users.logout({ id: currentUser.id! });
    });

    it("list consents granted by the user", async () => {
      const consents = await kcAdminClient.users.listConsents({
        id: currentUser.id!,
      });

      expect(consents).to.be.ok;
    });

    it("revoke consent and offline tokens for particular client", async () => {
      // @TODO: In order to test it, currentUser has to granted consent to client
      const consents = await kcAdminClient.users.listConsents({
        id: currentUser.id!,
      });

      if (consents.length) {
        const consent = consents[0];

        await kcAdminClient.users.revokeConsent({
          id: currentUser.id!,
          clientId: consent.clientId!,
        });
      }
    });

    it("impersonate user", async () => {
      const result = await kcAdminClient.users.impersonation(
        { id: currentUser.id! },
        { user: currentUser.id!, realm: kcAdminClient.realmName },
      );
      expect(result).to.be.ok;
      await kcAdminClient.auth(credentials);
    });
  });

  describe("Federated Identity user integration", () => {
    before(async () => {
      kcAdminClient = new KeycloakAdminClient();
      await kcAdminClient.auth(credentials);

      federatedIdentity = {
        identityProvider: "foobar",
        userId: "userid1",
        userName: "username1",
      };
    });

    it("should list user federated identities and expect empty", async () => {
      const federatedIdentities =
        await kcAdminClient.users.listFederatedIdentities({
          id: currentUser.id!,
        });
      expect(federatedIdentities).to.be.eql([]);
    });

    it("should add federated identity to user", async () => {
      await kcAdminClient.users.addToFederatedIdentity({
        id: currentUser.id!,
        federatedIdentityId: "foobar",
        federatedIdentity,
      });

      // @TODO: In order to test the integration with federated identities, the User Federation
      // would need to be created first, this is not implemented yet.
      // const federatedIdentities = await kcAdminClient.users.listFederatedIdentities({
      //   id: currentUser.id,
      // });
      // expect(federatedIdentities[0]).to.be.eql(federatedIdentity);
    });

    it("should remove federated identity from user", async () => {
      await kcAdminClient.users.delFromFederatedIdentity({
        id: currentUser.id!,
        federatedIdentityId: "foobar",
      });

      const federatedIdentities =
        await kcAdminClient.users.listFederatedIdentities({
          id: currentUser.id!,
        });
      expect(federatedIdentities).to.be.eql([]);
    });
  });
});
