Apply restricted voting range

If a specific PatchSet imposes a voting range on ChatGPT Gerrit user
that is more restrictive than the default bounds set by `votingMinScore`
and `votingMaxScore`, this narrower range takes precedence.

Jira-Id: IT-103
Change-Id: Iba796b79d39cc88a1e4567c685b16172fe7073d4
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
index e2d7755..63027b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -5,8 +5,10 @@
 import com.googlesource.gerrit.plugins.chatgpt.client.*;
 import com.googlesource.gerrit.plugins.chatgpt.client.chatgpt.ChatGptClient;
 import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClient;
+import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClientDetail;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.chatGpt.ChatGptReplyItem;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.chatGpt.ChatGptResponseContent;
+import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritPatchSetDetail;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritCodeRange;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritComment;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.ReviewBatch;
@@ -45,9 +47,7 @@
             log.info("No file to review has been found in the PatchSet");
             return;
         }
-        config.configureDynamically(Configuration.KEY_GPT_REQUEST_USER_PROMPT,
-                gerritClient.getUserRequests(fullChangeId));
-        config.configureDynamically(Configuration.KEY_COMMENT_PROPERTIES_SIZE, commentProperties.size());
+        updateDynamicConfiguration(config, fullChangeId);
 
         String reviewReply = getReviewReply(config, fullChangeId, patchSet);
         log.debug("ChatGPT response: {}", reviewReply);
@@ -57,6 +57,28 @@
         gerritClient.setReview(fullChangeId, reviewBatches, reviewJson.getScore());
     }
 
+    private void updateDynamicConfiguration(Configuration config, String fullChangeId) {
+        config.configureDynamically(Configuration.KEY_GPT_REQUEST_USER_PROMPT,
+                gerritClient.getUserRequests(fullChangeId));
+        config.configureDynamically(Configuration.KEY_COMMENT_PROPERTIES_SIZE, commentProperties.size());
+        if (config.isVotingEnabled() && !isCommentEvent) {
+            GerritClientDetail gerritClientDetail = new GerritClientDetail(config,
+                    gerritClient.getGptAccountId(fullChangeId));
+            GerritPatchSetDetail.PermittedVotingRange permittedVotingRange = gerritClientDetail.getPermittedVotingRange(
+                    fullChangeId);
+            if (permittedVotingRange != null) {
+                if (permittedVotingRange.getMin() > config.getVotingMinScore()) {
+                    log.debug("Minimum ChatGPT voting score set to {}", permittedVotingRange.getMin());
+                    config.configureDynamically(Configuration.KEY_VOTING_MIN_SCORE, permittedVotingRange.getMin());
+                }
+                if (permittedVotingRange.getMax() < config.getVotingMaxScore()) {
+                    log.debug("Maximum ChatGPT voting score set to {}", permittedVotingRange.getMax());
+                    config.configureDynamically(Configuration.KEY_VOTING_MAX_SCORE, permittedVotingRange.getMax());
+                }
+            }
+        }
+    }
+
     private void addReviewBatch(Integer batchID, String batch) {
         ReviewBatch batchMap = new ReviewBatch();
         batchMap.setContent(batch);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
index aff6657..4273038 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
@@ -47,6 +47,10 @@
         return gerritSetChangesUri(fullChangeId, "/revisions/current/review");
     }
 
+    public static String gerritGetPatchSetDetailUri(String fullChangeId) {
+        return gerritSetChangesUri(fullChangeId, "/detail");
+    }
+
     public static String chatCompletionsUri() {
         return "/v1/chat/completions";
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
index ccbcdaa..b710afc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
@@ -67,6 +67,11 @@
         return gerritClientPatchSet.getFileDiffsProcessed();
     }
 
+    public Integer getGptAccountId(String fullChangeId) {
+        updateGerritClient(GerritClientType.COMMENTS, fullChangeId);
+        return gerritClientComments.getGptAccountId();
+    }
+
     public List<GerritComment> getCommentProperties(String fullChangeId) {
         updateGerritClient(GerritClientType.COMMENTS, fullChangeId);
         return gerritClientComments.getCommentProperties();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
index da35b89..772d598 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
@@ -29,7 +29,8 @@
     private static final Integer MAX_SECS_GAP_BETWEEN_EVENT_AND_COMMENT = 2;
 
     private final Gson gson = new Gson();
-    private final int gptAccountId;
+    @Getter
+    private final Integer gptAccountId;
     private final HashMap<String, GerritComment> commentMap;
     private long commentsStartTimestamp;
     private String authorUsername;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientDetail.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientDetail.java
new file mode 100644
index 0000000..55dbeae
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientDetail.java
@@ -0,0 +1,54 @@
+package com.googlesource.gerrit.plugins.chatgpt.client.gerrit;
+
+import com.google.gson.Gson;
+import com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator;
+import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritPatchSetDetail;
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URI;
+import java.util.List;
+
+@Slf4j
+public class GerritClientDetail extends GerritClientAccount {
+
+    private final Gson gson = new Gson();
+    private final Integer gptAccountId;
+
+    public GerritClientDetail(Configuration config, Integer gptAccountId) {
+        super(config);
+        this.gptAccountId = gptAccountId;
+    }
+
+
+    public GerritPatchSetDetail.PermittedVotingRange getPermittedVotingRange(String fullChangeId) {
+        GerritPatchSetDetail gerritPatchSetDetail;
+        try {
+            gerritPatchSetDetail = getReviewDetail(fullChangeId);
+        }
+        catch (Exception e) {
+            log.debug("Error retrieving PatchSet details", e);
+            return null;
+        }
+        List<GerritPatchSetDetail.Permission> permissions = gerritPatchSetDetail.getLabels().getCodeReview().getAll();
+        if (permissions == null) {
+            log.debug("No limitations on the ChatGPT voting range were detected");
+            return null;
+        }
+        for (GerritPatchSetDetail.Permission permission : permissions) {
+            if (permission.getAccountId() == gptAccountId) {
+                log.debug("PatchSet voting range detected for ChatGPT user: {}", permission.getPermittedVotingRange());
+                return permission.getPermittedVotingRange();
+            }
+        }
+        return null;
+    }
+
+    private GerritPatchSetDetail getReviewDetail(String fullChangeId) throws Exception {
+        URI uri = URI.create(config.getGerritAuthBaseUrl()
+                + UriResourceLocator.gerritGetPatchSetDetailUri(fullChangeId));
+        String responseBody = forwardGetRequest(uri);
+        return gson.fromJson(responseBody, GerritPatchSetDetail.class);
+    }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/gerrit/GerritPatchSetDetail.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/gerrit/GerritPatchSetDetail.java
new file mode 100644
index 0000000..2570f4c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/gerrit/GerritPatchSetDetail.java
@@ -0,0 +1,39 @@
+package com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class GerritPatchSetDetail {
+    private Labels labels;
+
+    @Data
+    public static class Labels {
+        @SerializedName("Code-Review")
+        private CodeReview codeReview;
+    }
+
+    @Data
+    public static class CodeReview {
+        private List<Permission> all;
+    }
+
+    @Data
+    public static class Permission {
+        private Integer value;
+        private String date;
+        @SerializedName("permitted_voting_range")
+        private PermittedVotingRange permittedVotingRange;
+        @SerializedName("_account_id")
+        private int accountId;
+    }
+
+    @Data
+    public static class PermittedVotingRange {
+        private int min;
+        private int max;
+    }
+
+}
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 f953df9..92c4e9e 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
@@ -77,6 +77,8 @@
     public static final String KEY_GPT_SYSTEM_PROMPT = "gptSystemPrompt";
     public static final String KEY_GPT_REQUEST_USER_PROMPT = "gptRequestUserPrompt";
     public static final String KEY_COMMENT_PROPERTIES_SIZE = "commentPropertiesSize";
+    public static final String KEY_VOTING_MIN_SCORE = "votingMinScore";
+    public static final String KEY_VOTING_MAX_SCORE = "votingMaxScore";
     private static final String KEY_GPT_TOKEN = "gptToken";
     private static final String KEY_GERRIT_AUTH_BASE_URL = "gerritAuthBaseUrl";
     private static final String KEY_GERRIT_USERNAME = "gerritUserName";
@@ -101,8 +103,6 @@
     private static final String KEY_MAX_REVIEW_FILE_SIZE = "maxReviewFileSize";
     private static final String KEY_ENABLED_FILE_EXTENSIONS = "enabledFileExtensions";
     private static final String KEY_ENABLED_VOTING = "enabledVoting";
-    private static final String KEY_VOTING_MIN_SCORE = "votingMinScore";
-    private static final String KEY_VOTING_MAX_SCORE = "votingMaxScore";
 
     // Prompt constants loaded from JSON file
     public static String DEFAULT_GPT_SYSTEM_PROMPT;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
index a0feb6a..0f40ec4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
@@ -168,6 +168,13 @@
                         .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
                         .withBody("{\"revisions\":{\"aa5be5ebb80846475ec4dfe43e0799eb73c6415a\":{}}}")));
 
+        // Mock the behavior of the gerritGetPatchSetDetailUri request
+        WireMock.stubFor(WireMock.get(gerritGetPatchSetDetailUri(fullChangeId))
+                .willReturn(WireMock.aResponse()
+                        .withStatus(HTTP_OK)
+                        .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
+                        .withBodyFile("gerritPatchSetDetail.json")));
+
         // Mock the behavior of the gerritPatchSetFiles request
         WireMock.stubFor(WireMock.get(gerritPatchSetFilesUri(fullChangeId))
                 .willReturn(WireMock.aResponse()
diff --git a/src/test/resources/__files/gerritPatchSetDetail.json b/src/test/resources/__files/gerritPatchSetDetail.json
new file mode 100644
index 0000000..8c4ca4d
--- /dev/null
+++ b/src/test/resources/__files/gerritPatchSetDetail.json
@@ -0,0 +1,39 @@
+{
+  "labels": {
+    "Code-Review": {
+      "all": [
+        {
+          "value": 1,
+          "date": "2024-02-06 07:38:58.000000000",
+          "permitted_voting_range": {
+            "min": -1,
+            "max": 1
+          },
+          "_account_id": 1000001,
+          "name": "ChatGPT",
+          "display_name": "ChatGPT",
+          "username": "gpt"
+        }
+      ],
+      "values": {
+        "-2": "This shall not be submitted",
+        "-1": "I would prefer this is not submitted as is",
+        " 0": "No score",
+        "+1": "Looks good to me, but someone else must approve",
+        "+2": "Looks good to me, approved"
+      },
+      "description": "",
+      "value": 1,
+      "default_value": 0
+    }
+  },
+  "permitted_labels": {
+    "Code-Review": [
+      "-2",
+      "-1",
+      " 0",
+      "+1",
+      "+2"
+    ]
+  }
+}
\ No newline at end of file