Use direct GerritApi calls in GerritClientDetail

The `GerritClientDetail` used to call REST API in order to retrive the
change details and map them to the internal `GerritPatchSetDetail`
representation. Alter it to use direct `GerritApi` calls with the
following changes:
* obtain user's `Account.Id` from the username and set it into the
  `Configuration`
* add `openRequestContext` function to `Configuration` that is
  responsible for setting the user's context to the `GerritApi`
* use `GerritApi` in order to retrieve `ChangeInfo`
* map the `ChangeInfo` to `GerritPatchSetDetail`
* load `__files/gerritPatchSetDetail.json` as `ChangeInfo` and mock
  `GerritApi` in tests so that they are passing

Bug: Issue 336577745
Change-Id: I5a24c6671529a9cc2c4a06c152fa638c684e99b7
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/ConfigCreator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/ConfigCreator.java
index 4a17d31..6e85051 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/ConfigCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/ConfigCreator.java
@@ -1,16 +1,15 @@
 package com.googlesource.gerrit.plugins.chatgpt.config;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import lombok.extern.slf4j.Slf4j;
@@ -26,25 +25,43 @@
     private final AccountCache accountCache;
     private final PluginConfigFactory configFactory;
 
+    private final OneOffRequestContext context;
+    private final GerritApi gerritApi;
+
     @Inject
-    ConfigCreator(@PluginName String pluginName, AccountCache accountCache, PluginConfigFactory configFactory) {
+    ConfigCreator(@PluginName String pluginName, AccountCache accountCache, PluginConfigFactory configFactory, OneOffRequestContext context, GerritApi gerritApi) {
         this.pluginName = pluginName;
-        this.configFactory = configFactory;
         this.accountCache = accountCache;
+        this.configFactory = configFactory;
+        this.context = context;
+        this.gerritApi = gerritApi;
     }
 
-    public Configuration createConfig(Project.NameKey projectName)
-            throws NoSuchProjectException {
+    public Configuration createConfig(Project.NameKey projectName) throws NoSuchProjectException {
         PluginConfig globalConfig = configFactory.getFromGerritConfig(pluginName);
-        log.debug("These configuration items have been set in the global configuration: {}", globalConfig.getNames());
+        log.debug(
+            "These configuration items have been set in the global configuration: {}",
+            globalConfig.getNames());
         PluginConfig projectConfig = configFactory.getFromProjectConfig(projectName, pluginName);
-        log.debug("These configuration items have been set in the project configuration: {}", projectConfig.getNames());
-        return new Configuration(globalConfig, projectConfig, getGerritUserEmail(globalConfig));
+        log.debug(
+            "These configuration items have been set in the project configuration: {}",
+            projectConfig.getNames());
+        Optional<AccountState> gptAccount = getAccount(globalConfig);
+        String email = gptAccount.map(a -> a.account().preferredEmail()).orElse("");
+        Account.Id accountId =
+            gptAccount
+                .map(a -> a.account().id())
+                .orElseThrow(
+                    () ->
+                        new RuntimeException(
+                            String.format(
+                                "Given account %s doesn't exist",
+                                globalConfig.getString(Configuration.KEY_GERRIT_USERNAME))));
+        return new Configuration(context, gerritApi, globalConfig, projectConfig, email, accountId);
     }
 
-    private String getGerritUserEmail(PluginConfig globalConfig) {
+    private Optional<AccountState> getAccount(PluginConfig globalConfig) {
         String gptUser = globalConfig.getString(Configuration.KEY_GERRIT_USERNAME);
-        Optional<AccountState> gptAccount = accountCache.getByUsername(gptUser);
-        return gptAccount.map(a -> a.account().preferredEmail()).orElse("");
+        return accountCache.getByUsername(gptUser);
     }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
index a9079e1..d5c189c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
@@ -1,6 +1,11 @@
 package com.googlesource.gerrit.plugins.chatgpt.config;
 
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
@@ -120,21 +125,30 @@
     private static final String KEY_IGNORE_OUTDATED_INLINE_COMMENTS = "ignoreOutdatedInlineComments";
     private static final String KEY_IGNORE_RESOLVED_CHAT_GPT_COMMENTS = "ignoreResolvedChatGptComments";
 
+    private final OneOffRequestContext context;
+    @Getter
+    private final Account.Id userId;
     @Getter
     private final PluginConfig globalConfig;
     @Getter
     private final PluginConfig projectConfig;
     @Getter
     private final String gerritUserEmail;
+    @Getter
+    private final GerritApi gerritApi;
 
-    public Configuration(PluginConfig globalConfig, PluginConfig projectConfig) {
-        this(globalConfig, projectConfig, "");
-    }
 
-    public Configuration(PluginConfig globalConfig, PluginConfig projectConfig, String gerritUserEmail) {
+    public Configuration(OneOffRequestContext context, GerritApi gerritApi, PluginConfig globalConfig, PluginConfig projectConfig, String gerritUserEmail, Account.Id userId) {
+        this.context = context;
+        this.gerritApi = gerritApi;
         this.globalConfig = globalConfig;
         this.projectConfig = projectConfig;
         this.gerritUserEmail = gerritUserEmail;
+        this.userId = userId;
+    }
+
+    public ManualRequestContext openRequestContext() {
+      return context.openAs(userId);
     }
 
     public String getGptToken() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientDetail.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientDetail.java
index f67c82a..4f6350e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientDetail.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientDetail.java
@@ -1,26 +1,42 @@
 package com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit;
 
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.server.util.ManualRequestContext;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.UriResourceLocator;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritComment;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritPatchSetDetail;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritPermittedVotingRange;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
 import lombok.extern.slf4j.Slf4j;
 
-import java.net.URI;
-import java.util.List;
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.toList;
 
-import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
 
 @Slf4j
-public class GerritClientDetail extends GerritClientBase {
+public class GerritClientDetail {
+    private static final SimpleDateFormat DATE_FORMAT = newFormat();
+
     private GerritPatchSetDetail gerritPatchSetDetail;
     private final int gptAccountId;
+    private final Configuration config;
 
     public GerritClientDetail(Configuration config, ChangeSetData changeSetData) {
-        super(config);
         this.gptAccountId = changeSetData.getGptAccountId();
+        this.config = config;
     }
 
     public List<GerritComment> getMessages(GerritChange change) {
@@ -54,18 +70,102 @@
             return;
         }
         try {
-            gerritPatchSetDetail = getReviewDetail(change.getFullChangeId());
+            gerritPatchSetDetail = getReviewDetail(change);
         }
         catch (Exception e) {
             log.error("Error retrieving PatchSet details", e);
         }
     }
 
-    private GerritPatchSetDetail getReviewDetail(String fullChangeId) throws Exception {
-        URI uri = URI.create(config.getGerritAuthBaseUrl()
-                + UriResourceLocator.gerritGetPatchSetDetailUri(fullChangeId));
-        String responseBody = forwardGetRequest(uri);
-        return getGson().fromJson(responseBody, GerritPatchSetDetail.class);
+    private GerritPatchSetDetail getReviewDetail(GerritChange change) throws Exception {
+        try (ManualRequestContext requestContext = config.openRequestContext()) {
+          ChangeInfo info =
+              config
+                  .getGerritApi()
+                  .changes()
+                  .id(change.getProjectName(), change.getBranchNameKey().shortName(), change.getChangeKey().get())
+                  .get();
+
+          GerritPatchSetDetail detail = new GerritPatchSetDetail();
+          detail.setWorkInProgress(info.workInProgress);
+          Optional.ofNullable(info.labels)
+              .map(Map::entrySet)
+              .map(Set::stream)
+              .flatMap(
+                  labels ->
+                      labels
+                          .filter(label -> LabelId.CODE_REVIEW.equals(label.getKey()))
+                          .map(GerritClientDetail::toLabels)
+                          .findAny())
+              .ifPresent(detail::setLabels);
+          Optional.ofNullable(info.messages)
+              .map(messages -> messages.stream().map(GerritClientDetail::toComment).collect(toList()))
+              .ifPresent(detail::setMessages);
+
+          return detail;
+        }
     }
 
+    private static GerritPatchSetDetail.Labels toLabels(Entry<String, LabelInfo> label) {
+        List<GerritPatchSetDetail.Permission> permissions =
+            Optional.ofNullable(label.getValue().all)
+                .map(all -> all.stream().map(GerritClientDetail::toPermission).collect(toList()))
+                .orElse(emptyList());
+        GerritPatchSetDetail.CodeReview codeReview = new GerritPatchSetDetail.CodeReview();
+        codeReview.setAll(permissions);
+        GerritPatchSetDetail.Labels labels = new GerritPatchSetDetail.Labels();
+        labels.setCodeReview(codeReview);
+        return labels;
+    }
+
+    private static GerritPatchSetDetail.Permission toPermission(ApprovalInfo value) {
+        GerritPatchSetDetail.Permission permission = new GerritPatchSetDetail.Permission();
+        permission.setValue(value.value);
+        Optional.ofNullable(value.date).ifPresent(date -> permission.setDate(toDateString(date)));
+        Optional.ofNullable(value.permittedVotingRange)
+            .ifPresent(
+                permittedVotingRange -> {
+                  GerritPermittedVotingRange range = new GerritPermittedVotingRange();
+                  range.setMin(permittedVotingRange.min);
+                  range.setMax(permittedVotingRange.max);
+                  permission.setPermittedVotingRange(range);
+                });
+        permission.setAccountId(value._accountId);
+        return permission;
+    }
+
+    private static GerritComment toComment(ChangeMessageInfo message) {
+        GerritComment comment = new GerritComment();
+        Optional.ofNullable(message.author).ifPresent(author -> comment.setAuthor(toAuthor(author)));
+        comment.setId(message.id);
+        comment.setTag(message.tag);
+        Optional.ofNullable(message.date).ifPresent(date -> comment.setDate(toDateString(date)));
+        comment.setMessage(message.message);
+        comment.setPatchSet(message._revisionNumber);
+        return comment;
+    }
+
+    private static GerritComment.Author toAuthor(AccountInfo authorInfo ) {
+        GerritComment.Author author = new GerritComment.Author();
+        author.setAccountId(authorInfo._accountId);
+        author.setName(authorInfo.name);
+        author.setDisplayName(author.getDisplayName());
+        author.setEmail(authorInfo.email);
+        author.setUsername(authorInfo.username);
+        return author;
+    }
+
+    /**
+     * Date format copied from <b>com.google.gerrit.json.SqlTimestampDeserializer</b>
+     */
+    private static String toDateString(Timestamp input) {
+        return DATE_FORMAT.format(input) + "000000";
+    }
+
+    private static SimpleDateFormat newFormat() {
+        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        f.setTimeZone(TimeZone.getTimeZone("UTC"));
+        f.setLenient(true);
+        return f;
+    }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/gerrit/GerritPatchSetDetail.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/gerrit/GerritPatchSetDetail.java
index 0ada543..425cd86 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/gerrit/GerritPatchSetDetail.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/gerrit/GerritPatchSetDetail.java
@@ -5,6 +5,7 @@
 
 import java.util.List;
 
+// TODO remove once migration to GerritApi is finished and com.google.gerrit.extensions.common.ChangeInfo is used
 @Data
 public class GerritPatchSetDetail {
     private Labels labels;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
index 852531d..0a0b535 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
@@ -2,6 +2,7 @@
 
 import com.github.tomakehurst.wiremock.client.WireMock;
 import com.google.common.net.HttpHeaders;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.UriResourceLocatorStateful;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.prompt.ChatGptPromptStateful;
 import com.googlesource.gerrit.plugins.chatgpt.settings.Settings.MODES;
@@ -41,7 +42,7 @@
         chatGptPromptStateful = new ChatGptPromptStateful(config, getGerritChange());
     }
 
-    protected void setupMockRequests() {
+    protected void setupMockRequests() throws RestApiException {
         super.setupMockRequests();
 
         // Mock the behavior of the ChatGPT create file request
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
index 07880e8..4a0da3d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
@@ -3,6 +3,7 @@
 import com.github.tomakehurst.wiremock.client.WireMock;
 import com.google.common.net.HttpHeaders;
 import com.googlesource.gerrit.plugins.chatgpt.listener.EventHandlerTask;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.UriResourceLocatorStateless;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.prompt.ChatGptPromptStateless;
 import lombok.extern.slf4j.Slf4j;
@@ -47,7 +48,7 @@
         chatGptPromptStateless = new ChatGptPromptStateless(config);
     }
 
-    protected void setupMockRequests() {
+    protected void setupMockRequests() throws RestApiException {
         super.setupMockRequests();
 
         String fullChangeId = getGerritChange().getFullChangeId();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
index a8af5a5..b252280 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
@@ -11,6 +11,12 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
@@ -19,6 +25,8 @@
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.PatchSetEvent;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gson.Gson;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.inject.AbstractModule;
@@ -96,6 +104,11 @@
     @Mock
     protected PluginDataHandler pluginDataHandler;
 
+    @Mock
+    protected OneOffRequestContext context;
+    @Mock
+    protected GerritApi gerritApi;
+
     protected PluginConfig globalConfig;
     protected PluginConfig projectConfig;
     protected Configuration config;
@@ -106,7 +119,7 @@
     protected JsonArray prompts;
 
     @Before
-    public void before() throws NoSuchProjectException {
+    public void before() throws NoSuchProjectException, RestApiException {
         initGlobalAndProjectConfig();
         initConfig();
         setupMockRequests();
@@ -147,13 +160,13 @@
     }
 
     protected void initConfig() {
-        config = new Configuration(globalConfig, projectConfig);
+        config = new Configuration(context, gerritApi, globalConfig, projectConfig, "gpt@email.com", Account.id(1000000));
 
         // Mock the config instance values
         when(config.getGerritUserName()).thenReturn(GERRIT_GPT_USERNAME);
     }
 
-    protected void setupMockRequests() {
+    protected void setupMockRequests() throws RestApiException {
         String fullChangeId = getGerritChange().getFullChangeId();
 
         // Mock the behavior of the gerritAccountIdUri request
@@ -191,6 +204,7 @@
                         .withStatus(HTTP_OK)
                         .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
                         .withBodyFile("gerritPatchSetDetail.json")));
+        mockGerritApi();
 
         // Mock the behavior of the gerritPatchSet comments request
         WireMock.stubFor(WireMock.get(gerritGetAllPatchSetCommentsUri(fullChangeId))
@@ -205,6 +219,16 @@
                         .withStatus(HTTP_OK)));
     }
 
+    private void mockGerritApi() throws RestApiException {
+        Gson gson = OutputFormat.JSON.newGson();
+        ChangeInfo changeInfo = gson.fromJson(readTestFile("__files/gerritPatchSetDetail.json"), ChangeInfo.class);
+        Changes changesMock = mock(Changes.class);
+        when(gerritApi.changes()).thenReturn(changesMock);
+        ChangeApi changeApiMock = mock(ChangeApi.class);
+        when(changesMock.id(PROJECT_NAME.get(), BRANCH_NAME.shortName(), CHANGE_ID.get())).thenReturn(changeApiMock);
+        when(changeApiMock.get()).thenReturn(changeInfo);
+    }
+
     protected void initComparisonContent() {}
 
     protected String readTestFile(String filename) {