/*
 * Copyright 2017-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.opendevstack.provision.controller;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.hamcrest.CoreMatchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.verification.VerificationMode;
import org.opendevstack.provision.SpringBoot;
import org.opendevstack.provision.adapter.IBugtrackerAdapter;
import org.opendevstack.provision.adapter.ICollaborationAdapter;
import org.opendevstack.provision.adapter.IJobExecutionAdapter;
import org.opendevstack.provision.adapter.ISCMAdapter;
import org.opendevstack.provision.adapter.ISCMAdapter.URL_TYPE;
import org.opendevstack.provision.adapter.exception.AdapterException;
import org.opendevstack.provision.adapter.exception.CreateProjectPreconditionException;
import org.opendevstack.provision.model.OpenProjectData;
import org.opendevstack.provision.model.ProjectData;
import org.opendevstack.provision.services.*;
import org.opendevstack.provision.storage.IStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

/**
 * @author Torsten Jaeschke
 * @author utschig
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBoot.class)
@DirtiesContext
@ActiveProfiles("crowd")
public class ProjectApiControllerTest {

  @Mock private IBugtrackerAdapter jiraAdapter;
  @Mock private ICollaborationAdapter confluenceAdapter;
  @Mock private ISCMAdapter bitbucketAdapter;
  @Mock private IJobExecutionAdapter jenkinsPipelineAdapter;
  @Mock private MailAdapter mailAdapter;
  @Mock private IStorage storage;
  @Mock private StorageAdapter filteredStorage;

  @Mock private CrowdProjectIdentityMgmtAdapter idm;

  @InjectMocks @Autowired private ProjectApiController apiController;

  private MockMvc mockMvc;

  private OpenProjectData data;

  @Value("${project.template.key.names}")
  private String projectKeys;

  @Autowired private List<String> projectTemplateKeyNames;

  @Autowired private JiraAdapter realJiraAdapter;

  @Autowired ConfluenceAdapter realConfluenceAdapter;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    mockMvc = MockMvcBuilders.standaloneSetup(apiController).build();
    initOpenProjectData();
    when(jiraAdapter.isSpecialPermissionSchemeEnabled()).thenReturn(true);

    apiController.setCheckPreconditionsEnabled(true);

    // Reset status of api controller for each test
    apiController.setConfluenceAdapterEnable(true);
  }

  private void initOpenProjectData() {
    data = new OpenProjectData();
    data.projectKey = "KEY";
    data.projectName = "Name";
    data.description = "Description";

    Map<String, String> someQuickstarter = new HashMap<>();
    someQuickstarter.put("key", "value");
    List<Map<String, String>> quickstarters = new ArrayList<>();
    quickstarters.add(someQuickstarter);
    data.quickstarters = quickstarters;

    data.platformRuntime = false;
    data.specialPermissionSet = true;
    data.projectAdminUser = "clemens";
    data.projectAdminGroup = "group";
    data.projectUserGroup = "group";
    data.projectReadonlyGroup = "group";
  }

  @Test
  public void addProjectWithoutOCPosNeg() throws Exception {

    OpenProjectData bugTrackProject = copyFromProject(data);
    bugTrackProject.bugtrackerUrl = "bugtracker";

    String collaborationSpaceURL = "collspace";
    bugTrackProject.collaborationSpaceUrl = collaborationSpaceURL;

    when(jiraAdapter.createBugtrackerProjectForODSProject(isNotNull())).thenReturn(bugTrackProject);
    when(confluenceAdapter.createCollaborationSpaceForODSProject(isNotNull()))
        .thenReturn(collaborationSpaceURL);
    Mockito.doNothing().when(mailAdapter).notifyUsersAboutProject(data);
    when(storage.storeProject(data)).thenReturn("created");

    Mockito.doNothing().when(idm).validateIdSettingsOfProject(data);

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());

    Mockito.verify(jenkinsPipelineAdapter, never()).createPlatformProjects(isNotNull());
    Mockito.verify(bitbucketAdapter, never()).checkCreateProjectPreconditions(isNotNull());
    Mockito.verify(bitbucketAdapter, never()).createSCMProjectForODSProject(isNotNull());

    // try with failing storage
    when(storage.storeProject(data)).thenThrow(IOException.class);
    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is5xxServerError());
  }

  @Test
  public void addProjectWithOC() throws Exception {

    List.of(true, false)
        .forEach(
            confluenceAdapterEnabled -> {
              try {
                apiController.setConfluenceAdapterEnable(confluenceAdapterEnabled);

                data.platformRuntime = true;
                data.quickstarters = null;

                OpenProjectData bugTrackProject = copyFromProject(data);
                String bugtrackerUrl = "bugtracker";
                bugTrackProject.bugtrackerUrl = bugtrackerUrl;
                String collaborationSpaceURL = "collspace";
                bugTrackProject.collaborationSpaceUrl = collaborationSpaceURL;

                when(jiraAdapter.createBugtrackerProjectForODSProject(isNotNull()))
                    .thenReturn(bugTrackProject);
                when(confluenceAdapter.createCollaborationSpaceForODSProject(isNotNull()))
                    .thenReturn(collaborationSpaceURL);

                OpenProjectData projectSCM = copyFromProject(bugTrackProject);

                projectSCM.scmvcsUrl = "scmspace";

                Map<String, Map<URL_TYPE, String>> repos = new HashMap<>();

                when(bitbucketAdapter.createSCMProjectForODSProject(isNotNull()))
                    .thenReturn(projectSCM.scmvcsUrl);
                when(bitbucketAdapter.createComponentRepositoriesForODSProject(isNotNull()))
                    .thenReturn(repos);
                when(bitbucketAdapter.createAuxiliaryRepositoriesForODSProject(
                        isNotNull(), isNotNull()))
                    .thenReturn(repos);
                when(jenkinsPipelineAdapter.createPlatformProjects(isNotNull())).thenReturn(data);
                Mockito.doNothing().when(mailAdapter).notifyUsersAboutProject(data);
                when(storage.storeProject(data)).thenReturn("created");

                Mockito.doNothing().when(idm).validateIdSettingsOfProject(data);

                mockMvc
                    .perform(
                        post("/api/v2/project")
                            .content(asJsonString(data))
                            .contentType(MediaType.APPLICATION_JSON)
                            .accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());

                verifyAddProjectAdapterCalls(times(1), times(confluenceAdapterEnabled ? 1 : 0));
              } catch (Exception e) {
                throw new RuntimeException(e);
              }
            });
  }

  @Test
  public void whenOnyCheckPreconditionsThenDoNotCreateProject() throws Exception {

    final AtomicBoolean initial = new AtomicBoolean(Boolean.TRUE);

    List.of(Boolean.TRUE, Boolean.FALSE, Boolean.FALSE)
        .forEach(
            value -> {

              // Only check preconditions
              try {
                OpenProjectData projectData = copyFromProject(data);

                // First run platformRuntime and bugtrackerSpace are equal true
                // 2 and 3 run, either one is true the other is false
                projectData.platformRuntime = initial.get() || value;
                projectData.bugtrackerSpace = !projectData.platformRuntime || value;
                initial.set(projectData.bugtrackerSpace);

                mockMvc
                    .perform(
                        post("/api/v2/project")
                            .content(asJsonString(projectData))
                            .param(
                                ProjectApiController
                                    .ADD_PROJECT_PARAM_NAME_ONLY_CHECK_PRECONDITIONS,
                                "true")
                            .contentType(MediaType.APPLICATION_JSON)
                            .accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(
                        MockMvcResultMatchers.content()
                            .string(
                                "{\"endpoint\":\"ADD_PROJECT\",\"stage\":\"CHECK_PRECONDITIONS\",\"status\":\"COMPLETED_SUCCESSFULLY\"}"))
                    .andDo(MockMvcResultHandlers.print());

              } catch (Exception e) {
                throw new RuntimeException(e);
              }
            });

    // each adapter should be called 2 times
    verifyCheckPreconditionsCalls(times(2), times(2));
  }

  public void verifyCheckPreconditionsCalls(VerificationMode platform, VerificationMode bugtracker)
      throws CreateProjectPreconditionException, IOException {

    //    VerificationMode platform = platformProject ? times(1) : times(0);
    //    VerificationMode bugtracker = bugtrackerProject ? times(1) : times(0);

    Mockito.verify(bitbucketAdapter, platform).checkCreateProjectPreconditions(isNotNull());
    // jira components
    Mockito.verify(jiraAdapter, bugtracker).checkCreateProjectPreconditions(isNotNull());
    Mockito.verify(confluenceAdapter, bugtracker).checkCreateProjectPreconditions(isNotNull());

    Mockito.verify(jenkinsPipelineAdapter, never()).createPlatformProjects(isNotNull());
    Mockito.verify(bitbucketAdapter, never()).createSCMProjectForODSProject(isNotNull());

    //    reset(bitbucketAdapter, jiraAdapter, confluenceAdapter);
  }

  public void verifyAddProjectAdapterCalls(VerificationMode times)
      throws IOException, CreateProjectPreconditionException {
    verifyAddProjectAdapterCalls(times, times);
  }

  public void verifyAddProjectAdapterCalls(VerificationMode times, VerificationMode confluenceTimes)
      throws IOException, CreateProjectPreconditionException {
    Mockito.verify(jenkinsPipelineAdapter, times).createPlatformProjects(isNotNull());
    // check preconditions should be always called
    Mockito.verify(bitbucketAdapter, times(1)).checkCreateProjectPreconditions(isNotNull());
    Mockito.verify(bitbucketAdapter, times).createSCMProjectForODSProject(isNotNull());
    Mockito.verify(bitbucketAdapter, times).createComponentRepositoriesForODSProject(isNotNull());
    // jira components
    Mockito.verify(jiraAdapter, times)
        .createComponentsForProjectRepositories(isNotNull(), isNotNull());

    Mockito.clearInvocations(
        jiraAdapter, confluenceAdapter, bitbucketAdapter, jenkinsPipelineAdapter);
  }

  @Test
  public void whenCheckPreconditionsBadParamThenBadRequest() throws Exception {

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .param(
                    ProjectApiController.ADD_PROJECT_PARAM_NAME_ONLY_CHECK_PRECONDITIONS,
                    "wrong-value")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isBadRequest())
        .andDo(MockMvcResultHandlers.print());

    // ensure that cleanup was NOT called
    Mockito.verify(jiraAdapter, times(0)).cleanup(isNotNull(), isNotNull());
  }

  @Test
  public void whenCheckPreconditionsFailThenReturnErrorInResponseBody() throws Exception {

    data.platformRuntime = true;

    List<CheckPreconditionFailure> preconditionFailures = new ArrayList<>();
    preconditionFailures.add(CheckPreconditionFailure.getUnexistantUserInstance("failure1"));
    preconditionFailures.add(CheckPreconditionFailure.getUnexistantUserInstance("failure2"));

    StringBuffer expectedBody = new StringBuffer();
    expectedBody
        .append(
            CheckPreconditionsResponse.JobStage.CHECK_PRECONDITIONS
                + CheckPreconditionsResponse.KEY_VALUE_SEPARATOR
                + CheckPreconditionsResponse.JobStatus.FAILED)
        .append(System.lineSeparator());
    expectedBody
        .append(
            CheckPreconditionsResponse.JobStage.CHECK_PRECONDITIONS
                + CheckPreconditionsResponse.KEY_VALUE_SEPARATOR
                + String.join(
                    CheckPreconditionsResponse.ERRORS_DELIMITER,
                    Arrays.toString(preconditionFailures.toArray())))
        .append(System.lineSeparator());

    when(bitbucketAdapter.checkCreateProjectPreconditions(isNotNull()))
        .thenReturn(preconditionFailures);

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is(HttpStatus.SERVICE_UNAVAILABLE.value()))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    "{\"endpoint\":\"ADD_PROJECT\",\"stage\":\"CHECK_PRECONDITIONS\",\"status\":\"FAILED\",\"errors\":[{\"error-code\":\"UNEXISTANT_USER\",\"error-message\":\"failure1\"},{\"error-code\":\"UNEXISTANT_USER\",\"error-message\":\"failure2\"}]}"));

    verifyAddProjectAdapterCalls(times(0));

    // ensure that cleanup was NOT called
    Mockito.verify(jiraAdapter, times(0)).cleanup(isNotNull(), isNotNull());
  }

  @Test
  public void whenCheckPreconditionsThrowsExceptionThenReturnServerError() throws Exception {

    data.platformRuntime = true;

    String errorMessage = "thrown in unit test";
    AdapterException thrownInTest = new AdapterException(new RuntimeException(errorMessage));
    when(bitbucketAdapter.checkCreateProjectPreconditions(isNotNull()))
        .thenThrow(
            new CreateProjectPreconditionException("bitbucket", data.projectKey, thrownInTest));

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is(HttpStatus.SERVICE_UNAVAILABLE.value()))
        .andExpect(
            MockMvcResultMatchers.header()
                .string(
                    ProjectApiController.RETRY_AFTER_HEADER,
                    ProjectApiController.RETRY_AFTER_INTERVAL_IN_SECONDS))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(CoreMatchers.containsString("\"error-code\":\"EXCEPTION\"")));

    verifyAddProjectAdapterCalls(times(0));

    // ensure that cleanup was NOT called
    Mockito.verify(jiraAdapter, times(0)).cleanup(isNotNull(), isNotNull());
  }

  @Test
  public void addProjectAgainstExistingOne() throws Exception {

    when(storage.getProject(data.projectKey)).thenReturn(data);

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is5xxServerError())
        .andDo(MockMvcResultHandlers.print())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    CoreMatchers.containsString(
                        "("
                            + data.projectKey
                            + ") or name "
                            + "("
                            + data.projectName
                            + ") already exists")));

    // ensure that cleanup was NOT called
    Mockito.verify(jiraAdapter, times(0)).cleanup(isNotNull(), isNotNull());
  }

  @Test
  public void addProjectEmptyAndBadRequest() throws Exception {

    mockMvc
        .perform(
            post("/api/v2/project")
                .content("{}")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isBadRequest())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void addProjectNullAnd4xxClientResult() throws Exception {
    mockMvc
        .perform(
            post("/api/v2/project")
                .content("")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is4xxClientError())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void addProjectKeyOnlyAndExpectBadRequest() throws Exception {
    OpenProjectData data = new OpenProjectData();
    data.projectKey = "KEY";
    data.platformRuntime = true;

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isBadRequest())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void addProjectInLegacyFormatErrorsOut() throws Exception {
    ProjectData oldLegacyData = new ProjectData();
    oldLegacyData.name = "abcName";
    oldLegacyData.key = "abcKey";

    // new endpoint - old format, fail!
    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(oldLegacyData))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isBadRequest());

    // wrong (old) endpoint
    mockMvc
        .perform(
            post("/api/v1/project")
                .content(asJsonString(oldLegacyData))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isNotFound());
  }

  @Test
  public void validateProjectWithProjectExists() throws Exception {
    when(jiraAdapter.projectKeyExists(isNotNull(String.class))).thenReturn(true);

    mockMvc
        .perform(
            get("/api/v2/project/validate")
                .param("projectName", "project")
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isNotAcceptable())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void validateProjectWithProjectNotExists() throws Exception {
    when(jiraAdapter.projectKeyExists(isNotNull(String.class))).thenReturn(false);

    mockMvc
        .perform(
            get("/api/v2/project/validate")
                .param("projectName", "project")
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void validateKeyWithKeyExists() throws Exception {
    when(jiraAdapter.projectKeyExists(isNotNull(String.class))).thenReturn(true);
    mockMvc
        .perform(
            get("/api/v2/project/key/validate")
                .param("projectKey", "project")
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isNotAcceptable())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void validateKeyWithKeyNotExists() throws Exception {
    when(jiraAdapter.projectKeyExists(isNotNull(String.class))).thenReturn(false);

    mockMvc
        .perform(
            get("/api/v2/project/key/validate")
                .param("projectKey", "project")
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void generateKey() throws Exception {
    when(jiraAdapter.buildProjectKey(isNotNull(String.class))).thenReturn("PROJ");

    mockMvc
        .perform(
            get("/api/v2/project/key/generate")
                .param("name", "project")
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void givenGetAllProjects_whenProjectFound_thenOK() throws Exception {

    Map<String, OpenProjectData> projects = new HashMap<>();

    projects.put(data.projectKey, data);

    OpenProjectData copy = copyFromProject(data);
    copy.projectKey = copy.projectKey + 2;
    projects.put(copy.projectKey, copy);

    when(filteredStorage.getProjects()).thenReturn(projects);

    ResultActions resultActions =
        mockMvc
            .perform(get("/api/v2/project").accept(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print());

    String content = resultActions.andReturn().getResponse().getContentAsString();

    ObjectMapper mapper = new ObjectMapper();

    JsonNode jsonNode = mapper.readTree(content);

    assertEquals(2, jsonNode.size());

    JsonNode node = jsonNode.findValue(copy.projectKey);

    assertEquals(copy.projectKey, node.get("projectKey").asText());
  }

  @Test
  public void givenGetAllProjects_whenException_thenInternalServerErrorResponse() throws Exception {

    when(filteredStorage.getProjects()).thenReturn(null);

    mockMvc
        .perform(get("/api/v2/project").accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is5xxServerError())
        .andExpect(MockMvcResultMatchers.content().string(""))
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void getProject() throws Exception {
    // arbitrary number
    mockMvc
        .perform(get("/api/v2/project/1").accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isNotFound())
        .andDo(MockMvcResultHandlers.print());

    data.projectKey = "1";

    when(filteredStorage.getFilteredSingleProject("1")).thenReturn(data);

    mockMvc
        .perform(get("/api/v2/project/1").accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());
  }

  @Test
  public void getProjectTemplateKeys() throws Exception {

    BiConsumer<MediaType, String> consumer =
        (mediaType, responseBody) -> {
          try {
            mockMvc
                .perform(get("/api/v2/project/templates").accept(mediaType))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(
                    MockMvcResultMatchers.content()
                        .string(CoreMatchers.containsString(responseBody)))
                .andDo(MockMvcResultHandlers.print());
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        };

    consumer.accept(
        MediaType.TEXT_PLAIN,
        ProjectTemplateType.createProjectTemplateKeysAsString(projectTemplateKeyNames));

    consumer.accept(
        MediaType.APPLICATION_JSON,
        ProjectApiController.createProjectTemplateKeysJson(projectTemplateKeyNames));

    consumer =
        (mediaType, responseBody) -> {
          try {
            mockMvc
                .perform(get("/api/v2/project/templates").accept(mediaType))
                .andExpect(MockMvcResultMatchers.status().isUnsupportedMediaType())
                .andExpect(
                    MockMvcResultMatchers.content()
                        .string(CoreMatchers.containsString(responseBody)))
                .andDo(MockMvcResultHandlers.print());
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        };

    ObjectMapper mapper = new ObjectMapper();
    String body =
        mapper.writeValueAsString(
            Map.of("ERROR:", "Unsupported accept type: " + MediaType.APPLICATION_OCTET_STREAM));

    consumer.accept(MediaType.APPLICATION_OCTET_STREAM, body);
  }

  @Test
  public void getProjectTypeTemplatesForKey() throws Exception {
    apiController.jiraAdapter = realJiraAdapter;
    apiController.setConfluenceAdapter(realConfluenceAdapter);

    mockMvc
        .perform(get("/api/v2/project/template/default").accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    CoreMatchers.containsString(
                        "bugTrackerTemplate\":\"software#com.pyxis.greenhopper.jira:gh-scrum-template\"")))
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    CoreMatchers.containsString(
                        "collabSpaceTemplate\":\"com.atlassian.confluence.plugins.confluence-space-blueprints:documentation-space-blueprint")));

    mockMvc
        .perform(get("/api/v2/project/template/nonExistant").accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    CoreMatchers.containsString(
                        "bugTrackerTemplate\":\"software#com.pyxis.greenhopper.jira:gh-scrum-template\"")))
        .andExpect(
            MockMvcResultMatchers.content()
                .string(
                    CoreMatchers.containsString(
                        "collabSpaceTemplate\":\"com.atlassian.confluence.plugins.confluence-space-blueprints:documentation-space-blueprint")));

    mockMvc
        .perform(get("/api/v2/project/template/").accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().is4xxClientError());
  }

  @Test
  public void updateProjectWithAndWithoutOC() throws Exception {
    data.platformRuntime = false;
    data.quickstarters = null;

    OpenProjectData bugTrackProject = copyFromProject(data);
    String collaborationSpaceURL = "collspace";
    bugTrackProject.collaborationSpaceUrl = collaborationSpaceURL;

    Map<String, Map<URL_TYPE, String>> repos = new HashMap<>();

    when(jiraAdapter.createBugtrackerProjectForODSProject(isNotNull())).thenReturn(data);
    when(confluenceAdapter.createCollaborationSpaceForODSProject(isNotNull()))
        .thenReturn(collaborationSpaceURL);

    OpenProjectData projectSCM = copyFromProject(data);

    projectSCM.scmvcsUrl = "scmspace";

    when(bitbucketAdapter.createSCMProjectForODSProject(isNotNull()))
        .thenReturn(projectSCM.scmvcsUrl);
    when(bitbucketAdapter.createComponentRepositoriesForODSProject(isNotNull())).thenReturn(repos);
    when(bitbucketAdapter.createAuxiliaryRepositoriesForODSProject(isNotNull(), isNotNull()))
        .thenReturn(repos);
    when(jenkinsPipelineAdapter.createPlatformProjects(isNotNull())).thenReturn(data);
    Mockito.doNothing().when(mailAdapter).notifyUsersAboutProject(data);
    when(storage.storeProject(data)).thenReturn("created");

    // not existing
    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isNotFound());

    // existing - store prior
    when(storage.getProject(anyString())).thenReturn(data);

    // upgrade to OC - with minimal set
    OpenProjectData upgrade = new OpenProjectData();
    upgrade.projectKey = data.projectKey;
    upgrade.platformRuntime = true;
    apiController.ocUpgradeAllowed = true;

    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(upgrade))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());

    Mockito.verify(jenkinsPipelineAdapter, times(1)).createPlatformProjects(isNotNull());
    Mockito.verify(bitbucketAdapter, times(1)).createSCMProjectForODSProject(isNotNull());

    // upgrade to OC with upgrade forbidden
    data.platformRuntime = false;
    when(storage.getProject(anyString())).thenReturn(data);
    upgrade.platformRuntime = true;

    apiController.ocUpgradeAllowed = false;
    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(upgrade))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().is5xxServerError())
        .andExpect(MockMvcResultMatchers.content().string(CoreMatchers.containsString("upgrade")))
        .andDo(MockMvcResultHandlers.print());

    // now w/o upgrade
    upgrade.platformRuntime = false;

    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());

    Mockito.verify(jenkinsPipelineAdapter).createPlatformProjects(isNotNull());
    Mockito.verify(bitbucketAdapter).createSCMProjectForODSProject(isNotNull());

    // now w/o upgrade
    data.platformRuntime = true;
    upgrade.platformRuntime = false;

    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());

    Mockito.verify(bitbucketAdapter, times(2)).createSCMProjectForODSProject(isNotNull());
  }

  @Test
  public void updateProjectWithQSAdditionOnly() throws Exception {
    // allow upgrade
    apiController.ocUpgradeAllowed = true;

    data.platformRuntime = false;
    data.quickstarters = null;

    // existing - store prior
    when(storage.getProject(anyString())).thenReturn(data);

    // upgrade to OC - based on a quickstarter
    OpenProjectData upgrade = new OpenProjectData();
    upgrade.projectKey = data.projectKey;
    upgrade.platformRuntime = false;

    Map<String, String> newQS = new HashMap<>();
    newQS.put("component_type", "someComponentType");
    newQS.put("component_id", "someComponentName");

    upgrade.quickstarters = new ArrayList<>();
    upgrade.quickstarters.add(newQS);

    // this will error out - because of the test mock, but the key is scm project creation
    mockMvc
        .perform(
            put("/api/v2/project")
                .content(asJsonString(upgrade))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andDo(MockMvcResultHandlers.print());

    Mockito.verify(bitbucketAdapter).createSCMProjectForODSProject(isNotNull());
  }

  @Test
  public void updateProjectWithValidAndInvalidComponentId() throws Exception {

    // allow upgrade
    apiController.ocUpgradeAllowed = true;

    data.platformRuntime = false;
    data.quickstarters = null;

    // existing - store prior
    when(storage.getProject(anyString())).thenReturn(data);

    // upgrade to OC - based on a quickstarter
    OpenProjectData upgrade = new OpenProjectData();
    upgrade.projectKey = data.projectKey;
    upgrade.platformRuntime = false;

    BiConsumer<String, Boolean> request =
        (componentId, successful) -> {
          Map<String, String> newQS = new HashMap<>();
          newQS.put("component_type", "someComponentType");
          newQS.put("component_id", componentId);

          upgrade.quickstarters = new ArrayList<>();
          upgrade.quickstarters.add(newQS);

          // this will error out - because of the test mock, but the key is scm project creation
          try {
            mockMvc
                .perform(
                    put("/api/v2/project")
                        .content(asJsonString(upgrade))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(
                    successful.equals(Boolean.FALSE)
                        ? MockMvcResultMatchers.status().isBadRequest()
                        : MockMvcResultMatchers.status().is2xxSuccessful());
          } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
          }
        };

    String tooShort = "ad";
    String tooLong = "1234567890123456789012345678901234567890addd";
    Arrays.asList(".-adfasfdasdfasdfsad", tooShort, tooLong, "", null).stream()
        .forEach(s -> request.accept(s, false));
  }

  @Test
  public void testProjectDescLengh() throws Exception {
    data.description =
        "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890STOPHERE";

    OpenProjectData dataReturn = this.copyFromProject(data);
    dataReturn.description = ProjectApiController.createShortenedDescription(dataReturn);

    dataReturn.bugtrackerUrl = "bugtracker";

    String collaborationSpaceURL = "collspace";
    dataReturn.collaborationSpaceUrl = collaborationSpaceURL;

    when(jiraAdapter.createBugtrackerProjectForODSProject(isNotNull())).thenReturn(dataReturn);
    when(confluenceAdapter.createCollaborationSpaceForODSProject(isNotNull()))
        .thenReturn(collaborationSpaceURL);
    Mockito.doNothing().when(mailAdapter).notifyUsersAboutProject(dataReturn);
    when(storage.storeProject(data)).thenReturn("created");

    Mockito.doNothing().when(idm).validateIdSettingsOfProject(dataReturn);

    mockMvc
        .perform(
            post("/api/v2/project")
                .content(asJsonString(data))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print())
        .andExpect(
            MockMvcResultMatchers.content()
                .string(CoreMatchers.containsString(dataReturn.description + "\"")));

    // test with null
    try {
      ProjectApiController.createShortenedDescription(null);
      fail();
    } catch (IllegalArgumentException iae) {
      assertTrue(iae.getMessage().contains("project"));
    }

    // test with content
    String description = ProjectApiController.createShortenedDescription(data);
    assertEquals(dataReturn.description, description);

    // test with null description
    data.description = null;
    description = ProjectApiController.createShortenedDescription(data);
    assertEquals(data.description, description);
  }

  public static String asJsonString(final Object obj) {
    try {
      final ObjectMapper mapper = new ObjectMapper();
      final String jsonContent = mapper.writeValueAsString(obj);
      return jsonContent;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Test
  public void testProjectDeleteForbidden() {
    apiController.setCleanupAllowed(false);
    IOException testExForbidden = null;
    try {
      this.apiController.deleteProject("1245");
    } catch (IOException mustbeThrown) {
      testExForbidden = mustbeThrown;
      assertEquals("Cleanup of projects is NOT allowed", mustbeThrown.getMessage());
    }
    assertNotNull(testExForbidden);
  }

  @Test
  public void testProjectComponentDeleteForbidden() {
    IOException testExForbidden = null;
    apiController.setCleanupAllowed(false);
    try {
      this.apiController.deleteComponents(new OpenProjectData());
    } catch (IOException mustbeThrown) {
      testExForbidden = mustbeThrown;
      assertEquals("Cleanup of projects is NOT allowed", mustbeThrown.getMessage());
    }
    assertNotNull(testExForbidden);
  }

  private OpenProjectData copyFromProject(OpenProjectData origin) {
    OpenProjectData data = new OpenProjectData();
    data.projectKey = origin.projectKey;
    data.projectName = origin.projectName;
    data.description = origin.description;
    data.platformRuntime = origin.platformRuntime;
    data.scmvcsUrl = origin.scmvcsUrl;
    data.quickstarters = origin.quickstarters;
    data.specialPermissionSet = origin.specialPermissionSet;
    data.projectAdminUser = origin.projectAdminUser;
    data.projectAdminGroup = origin.projectAdminGroup;
    data.projectUserGroup = origin.projectUserGroup;
    data.projectReadonlyGroup = origin.projectReadonlyGroup;
    return data;
  }

  @Test
  public void validateQuickstartersAcceptNoNullOrEmptyParams() {

    // case parameter is null
    try {
      ProjectApiController.validateQuickstarters(
          null, new ArrayList<Consumer<Map<String, String>>>());
    } catch (IllegalArgumentException e) {
      // expected!
      Assert.assertTrue(e.getMessage().contains("null"));
    }

    // case validators list is empty
    try {
      ProjectApiController.validateQuickstarters(
          data, new ArrayList<Consumer<Map<String, String>>>());
    } catch (IllegalArgumentException e) {
      // expected!
      Assert.assertTrue(e.getMessage().contains("validators"));
    }

    // case data is not null and validators is not empty
    Consumer<Map<String, String>> acceptAllValidator =
        stringStringMap -> {
          return;
        };
    ProjectApiController.validateQuickstarters(data, Arrays.asList(acceptAllValidator));
  }
}