package com.atlassian.bitbucket.internal.importer.bitbucketcloud;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.importer.*;
import com.atlassian.bitbucket.rest.RestMapEntity;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.test.ClasspathResourceRule;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.httpclient.api.Response;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static com.atlassian.bitbucket.internal.importer.ExternalRepositoryServiceTestHelper.*;
import static com.atlassian.bitbucket.internal.importer.ImporterConstants.PROPERTY_BITBUCKET_CLOUD_API_URL;
import static com.atlassian.bitbucket.internal.importer.ImporterConstants.PROPERTY_BITBUCKET_CLOUD_API_URL_DEFAULT;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class BitbucketCloudRepositoryServiceTest {

    @ClassRule
    public static final ClasspathResourceRule resources = new ClasspathResourceRule();

    private static final String API_URL = "https://api.bitbucket.org/2.0";
    private static final String TEAM_URL = API_URL + "/teams?page=1&pagelen=10&role=member";
    private static final Credential CREDENTIAL = new Credential.Builder("user", "password").build();
    private static final I18nService I18N_SERVICE = new SimpleI18nService();
    private static final String OWNER_NAME = "test";
    private static final String REPOSITORY_JSON_FILENAME = "/bitbucketcloud/repository_response.json";
    private static final String REPO_NAME = "somerep";
    private static final String REPOSITORY_URL = API_URL + "/repositories/" + OWNER_NAME + "?page=1&pagelen=10&q=scm+%3D+%22git%22+AND+%28name+%7E+%22" +
            REPO_NAME + "%22+OR+description+%7E+%22" + REPO_NAME + "%22%29";
    private static final String TEAM_JSON_FILENAME = "/bitbucketcloud/team_response.json";
    private static final String USER_URL = API_URL + "/users/";
    private static final String USER_JSON_FILENAME = "/bitbucketcloud/user_response.json";

    @Mock
    private ConstraintViolation nullViolation;
    private ExternalOwnerSearchRequest ownerSearchRequest;
    @Mock
    private ApplicationPropertiesService propertiesService;
    private StubRequestExecutor stubRequestExecutor;
    private ExternalRequest externalRequest;
    private ExternalRepositorySearchRequest repositorySearchRequest;
    private BitbucketCloudRepositoryService service;
    @Mock
    private Validator validator;

    @Before
    public void setup() throws Exception {
        String teamJson = new String(Files.readAllBytes(resources.get(TEAM_JSON_FILENAME).toPath()));
        String repositoryJson = new String(Files.readAllBytes(resources.get(REPOSITORY_JSON_FILENAME).toPath()));
        String userJson = new String(Files.readAllBytes(resources.get(USER_JSON_FILENAME).toPath()));

        externalRequest = new ExternalRequest
                .Builder(CREDENTIAL, bitbucketCloud())
                .build();
        repositorySearchRequest = new ExternalRepositorySearchRequest
                .Builder(CREDENTIAL, bitbucketCloud())
                .ownerName(OWNER_NAME)
                .filter(REPO_NAME)
                .build();
        ownerSearchRequest = new ExternalOwnerSearchRequest
                .Builder(CREDENTIAL, bitbucketCloud())
                .build();
        stubRequestExecutor = new StubRequestExecutor(ImmutableMap.of(
                TEAM_URL, getResponse(teamJson),
                REPOSITORY_URL, getResponse(repositoryJson),
                USER_URL + CREDENTIAL.getUsername(), getResponse(userJson)));

        when(propertiesService.getPluginProperty(PROPERTY_BITBUCKET_CLOUD_API_URL, PROPERTY_BITBUCKET_CLOUD_API_URL_DEFAULT)).thenReturn(PROPERTY_BITBUCKET_CLOUD_API_URL_DEFAULT);
        when(validator.validate(any())).thenAnswer(invocationOnMock ->
                validate((RestMapEntity) invocationOnMock.getArguments()[0]));
        service = new BitbucketCloudRepositoryService(I18N_SERVICE, propertiesService, stubRequestExecutor, validator);
    }

    @Test
    public void testGetOAuthScopes() throws Exception {
        assertMissingOAuthScopes("team, repository", new OAuthScope[]{});
        assertMissingOAuthScopes("team:write, repository", new OAuthScope[]{});
        assertMissingOAuthScopes("team, repository:write", new OAuthScope[]{});
        assertMissingOAuthScopes("team, repository:admin", new OAuthScope[]{});
        assertMissingOAuthScopes("repository", OAuthScope.OWNER);
        assertMissingOAuthScopes("random", OAuthScope.OWNER, OAuthScope.REPOSITORY);
        assertMissingOAuthScopes(null, OAuthScope.OWNER, OAuthScope.REPOSITORY);
        assertMissingOAuthScopes("", OAuthScope.OWNER, OAuthScope.REPOSITORY);
        assertMissingOAuthScopes("a, b, c", OAuthScope.OWNER, OAuthScope.REPOSITORY);
    }

    @Test
    public void testGetUser() throws Exception {
        assertUser(service.getUser(externalRequest), "testuser", "Atlassian Test User", "https://bitbucket.org/account/testuser/avatar/32/");
    }

    @Test
    public void testSearchOwners() throws Exception {
        Page<ExternalOwner> externalOwnerPage = service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));

        List<ExternalOwner> owners = StreamSupport.stream(
                externalOwnerPage
                        .getValues()
                        .spliterator(), false)
                .collect(Collectors.toList());
        assertThat(externalOwnerPage.getIsLastPage(), equalTo(true));
        assertThat(owners.size(), equalTo(4));
        assertOwner(owners.get(0), "atlassianlabs", "Atlassian Labs ", "https://bitbucket.org/account/atlassianlabs/avatar/32/");
        assertOwner(owners.get(1), "bitbucket", "Atlassian Bitbucket", "https://bitbucket.org/account/bitbucket/avatar/32/");
    }

    @Test
    public void testSearchRepositories() throws Exception {
        Page<ExternalRepository> externalRepositoryPage = service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));

        List<ExternalRepository> externalRepositories = StreamSupport.stream(
                externalRepositoryPage.getValues().spliterator(), false)
                .collect(Collectors.toList());
        assertThat(externalRepositories.size(), equalTo(10));
        assertRepository(externalRepositories.get(0),
                "connector-eclipse",
                "Atlassian Connector for Eclipse provides Eclipse integration for JIRA, FishEye and Bamboo.",
                "https://KristyHughes@bitbucket.org/atlassian/connector-eclipse");
        assertRepository(externalRepositories.get(1),
                "connector-idea", "",
                "https://KristyHughes@bitbucket.org/atlassian/connector-idea");
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testNullResponseEntityThrowsExceptionForSearchRepositories() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, null);
        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testNullResponseEntityThrowsExceptionForSearchOwners() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, null);

        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testEmptyResponseEntityThrowsExceptionForSearchRepositories() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, "");
        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testEmptyResponseEntityThrowsExceptionForSearchOwners() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, "");
        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testInvalidResponseEntityThrowsExceptionForSearchRepositories() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, "{");
        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ExternalRequestFailedException.class)
    public void testInvalidResponseEntityThrowsExceptionForSearchOwners() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, "{");
        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testEmptyMappedResponseEntityThrowsExceptionForSearchRepository() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, "{}");
        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testEmptyMappedResponseEntityThrowsExceptionForSearchOwners() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, "{}");
        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testWrongResponseEntityThrowsExceptionForSearchRepositories() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, "{\"blah\": \"a\"}");

        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testWrongResponseEntityThrowsExceptionForSearchOwners() throws Exception {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, "{\"blah\": \"a\"}");

        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testWrongResponseTypeThrowsExceptionForSearchRepositories() throws Exception {
        String teamJson = new String(Files.readAllBytes(resources.get(TEAM_JSON_FILENAME).toPath()));
        BitbucketCloudRepositoryService service = setupBibucketCloudService(REPOSITORY_URL, teamJson);

        service.searchRepositories(repositorySearchRequest, PageUtils.newRequest(0, 10));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testWrongResponseTypeThrowsExceptionForSearchOwners() throws Exception {
        String teamJson = new String(Files.readAllBytes(resources.get(REPOSITORY_JSON_FILENAME).toPath()));
        BitbucketCloudRepositoryService service = setupBibucketCloudService(TEAM_URL, teamJson);

        service.searchOwners(ownerSearchRequest, PageUtils.newRequest(0, 10));
    }

    private void assertMissingOAuthScopes(String headerValue, OAuthScope... oAuthScopes) throws IOException {
        BitbucketCloudRepositoryService service = setupBibucketCloudService(ImmutableMap.of(
                USER_URL + CREDENTIAL.getUsername(), getResponse("", "X-OAuth-Scopes", headerValue)));
        ExternalRequest request = new ExternalRequest.Builder(CREDENTIAL, bitbucketCloud()).build();
        if (oAuthScopes.length == 0) {
            assertTrue(Iterables.isEmpty(service.getMissingOAuthScopes(request)));
        } else {
            assertThat(service.getMissingOAuthScopes(request), IsIterableContainingInAnyOrder.containsInAnyOrder(oAuthScopes));
        }
    }
    
    private ExternalSource bitbucketCloud() {
        return new ExternalSource.Builder(SourceType.BITBUCKET_CLOUD).build();
    }

    private BitbucketCloudRepositoryService setupBibucketCloudService(Map<String, Response> responseMap) throws IOException {
        stubRequestExecutor = new StubRequestExecutor(responseMap);
        return new BitbucketCloudRepositoryService(I18N_SERVICE, propertiesService, stubRequestExecutor, validator);
    }

    private BitbucketCloudRepositoryService setupBibucketCloudService(String url, String response) throws IOException {
        stubRequestExecutor = new StubRequestExecutor(url, getResponse(response));
        return new BitbucketCloudRepositoryService(I18N_SERVICE, propertiesService, stubRequestExecutor, validator);
    }

    private Object validate(RestMapEntity response) {
        ImmutableSet.Builder<Object> errorBuilder = ImmutableSet.builder();
        if (response instanceof BitbucketCloudResponse) {
            BitbucketCloudResponse cloudResponse = (BitbucketCloudResponse) response;
            if (cloudResponse.getSize() < 0 || cloudResponse.getPageSize() < 0 || cloudResponse.getPage() < 0) {
                errorBuilder.add(nullViolation);
            }
        }
        if (response instanceof BitbucketCloudRepositoryResponse) {
            BitbucketCloudRepositoryResponse repositoryResponse = (BitbucketCloudRepositoryResponse) response;
            List<BitbucketCloudRepository> repositories = repositoryResponse.getValues();
            if (repositories == null) {
                errorBuilder.add(nullViolation);
            } else if (repositories.stream().anyMatch(repository ->
                    repository.getDescription() == null ||
                            repository.getName() == null ||
                            repository.getCloneUrl() == null)) {
                errorBuilder.add(nullViolation);
            }
        }
        if (response instanceof BitbucketCloudOwnerResponse) {
            BitbucketCloudOwnerResponse ownerResponse = (BitbucketCloudOwnerResponse) response;
            List<BitbucketCloudOwner> owners = ownerResponse.getValues();
            if (owners == null) {
                errorBuilder.add(nullViolation);
            } else if (owners.stream().anyMatch(owner ->
                    owner.getDescription() == null ||
                            owner.getName() == null ||
                            owner.getType() == null ||
                            owner.getAvatarUrl() == null)) {
                errorBuilder.add(nullViolation);
            }
        }
        return errorBuilder.build();
    }
}