package com.atlassian.bitbucket.internal.mirroring.mirror.client;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.ExternalRepository;
import com.atlassian.bitbucket.internal.mirroring.mirror.RepositoryRequestRateExceededException;
import com.atlassian.bitbucket.internal.mirroring.mirror.UnauthorizedException;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamServerRequestFailedException;
import com.atlassian.bitbucket.internal.mirroring.mirror.jwt.JwtSignedRequestFactory;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringNavBuilder;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringUrl;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.StubMirroringNavBuilder;
import com.atlassian.bitbucket.json.JsonRenderer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.httpclient.api.HttpClient;
import com.atlassian.httpclient.api.Request;
import com.atlassian.httpclient.api.Response;
import com.atlassian.httpclient.api.ResponsePromise;
import com.google.common.base.Throwables;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;

import javax.validation.Validator;
import java.io.StringWriter;
import java.util.concurrent.ExecutionException;

import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;

public abstract class UpstreamClientTestBase<T extends InternalUpstreamClient> {
    protected static final int PAGE_SIZE = 2;

    protected T client;
    @Rule
    public final ExpectedException expectedException = ExpectedException.none();
    @Mock
    protected HttpClient httpClient;
    protected I18nService i18nService = new SimpleI18nService();
    @Mock
    protected JsonRenderer jsonRenderer;
    protected MirroringNavBuilder mirroringNavBuilder = new StubMirroringNavBuilder();
    protected ResponsePromise promise;
    protected Request.Builder requestBuilder;
    @Mock
    protected JwtSignedRequestFactory requestFactory;
    @Mock
    protected Response response;
    @Mock
    protected UpstreamServer upstream;
    @Mock
    protected Validator validator;

    @Test
    public void testGetRepository() throws ExecutionException, InterruptedException {
        String jsonProject = jsonProject(9, "name9", "key9", false);
        String responseEntity = jsonRepo(1, "name1", "slug1", "git", false, jsonProject);

        mockJwtGetResponse(mirroringNavBuilder.upstream(upstream).repositoryById("1"), responseEntity, 200);

        ExternalRepository repo = client.getRepository("1").claim();

        assertNotNull(repo);
        assertRepo(1, "name1", "slug1", "git", false, repo);
    }

    @Test(expected = RepositoryRequestRateExceededException.class)
    public void testGetRepositoryRateLimitExceeded() throws ExecutionException, InterruptedException {
        mockJwtGetResponse(mirroringNavBuilder.upstream(upstream).repositoryById("1"), "", 429);

        client.getRepository("1").claim();
    }

    @Test(expected = UpstreamServerRequestFailedException.class)
    public void testGetRepositoryWithEmptyResponse() throws ExecutionException, InterruptedException {
        mockJwtGetResponse(mirroringNavBuilder.upstream(upstream).repositoryById("1"), "", 200);

        client.getRepository("1").claim();
    }

    @Test(expected = UnauthorizedException.class)
    public void testGetRepositoryNotAuthorized() throws ExecutionException, InterruptedException {
        mockJwtRequest(mirroringNavBuilder.upstream(upstream).repositoryById("1"));
        when(requestBuilder.get()).thenReturn(promise);
        mockResponse(response, "", 401);

        client.getRepository("1").claim();
    }

    protected abstract void assertRepo(Integer id, String name, String slug, String scmId, boolean isPublic, ExternalRepository actual);

    protected void expectException(Class<? extends Throwable> type, String message) {
        expectedException.expect(type);
        expectedException.expectMessage(message);
    }

    protected abstract String jsonProject(Object id, String name, String key, boolean isPublic);

    protected abstract String jsonRepo(Object id, String name, String slug, String scmId, boolean isPublic, String jsonProject);

    protected ResponsePromise mockGetResponse(String absoluteUrl, String responseEntity, int statusCode)
            throws ExecutionException, InterruptedException {
        when(httpClient.newRequest(absoluteUrl)).thenReturn(requestBuilder);
        when(requestBuilder.get()).thenReturn(promise);

        mockResponse(response, responseEntity, statusCode);
        return promise;
    }

    protected ResponsePromise mockJwtGetResponse(MirroringUrl mirroringUrl, String responseEntity, int statusCode)
            throws ExecutionException, InterruptedException {
        mockJwtRequest(mirroringUrl);
        when(requestBuilder.get()).thenReturn(promise);

        mockResponse(response, responseEntity, statusCode);
        return promise;
    }

    protected ResponsePromise mockJwtPostResponse(MirroringUrl mirroringUrl, String responseEntity, int statusCode)
            throws ExecutionException, InterruptedException {
        mockJwtRequest(mirroringUrl);
        when(requestBuilder.post()).thenReturn(promise);

        mockResponse(response, responseEntity, statusCode);
        return promise;
    }

    protected ResponsePromise mockPostResponse(String registrationUrl, String responseEntity, int statusCode)
            throws ExecutionException, InterruptedException {
        when(httpClient.newRequest(registrationUrl)).thenReturn(requestBuilder);
        when(requestBuilder.post()).thenReturn(promise);

        mockResponse(response, responseEntity, statusCode);
        return promise;
    }

    protected void mockJwtRequest(MirroringUrl mirroringUrl) {
        mockJwtRequest(mirroringUrl.getAbsolute());
    }

    protected void mockJwtRequest(String absoluteUrl) {
        when(requestFactory.newRequest(eq(upstream), argThat(new BaseMatcher<MirroringUrl>() {
            @Override
            public boolean matches(Object item) {
                return item != null
                        && item instanceof MirroringUrl
                        && ((MirroringUrl) item).getAbsolute().equals(absoluteUrl);
            }

            @Override
            public void describeTo(Description description) {
                description.appendText(String.format("MirroringUrl with absolute URL of %s", absoluteUrl));
            }
        }))).thenReturn(requestBuilder);
    }

    protected void mockResponse(Response response, String responseEntity, int statusCode) {
        when(response.getStatusCode()).thenReturn(statusCode);
        when(response.getEntity()).thenReturn(responseEntity);
        when(response.isSuccessful()).thenReturn(statusCode >= 200 && statusCode < 300);
        when(response.isOk()).thenReturn(statusCode == 200);
        when(response.isCreated()).thenReturn(statusCode == 201);
        when(response.isNoContent()).thenReturn(statusCode == 204);
        when(response.isUnauthorized()).thenReturn(statusCode == 401);
    }

    protected static String toJson(Object object) {
        StringWriter writer = new StringWriter();
        try (JsonGenerator generator = new ObjectMapper().getJsonFactory().createJsonGenerator(writer)) {
            generator.writeObject(object);
            generator.flush();
            return writer.toString();
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }
}
