/*
 * Copyright (c) 2012-2021 Red Hat, Inc.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.api.user.server;

import static io.restassured.RestAssured.given;
import static java.util.Collections.emptyList;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.testng.Assert.assertEquals;

import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.restassured.response.Response;
import java.util.Map;
import org.eclipse.che.account.api.AccountManager;
import org.eclipse.che.account.spi.AccountValidator;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.model.user.User;
import org.eclipse.che.api.core.rest.ApiExceptionMapper;
import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
import org.eclipse.che.api.user.server.model.impl.UserImpl;
import org.eclipse.che.api.user.shared.dto.UserDto;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.eclipse.che.dto.server.DtoFactory;
import org.everrest.assured.EverrestJetty;
import org.everrest.core.Filter;
import org.everrest.core.GenericContainerRequest;
import org.everrest.core.RequestFilter;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

/**
 * Tests for {@link UserService}
 *
 * @author Eugene Veovodin
 * @author Max Shaposhnik
 */
@Listeners({EverrestJetty.class, MockitoTestNGListener.class})
public class UserServiceTest {
  @SuppressWarnings("unused")
  private static final ApiExceptionMapper MAPPER = new ApiExceptionMapper();

  @SuppressWarnings("unused")
  private static final EnvironmentFilter FILTER = new EnvironmentFilter();

  private static final Subject SUBJECT = new SubjectImpl("user", "user123", "token", false);

  @Mock(answer = Answers.RETURNS_MOCKS)
  private UserManager userManager;

  @Mock private AccountManager accountManager;
  @Mock private TokenValidator tokenValidator;
  @Mock private UserLinksInjector linksInjector;
  private UserValidator userValidator;
  @Captor private ArgumentCaptor<User> userCaptor;
  private UserService userService;

  @BeforeMethod
  public void initService() {
    initMocks(this);

    userValidator = new UserValidator(new AccountValidator(accountManager));

    // Return the incoming instance when injectLinks is called
    lenient()
        .when(linksInjector.injectLinks(any(), any()))
        .thenAnswer(inv -> inv.getArguments()[0]);

    userService = new UserService(userManager, tokenValidator, userValidator, linksInjector, true);
  }

  @Test
  public void shouldCreateUserFromToken() throws Exception {
    when(tokenValidator.validateToken("token_value"))
        .thenReturn(new UserImpl("id", "test@eclipse.org", "test"));

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .contentType("application/json")
            .post(SECURE_PATH + "/user?token=token_value");

    assertEquals(response.statusCode(), 201);
    verify(userManager).create(userCaptor.capture(), anyBoolean());
    final User user = userCaptor.getValue();
    assertEquals(user.getEmail(), "test@eclipse.org");
    assertEquals(user.getName(), "test");
  }

  @Test
  public void shouldCreateUserFromEntity() throws Exception {
    final UserDto newUser =
        newDto(UserDto.class)
            .withName("test")
            .withEmail("test@codenvy.com")
            .withPassword("password12345");
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .body(newUser)
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 201);
    verify(userManager).create(userCaptor.capture(), anyBoolean());
    final User user = userCaptor.getValue();
    assertEquals(user.getEmail(), "test@codenvy.com");
    assertEquals(user.getName(), "test");
    assertEquals(user.getPassword(), "password12345");
  }

  @Test
  public void shouldNotCreateUserFromEntityWhenPasswordIsNotValid() throws Exception {
    final UserDto newUser =
        newDto(UserDto.class).withName("test").withEmail("test@codenvy.com").withPassword("1");
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .body(newUser)
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 400);
    assertEquals(unwrapError(response), "Password should contain at least 8 characters");
  }

  @Test
  public void shouldNotCreateUserIfTokenIsNotValid() throws Exception {
    when(tokenValidator.validateToken("token_value")).thenThrow(new ConflictException("error"));

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .contentType("application/json")
            .post(SECURE_PATH + "/user?token=token_value");

    assertEquals(response.statusCode(), 409);
    assertEquals(unwrapError(response), "error");
  }

  @Test
  public void shouldNotCreateUserFromEntityIfEntityIsNull() throws Exception {
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 400);
    assertEquals(unwrapError(response), "User required");
  }

  @Test
  public void shouldNotCreateUserFromEntityIfEmailIsNull() throws Exception {
    final UserDto newUser = newDto(UserDto.class).withName("test").withPassword("password12345");
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .body(newUser)
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 400);
    assertEquals(unwrapError(response), "User email required");
  }

  @Test
  public void shouldNotCreateUserFromEntityIfNameIsNull() throws Exception {
    final UserDto newUser =
        newDto(UserDto.class).withEmail("test@codenvy.com").withPassword("password12345");
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .body(newUser)
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 400);
    assertEquals(unwrapError(response), "User name required");
  }

  @Test
  public void shouldNotCreateUserFromEntityIfPasswordIsNotValid() throws Exception {
    final UserDto newUser =
        newDto(UserDto.class).withEmail("test@codenvy.com").withName("test").withPassword("1");
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .body(newUser)
            .contentType("application/json")
            .post(SECURE_PATH + "/user");

    assertEquals(response.statusCode(), 400);
    assertEquals(unwrapError(response), "Password should contain at least 8 characters");
  }

  @Test
  public void shouldBeAbleToGetCurrentUser() throws Exception {
    when(userManager.getById(SUBJECT.getUserId())).thenReturn(copySubject());

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user");

    assertEquals(response.getStatusCode(), 200);
  }

  @Test
  public void shouldBeAbleToGetUserById() throws Exception {
    final UserImpl testUser = copySubject();
    when(userManager.getById(SUBJECT.getUserId())).thenReturn(testUser);

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user/" + SUBJECT.getUserId());

    assertEquals(response.getStatusCode(), 200);
    final UserDto fetchedUser = unwrapDto(response, UserDto.class);
    assertEquals(fetchedUser.getId(), testUser.getId());
    assertEquals(fetchedUser.getName(), testUser.getName());
    assertEquals(fetchedUser.getEmail(), testUser.getEmail());
  }

  @Test
  public void shouldBeAbleToFindUserByEmail() throws Exception {
    final UserImpl testUser = copySubject();
    when(userManager.getByEmail(testUser.getEmail())).thenReturn(testUser);

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user/find?email=" + testUser.getEmail());

    assertEquals(response.getStatusCode(), 200);
    final UserDto fetchedUser = unwrapDto(response, UserDto.class);
    assertEquals(fetchedUser.getId(), testUser.getId());
    assertEquals(fetchedUser.getName(), testUser.getName());
    assertEquals(fetchedUser.getEmail(), testUser.getEmail());
  }

  @Test
  public void shouldBeAbleToFindUserByName() throws Exception {
    final UserImpl testUser = copySubject();
    when(userManager.getByName(testUser.getName())).thenReturn(testUser);

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user/find?name=" + testUser.getName());

    assertEquals(response.getStatusCode(), 200);
    final UserDto fetchedUser = unwrapDto(response, UserDto.class);
    assertEquals(fetchedUser.getId(), testUser.getId());
    assertEquals(fetchedUser.getName(), testUser.getName());
    assertEquals(fetchedUser.getEmail(), testUser.getEmail());
  }

  @Test
  public void shouldNotFindUserByNameOrEmailWhenBothSpecified() {
    final UserImpl testUser = copySubject();

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(
                SECURE_PATH
                    + "/user/find?"
                    + "name="
                    + testUser.getName()
                    + "&email="
                    + testUser.getEmail());

    assertEquals(response.getStatusCode(), 400);
    assertEquals(
        unwrapError(response), "Expected either user's email or name, while both values received");
  }

  @Test
  public void shouldNotFindUserByNameOrEmailWhenBothAreEmpty() {

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user/find");

    assertEquals(response.getStatusCode(), 400);
    assertEquals(unwrapError(response), "Missed user's email or name");
  }

  @Test
  public void shouldUpdatePassword() throws Exception {
    final UserImpl testUser = copySubject();
    when(userManager.getById(testUser.getId())).thenReturn(testUser);

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .contentType("application/x-www-form-urlencoded")
            .body("password=password12345")
            .when()
            .post(SECURE_PATH + "/user/password");

    assertEquals(response.getStatusCode(), 204);
    verify(userManager).update(userCaptor.capture());
    final User fetchedUser = userCaptor.getValue();
    assertEquals(fetchedUser.getPassword(), "password12345");
  }

  @Test
  public void shouldNotUpdatePasswordIfPasswordContainsOnlyDigits() {

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .contentType("application/x-www-form-urlencoded")
            .body("password=1234567890")
            .when()
            .post(SECURE_PATH + "/user/password");

    assertEquals(response.getStatusCode(), 400);
    assertEquals(unwrapError(response), "Password should contain letters and digits");
  }

  @Test
  public void shouldNotUpdatePasswordIfPasswordContainsLessThan8Chars() {

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .contentType("application/x-www-form-urlencoded")
            .body("password=0xf")
            .when()
            .post(SECURE_PATH + "/user/password");

    assertEquals(response.getStatusCode(), 400);
    assertEquals(unwrapError(response), "Password should contain at least 8 characters");
  }

  @Test
  public void shouldNotUpdatePasswordIfPasswordIsNull() {

    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .contentType("application/x-www-form-urlencoded")
            .when()
            .post(SECURE_PATH + "/user/password");

    assertEquals(response.getStatusCode(), 400);
    assertEquals(unwrapError(response), "Password required");
  }

  @Test
  public void shouldRemoveUser() throws Exception {
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .delete(SECURE_PATH + "/user/" + SUBJECT.getUserId());

    assertEquals(response.getStatusCode(), 204);
    verify(userManager).remove(SUBJECT.getUserId());
  }

  @Test
  public void shouldBeAbleToGetSettings() throws Exception {
    final Response response =
        given()
            .auth()
            .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
            .when()
            .get(SECURE_PATH + "/user/settings");

    assertEquals(response.getStatusCode(), 200);
    final Map<String, String> settings =
        new Gson().fromJson(response.print(), new TypeToken<Map<String, String>>() {}.getType());
    assertEquals(settings, ImmutableMap.of("che.auth.user_self_creation", "true"));
  }

  @Filter
  public static class EnvironmentFilter implements RequestFilter {

    public void doFilter(GenericContainerRequest request) {
      EnvironmentContext.getCurrent().setSubject(SUBJECT);
    }
  }

  private static <T> T unwrapDto(Response response, Class<T> dtoClass) {
    return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
  }

  private static String unwrapError(Response response) {
    return unwrapDto(response, ServiceError.class).getMessage();
  }

  private static UserImpl copySubject() {
    return new UserImpl(
        SUBJECT.getUserId(),
        SUBJECT.getUserName() + "@codenvy.com",
        SUBJECT.getUserName(),
        null,
        emptyList());
  }
}
