/*
 * Copyright (C) 2017 Google Inc.
 *
 * 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 com.google.copybara.git.github.api.testing;

import static com.google.common.truth.Truth.assertThat;
import static com.google.copybara.git.github.api.GitHubApi.PullRequestListParams.DirectionFilter.ASC;
import static com.google.copybara.git.github.api.GitHubApi.PullRequestListParams.SortFilter.CREATED;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;

import com.google.api.client.json.gson.GsonFactory;
import com.google.common.base.Preconditions;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.copybara.exception.RepoException;
import com.google.copybara.exception.ValidationException;
import com.google.copybara.git.github.api.AddLabels;
import com.google.copybara.git.github.api.CheckRuns;
import com.google.copybara.git.github.api.CombinedStatus;
import com.google.copybara.git.github.api.CreatePullRequest;
import com.google.copybara.git.github.api.CreateStatusRequest;
import com.google.copybara.git.github.api.GitHubApi;
import com.google.copybara.git.github.api.GitHubApi.PullRequestListParams;
import com.google.copybara.git.github.api.GitHubApiException;
import com.google.copybara.git.github.api.GitHubApiException.ResponseCode;
import com.google.copybara.git.github.api.GitHubApiTransport;
import com.google.copybara.git.github.api.GitHubCommit;
import com.google.copybara.git.github.api.Issue;
import com.google.copybara.git.github.api.Label;
import com.google.copybara.git.github.api.PullRequest;
import com.google.copybara.git.github.api.PullRequestComment;
import com.google.copybara.git.github.api.Ref;
import com.google.copybara.git.github.api.Review;
import com.google.copybara.git.github.api.Status;
import com.google.copybara.git.github.api.Status.State;
import com.google.copybara.git.github.api.UpdatePullRequest;
import com.google.copybara.git.github.api.UpdateReferenceRequest;
import com.google.copybara.git.github.api.User;
import com.google.copybara.git.github.api.UserPermissionLevel;
import com.google.copybara.git.github.api.UserPermissionLevel.GitHubUserPermission;
import com.google.copybara.profiler.LogProfilerListener;
import com.google.copybara.profiler.Profiler;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * Base test to run the same tests on various implementations of the GitHubApiTransport
 */
public abstract class AbstractGitHubApiTest {

  protected GitHubApi api;
  @Mock
  protected GitHubApiTransport transport;
  @Captor
  protected ArgumentCaptor<ImmutableListMultimap<String, String>> headerCaptor;
  protected Profiler profiler;

  @Before
  public void setUpFamework() throws Exception {
    MockitoAnnotations.initMocks(this);

    profiler = new Profiler(Ticker.systemTicker());
    profiler.init(ImmutableList.of(new LogProfilerListener()));
    api = new GitHubApi(getTransport(), profiler);
  }

  public abstract GitHubApiTransport getTransport() throws Exception;

  public abstract void trainMockGetWithHeaders(String apiPath, byte[] response,
      ImmutableMap<String, String> headers, int status);

  private void trainMockGet(String apiPath, byte[] response) {
    trainMockGetWithHeaders(apiPath, response, ImmutableMap.of(), /*status=*/200);
  }

  public abstract void trainMockPost(String apiPath, Predicate<String> validator, byte[] response);

  public abstract void trainMockDelete(String apiPath, Predicate<String> validator, int statusCode);

  @Test
  public void testGetPulls() throws Exception {
    checkGetPulls("/repos/example/project/pulls?per_page=100", PullRequestListParams.DEFAULT);

    checkGetPulls("/repos/example/project/pulls?per_page=100&head=foo:bar",
        PullRequestListParams.DEFAULT.withHead("foo:bar"));

    checkGetPulls("/repos/example/project/pulls?per_page=100"
            + "&head=user:branch&base=the_base&sort=created&direction=asc",
        PullRequestListParams.DEFAULT
            .withHead("user:branch")
            .withBase("the_base")
            .withDirection(ASC)
            .withSort(CREATED));
  }

  private void checkGetPulls(String expectedUrl, PullRequestListParams params) throws IOException, RepoException, ValidationException {
    trainMockGet(expectedUrl, getResource("pulls_testdata.json"));
    ImmutableList<PullRequest> pullRequests = api.getPullRequests(
        "example/project", params);

    assertThat(pullRequests).hasSize(2);
    assertThat(pullRequests.get(0).getNumber()).isEqualTo(12345);
    assertThat(pullRequests.get(1).getNumber()).isEqualTo(12346);

    assertThat(pullRequests.get(0).getState()).isEqualTo("open");
    assertThat(pullRequests.get(1).getState()).isEqualTo("closed");

    assertThat(pullRequests.get(0).getTitle()).isEqualTo("[TEST] example pull request one");
    assertThat(pullRequests.get(1).getTitle()).isEqualTo("Another title");

    assertThat(pullRequests.get(0).getBody()).isEqualTo("Example body.\r\n");
    assertThat(pullRequests.get(1).getBody()).isEqualTo("Some text\r\n"
        + "And even more text\r\n");

    assertThat(pullRequests.get(0).getHead().getLabel())
        .isEqualTo("googletestuser:example-branch");
    assertThat(pullRequests.get(0).getHead().getRef()).isEqualTo("example-branch");
    assertThat(pullRequests.get(0).getHead().getSha()).isEqualTo(
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");

    assertThat(pullRequests.get(1).getHead().getLabel())
        .isEqualTo("anothergoogletestuser:another-branch");
    assertThat(pullRequests.get(1).getHead().getRef()).isEqualTo("another-branch");
    assertThat(pullRequests.get(1).getHead().getSha()).isEqualTo(
        "dddddddddddddddddddddddddddddddddddddddd");
  }

  @Test
  public void testGetPull() throws Exception {
    trainMockGet(
        "/repos/example/project/pulls/12345", getResource("pulls_12345_testdata.json"));
    PullRequest pullRequest = api.getPullRequest("example/project", 12345);

    assertThat(pullRequest.getNumber()).isEqualTo(12345);
    assertThat(pullRequest.getState()).isEqualTo("open");
    assertThat(pullRequest.getUser().getLogin()).isEqualTo("googletestuser");
    assertThat(pullRequest.getAssignee().getLogin()).isEqualTo("octocat");
    assertThat(pullRequest.getAssignees()).hasSize(1);
    assertThat(pullRequest.getRequestedReviewers()).hasSize(2);
    assertThat(pullRequest.getRequestedReviewers().get(0).getLogin())
        .isEqualTo("some_requested_reviewer");
    assertThat(pullRequest.getRequestedReviewers().get(1).getLogin())
        .isEqualTo("other_requested_reviewer");
    assertThat(pullRequest.getAssignees().get(0).getLogin()).isEqualTo("octocat");
    assertThat(pullRequest.getTitle()).isEqualTo("[TEST] example pull request one");
    assertThat(pullRequest.getBody()).isEqualTo("Example body.\r\n");
    assertThat(pullRequest.getHead().getLabel())
        .isEqualTo("googletestuser:example-branch");
    assertThat(pullRequest.getHead().getRef()).isEqualTo("example-branch");
    assertThat(pullRequest.getHead().getSha()).isEqualTo(
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
  }

  @Test
  public void testUpdatePullRequest() throws Exception {
    JsonValidator<UpdatePullRequest> validator = createValidator(UpdatePullRequest.class,
        (request) ->
            request.getTitle().equals("title")
                && request.getBody().equals("body")
                && request.getState().equals(UpdatePullRequest.State.CLOSED));

    trainMockPost(
        "/repos/example/project/pulls/12345", validator, getResource("pulls_12345_testdata.json"));

    api.updatePullRequest("example/project", 12345,
        new UpdatePullRequest("title", "body", UpdatePullRequest.State.CLOSED));

    assertThat(validator.wasCalled()).isTrue();
  }

  @Test
  public void testGetUserPermissionLevel() throws Exception {
    trainMockGet("/repos/example/project/collaborators/foo/permission",
        getResource("user_permission_level_testdata.json"));
    UserPermissionLevel permissionLevel =
        api.getUserPermissionLevel("example/project", "foo");
    assertThat(permissionLevel.getPermission()).isEqualTo(GitHubUserPermission.ADMIN);
  }

  @Test
  public void testAuthenticatedUser() throws Exception {
    trainMockGet(
        "/user", getResource("get_authenticated_user_response_testdata.json"));
    User user =  api.getAuthenticatedUser();
    assertThat(user.getLogin()).isEqualTo("googletestuser");

  }

  @Test
  public void testGetPullFail() throws Exception {
    try {
      api.getPullRequest("example/project", 12345);
      fail();
    } catch (ValidationException e) {
      assertThat(e).hasMessageThat().contains("Pull Request not found");
      GitHubApiException cause = (GitHubApiException) e.getCause();
      assertThat(cause.getError().getMessage()).isNotEmpty();
      assertThat(cause.getError().getDocumentationUrl()).isNotEmpty();
      assertThat(cause.getRawError()).isNotEmpty();
      assertThat(cause.getHttpCode()).isEqualTo(404);
      assertThat(cause.getResponseCode()).isEqualTo(ResponseCode.NOT_FOUND);
    }
  }

  @Test
  public void testGetPullReviews() throws Exception {
    trainMockGetWithHeaders("/repos/octocat/Hello-World/pulls/12/reviews?per_page=100",
        getResource("pulls_12345_reviews_testdata.json"),
        ImmutableMap.of("Link", ""
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=2>; rel=\"next\", "
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=3>; rel=\"last\", "
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=1>; rel=\"first\""
        ), 200);

    trainMockGetWithHeaders("/repositories/123/pulls?per_page=100&page=2",
        getResource("pulls_12345_reviews_testdata.json"),
        ImmutableMap.of("Link", ""
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=1>; rel=\"prev\","
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=3>; rel=\"next\", "
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=3>; rel=\"last\", "
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=1>; rel=\"first\""
        ), 200);

    trainMockGetWithHeaders("/repositories/123/pulls?per_page=100&page=3",
        getResource("pulls_12345_reviews_testdata.json"),
        ImmutableMap.of("Link", ""
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=2>; rel=\"prev\","
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=3>; rel=\"last\", "
            + "<https://api.github.com/repositories/123/pulls?per_page=100&page=1>; rel=\"first\""
        ), 200);
    List<Review> reviews = api.getReviews("octocat/Hello-World", 12);

    assertThat(reviews).hasSize(3);
    for (Review review : reviews) {
      assertThat(review.getBody()).isEqualTo("Here is the body for the review.");
      assertThat(review.getId()).isEqualTo(80L);
      assertThat(review.getUser().getId()).isEqualTo(1L);
      assertThat(review.getUser().getLogin()).isEqualTo("octocat");
      assertThat(review.getState()).isEqualTo("APPROVED");
      assertThat(review.isApproved()).isTrue();
      assertThat(review.getCommitId())
          .isEqualTo("ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091");
    }
  }

  @Test
  public void testGetLsRemote() throws Exception {
    trainMockGet(
        "/repos/copybara-test/copybara/git/refs?per_page=100",
        getResource("lsremote_testdata.json"));
    ImmutableList<Ref> refs = api.getLsRemote("copybara-test/copybara");

    assertThat(refs).hasSize(3);
    assertThat(refs.get(0).toString()).contains("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    assertThat(refs.get(0).toString()).contains("refs/heads/master");
    assertThat(refs.get(0).getRef()).isEqualTo("refs/heads/master");
    assertThat(refs.get(0).getUrl())
        .isEqualTo("https://api.github.com/repos/copybara-test/copybara/git/refs/heads/master");
    assertThat(refs.get(0).getSha()).isEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    assertThat(refs.get(1).getRef()).isEqualTo("refs/pull/1/head");
    assertThat(refs.get(1).getSha()).isEqualTo("1234567890123456789012345678901234567890");
    assertThat(refs.get(2).getRef()).isEqualTo("refs/pull/1/merge");
    assertThat(refs.get(2).getSha()).isEqualTo("abcdefabcdefabcdefabcdefabcdefabcdefabcd");
  }

  @Test
  public void testGetLsRemote_empty() throws Exception {
    trainMockGetWithHeaders("/repos/copybara-test/copybara/git/refs?per_page=100",
        ("{\n"
            + "  \"message\": \"Git Repository is empty.\",\n"
            + "  \"documentation_url\":"
            + " \"https://developer.github.com/v3/git/refs/#get-all-references\"\n"
            + "}").getBytes(UTF_8), ImmutableMap.of(),
        409 // Http conflict
    );
    ImmutableList<Ref> refs = api.getLsRemote("copybara-test/copybara");

    assertThat(refs).isEmpty();
  }

  @Test
  public void testGetLsRemote_fail() throws Exception {
    trainMockGetWithHeaders("/repos/copybara-test/copybara/git/refs?per_page=100",
        ("{\n"
            + "  \"message\": \"Whatever you are looking for doesn't exist!!\"\n"
            + "}").getBytes(UTF_8), ImmutableMap.of(),
        404);
    try {
      api.getLsRemote("copybara-test/copybara");
      fail();
    } catch (GitHubApiException e) {
      assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NOT_FOUND);
    }
  }

  @Test
  public void testCreatePullRequest() throws Exception {
    trainMockPost(
        "/repos/example/project/pulls", createValidator(TestCreatePullRequest.class,
            (cpr) ->
                cpr.getTitle().equals("title")
                && cpr.getBase().equals("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
                && cpr.getBody().equals("[TEST] example pull request one")
                && cpr.getHead().equals("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")),
        getResource("pulls_12345_testdata.json"));
    // The test does not actually use the data in the CreatePullRequest
    PullRequest pullRequest = api.createPullRequest("example/project",
        new CreatePullRequest("title",
            "[TEST] example pull request one",
            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            "aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));

    assertThat(pullRequest.getNumber()).isEqualTo(12345);
    assertThat(pullRequest.getState()).isEqualTo("open");
    assertThat(pullRequest.getTitle()).isEqualTo("[TEST] example pull request one");
  }

  @Test
  public void testGetIssue() throws Exception {
    trainMockGet(
        "/repos/example/project/issues/12345", getResource("issues_12345_testdata.json"));
    Issue issue = api.getIssue("example/project", 12345);

    assertThat(issue.getNumber()).isEqualTo(12345);
    assertThat(issue.getUser().getLogin()).isEqualTo("googletestuser");
    assertThat(issue.getAssignee().getLogin()).isEqualTo("octocat");
    assertThat(issue.getAssignees()).hasSize(1);
    assertThat(issue.getAssignees().get(0).getLogin()).isEqualTo("octocat");
    assertThat(issue.getState()).isEqualTo("open");
    assertThat(issue.getTitle()).isEqualTo("[TEST] example pull request one");
    assertThat(issue.getBody()).isEqualTo("Example body.\r\n");
    assertThat(Lists.transform(issue.getLabels(), Label::getName))
        .containsExactly("cla: yes");
  }


  @Test
  public void testCreateStatus() throws Exception {
    trainMockPost("/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e",
        createValidator(TestCreateStatusRequest.class,
            csr -> csr.getContext().equals("continuous-integration/jenkins")
                && csr.getState().equals(State.SUCCESS)),
        getResource("create_status_response_testdata.json"));

    Status response = api.createStatus("octocat/Hello-World",
        "6dcb09b5b57875f334f61aebed695e2e4193db5e",
        new CreateStatusRequest(State.SUCCESS, "https://ci.example.com/1000/output",
            "Build has completed successfully", "continuous-integration/jenkins"));

    assertThat(response.getContext()).isEqualTo("continuous-integration/jenkins");
    assertThat(response.getTargetUrl()).isEqualTo("https://ci.example.com/1000/output");
    assertThat(response.getDescription()).isEqualTo("Build has completed successfully");
    assertThat(response.getState()).isEqualTo(State.SUCCESS);
    assertThat(response.getCreator()).isNotNull();
    assertThat(response.getCreator().getLogin()).isEqualTo("octocat");
  }

  @Test
  public void testUpdateReference() throws Exception {
    trainMockPost(
        "/repos/octocat/Hello-World/git/refs/heads/test",
        createValidator(
            TestUpdateReferenceRequest.class,
            urr ->
                urr.getSha1().equals("6dcb09b5b57875f334f61aebed695e2e4193db5e")
                    && urr.getForce()),
        getResource("update_reference_response_testdata.json"));

    Ref response = api.updateReference("octocat/Hello-World",
        "refs/heads/test",
        new UpdateReferenceRequest("6dcb09b5b57875f334f61aebed695e2e4193db5e", true));

    assertThat(response.getRef()).isEqualTo("refs/heads/test");
    assertThat(response.getSha()).isEqualTo("6dcb09b5b57875f334f61aebed695e2e4193db5e");
    assertThat(response.getUrl()).isEqualTo(
        "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/test");
  }

  @Test
  public void testDeleteRef() throws Exception {
    AtomicBoolean called = new AtomicBoolean(false);
    trainMockDelete("/repos/octocat/Hello-World/git/refs/heads/test",
        s -> {
          called.set(true);
          return true;
        }, 202);

    api.deleteReference("octocat/Hello-World", "refs/heads/test");
    assertThat(called.get()).isTrue();
  }

  @Test
  public void testDeleteRefFail() throws Exception {

    trainMockDelete("/repos/octocat/Hello-World/git/refs/heads/test", s -> true, 404);

    try {
      api.deleteReference("octocat/Hello-World", "refs/heads/test");
      fail();
    } catch (GitHubApiException e) {
      assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NOT_FOUND);
    }
  }

  @Test
  public void testGetReference() throws Exception {
    trainMockGet(
        "/repos/octocat/Hello-World/git/refs/heads/g3",
        getResource("get_reference_response_testdata.json"));

    Ref response = api.getReference("octocat/Hello-World", "refs/heads/g3");

    assertThat(response.getRef()).isEqualTo("refs/heads/g3");
    assertThat(response.getSha()).isEqualTo("9a2f372a62761ac378a62935c44cfcb9695d0661");
    assertThat(response.getUrl()).isEqualTo(
        "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/g3");
  }

  @Test
  public void testInvalidParameterWhenGetReference() throws Exception {
    try {
      api.getReference("octocat/Hello-World", "heads/g3");
      fail();
    } catch (ValidationException e) {
      assertThat(e).hasMessageThat().contains("Ref must start with \"refs/\"");
    }
  }

  @Test
  public void testGetAllReferences() throws Exception {
    trainMockGet(
        "/repos/octocat/Hello-World/git/refs?per_page=100",
        getResource("get_all_references_response_testdata.json"));

    ImmutableList<Ref> response = api.getReferences("octocat/Hello-World");

    assertThat(response.get(0).getRef()).isEqualTo("refs/heads/g3");
    assertThat(response.get(0).getSha()).isEqualTo("9a2f372a62761ac378a62935c44cfcb9695d0661");
    assertThat(response.get(0).getUrl()).isEqualTo(
        "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/g3");
    assertThat(response.get(1).getRef()).isEqualTo("refs/heads/master");
    assertThat(response.get(1).getSha()).isEqualTo("9a2f372a62761ac378a62935c44cfcb9695d0661");
    assertThat(response.get(1).getUrl()).isEqualTo(
        "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/master");
  }


  @Test
  public void testGetCombinedStatus() throws Exception {
    trainMockGet(
        "/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/status",
        getResource("get_combined_status_testdata.json"));

    CombinedStatus response = api.getCombinedStatus("octocat/Hello-World",
        "6dcb09b5b57875f334f61aebed695e2e4193db5e");

    assertThat(response.getSha()).isEqualTo("6dcb09b5b57875f334f61aebed695e2e4193db5e");
    assertThat(response.getStatuses()).hasSize(2);
    assertThat(response.getState()).isEqualTo(State.SUCCESS);
    assertThat(response.getTotalCount()).isEqualTo(2);

    assertThat(response.getStatuses().get(0).getContext())
        .isEqualTo("continuous-integration/jenkins");
    assertThat(response.getStatuses().get(0).getTargetUrl())
        .isEqualTo("https://ci.example.com/1000/output");
    assertThat(response.getStatuses().get(0).getDescription())
        .isEqualTo("Build has completed successfully");
    assertThat(response.getStatuses().get(0).getState()).isEqualTo(State.SUCCESS);
  }

  @Test
  public void testGetPullRequestComment() throws Exception {
    trainMockGet(
        "/repos/example/project/pulls/comments/12345",
        getResource("pulls_comment_12345_testdata.json"));
    PullRequestComment pullRequestComment = api.getPullRequestComment("example/project", 12345);

    assertThat(pullRequestComment.getId()).isEqualTo(12345);
    assertThat(pullRequestComment.getPosition()).isEqualTo(13);
    assertThat(pullRequestComment.getOriginalPosition()).isEqualTo(13);
    assertThat(pullRequestComment.getPath())
        .isEqualTo("java/com/google/copybara/git/GitEnvironment.java");
    assertThat(pullRequestComment.getUser().getLogin()).isEqualTo("googletestuser");
    assertThat(pullRequestComment.getCommitId())
        .isEqualTo("228ed14f89c19caed87717a8a53392f58c3a24f9");
    assertThat(pullRequestComment.getOriginalCommitId())
        .isEqualTo("7a8d55973a82b250e8c206673b2ae1e6bacb97d0");
    assertThat(pullRequestComment.getBody()).isEqualTo("This needs to be fixed.");
    assertThat(pullRequestComment.getDiffHunk())
        .isEqualTo(
            "@@ -36,11 +35,16 @@ public GitEnvironment(Map<String, String> environment) {\n"
                + "   }\n"
                + " \n"
                + "   public ImmutableMap<String, String> getEnvironment() {\n"
                + "-    Map<String, String> env = Maps.newHashMap(environment);\n"
                + "+    ImmutableMap.Builder<String, String> env = ImmutableMap.builder();");
    assertThat(pullRequestComment.getCreatedAt())
        .isEqualTo(ZonedDateTime.parse("2019-06-21T20:20:20Z"));
    assertThat(pullRequestComment.getUpdatedAt())
        .isEqualTo(ZonedDateTime.parse("2019-07-18T15:32:41Z"));
  }

  @Test
  public void testGetPullRequestComment_notFound() throws RepoException {
    try {
      api.getPullRequestComment("example/project", 12345);
      fail();
    } catch (ValidationException expected) {
      // expected
    }
  }

  @Test
  public void testGetPullRequestComments() throws Exception {
    trainMockGet(
        "/repos/example/project/pulls/12345/comments?per_page=100",
        getResource("pulls_comments_12345_testdata.json"));
    ImmutableList<PullRequestComment> comments =
        api.getPullRequestComments("example/project", 12345);
    assertThat(comments).hasSize(2);
  }

  @Test
  public void testGetPullRequestComments_notFound() throws Exception {
    try {
      api.getPullRequestComments("example/project", 12345);
      fail();
    } catch (ValidationException expected) {
      // expected
    }
  }

  @Test
  public void test_GetCheckRuns_Sucess() throws Exception {
    trainMockGet("/repos/example/project/commits/12345/check-runs",
        getResource("get_check_runs_testdata.json"));
    CheckRuns checkRuns =
        api.getCheckRuns("example/project", "12345");
    assertThat(checkRuns.getTotalCount()).isEqualTo(1);
    assertThat(checkRuns.getCheckRuns().get(0).getStatus()).isEqualTo("completed");
    assertThat(checkRuns.getCheckRuns().get(0).getConclusion()).isEqualTo("neutral");
    assertThat(checkRuns.getCheckRuns().get(0).getDetailUrl()).isEqualTo("https://example.com");
    assertThat(checkRuns.getCheckRuns().get(0).getApp().getId()).isEqualTo(1);
    assertThat(checkRuns.getCheckRuns().get(0).getApp().getName()).isEqualTo("Octocat App");
    assertThat(checkRuns.getCheckRuns().get(0).getApp().getSlug()).isEqualTo("octoapp");
  }

  @Test
  public void testGetCheckRunsHeader_containGitHubHeader() throws Exception {
    api = new GitHubApi(transport, profiler);
    api.getCheckRuns("example/project", "12345");
    verify(transport).get(any(), any(), headerCaptor.capture());
    assertThat(headerCaptor.getValue())
        .containsEntry("Accept", "application/vnd.github.antiope-preview+json");
  }

  @Test
  public void testGetPullRequestComments_empty() throws Exception {
    trainMockGet("/repos/example/project/pulls/12345/comments?per_page=100", "[]".getBytes(UTF_8));
    assertThat(api.getPullRequestComments("example/project", 12345)).isEmpty();
  }

  @Test
  public void testCommit() throws Exception {
    trainMockGet(
        "/repos/octocat/Hello-World/commits/604aa8e189a6fee605140ebbe4a3c34ad24619d1",
        // normally we use GH documentation examples but the docs are different of the actual
        // response
        getResource("commit_response_testdata.json"));

    GitHubCommit r = api.getCommit("octocat/Hello-World",
        "604aa8e189a6fee605140ebbe4a3c34ad24619d1");

    assertThat(r.getSha()).isEqualTo("604aa8e189a6fee605140ebbe4a3c34ad24619d1");
    assertThat(r.getAuthor().getLogin()).isEqualTo("copybara-author");
    assertThat(r.getCommitter().getLogin()).isEqualTo("copybara-committer");
    assertThat(r.getHtmlUrl()).isEqualTo(
        "https://github.com/google/copybara/commit/604aa8e189a6fee605140ebbe4a3c34ad24619d1");
    assertThat(r.getCommit().getMessage()).isEqualTo(""
        + "Temporal fix to the CI\n"
        + "\n"
        + "We use this deprecated flag until we migrate to the new\n"
        + "non-native rule.\n"
        + "\n"
        + "Will properly fix on Monday.\n"
        + "\n"
        + "Change-Id: Ia3c35b8ece932b94e0aa4c7a28bd16d35a260970");
    assertThat(r.getCommit().getAuthor().getName()).isEqualTo("The Author");
    assertThat(r.getCommit().getAuthor().getEmail()).isEqualTo("[email protected]");
    assertThat(r.getCommit().getAuthor().getDate())
        .isEqualTo(ZonedDateTime.parse("2018-12-07T23:36:45Z"));

    assertThat(r.getCommit().getCommitter().getName()).isEqualTo("The Committer");
    assertThat(r.getCommit().getCommitter().getEmail()).isEqualTo("[email protected]");
    assertThat(r.getCommit().getCommitter().getDate())
        .isEqualTo(ZonedDateTime.parse("2018-11-07T23:36:45Z"));
  }

  @Test
  public void testAddLabel() throws Exception {
    ImmutableList<String> labels = ImmutableList.of("my_label1", "my_label2");
    trainMockPost("/repos/example/project/issues/12345/labels", createValidator(
        AddLabels.class,
        (a) ->
            a.getLabels().equals(labels)), getResource("labels_response_testdata.json"));
    assertThat(api.addLabels("example/project", 12345, labels)).hasSize(2);
  }

  protected byte[] getResource(String testfile) throws IOException {
    return Files.readAllBytes(
        Paths.get(System.getenv("TEST_SRCDIR"),
            "copybara/java/com/google/copybara/git/github/api/"
                + "testing")
            .resolve(testfile));
  }

  private static <T> JsonValidator<T> createValidator(Class<T> clazz, Predicate<T> predicate) {
    return new JsonValidator<>(clazz, predicate);
  }

  // We don't want CreatePullRequest to be instantiable, this subclass sidesteps the issue.
  public static class TestCreatePullRequest extends CreatePullRequest {
    public TestCreatePullRequest() {
      super("invalid", "invalid", "invalid", "invalid");
    }
  }

  // We don't want CreateStatusRequest to be instantiable, this subclass sidesteps the issue.
  public static class TestCreateStatusRequest extends CreateStatusRequest {
    public TestCreateStatusRequest() {
      super(State.ERROR, "invalid", "invalid", "invalid");
    }
  }

  // We don't want UpdateReferenceRequest to be instantiable, this subclass sidesteps the issue.
  public static class TestUpdateReferenceRequest extends UpdateReferenceRequest {
    public TestUpdateReferenceRequest() {
      super("6dcb09b5b57875f334f61aebed695e2e4193db5e", true);
    }
  }

  private static class JsonValidator<T> implements Predicate<String> {
    private boolean called;
    private final Class<T> clazz;
    private final Predicate<T> predicate;

    JsonValidator(Class<T> clazz, Predicate<T> predicate) {
      this.clazz = Preconditions.checkNotNull(clazz);
      this.predicate = Preconditions.checkNotNull(predicate);
    }

    @Override
    public boolean test(String s) {
      try {
        T requestObject = GsonFactory.getDefaultInstance().createJsonParser(s).parse(clazz);
        called = true;
        return predicate.test(requestObject);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    boolean wasCalled() {
      return called;
    }
  }
}