001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.benchmarking.common.report;
017
018import com.google.gson.Gson;
019import com.google.gson.GsonBuilder;
020import de.cuioss.benchmarking.common.config.BenchmarkType;
021import de.cuioss.tools.logging.CuiLogger;
022
023import java.io.IOException;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.time.Instant;
027import java.time.ZoneOffset;
028import java.time.format.DateTimeFormatter;
029import java.util.LinkedHashMap;
030import java.util.Locale;
031import java.util.Map;
032
033import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.*;
034import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Arrows;
035import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Colors.*;
036import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.FileNames;
037import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Labels;
038import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Messages;
039import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.TrendDirection;
040import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Grades;
041import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.INFO;
042
043/**
044 * Generates shields.io compatible JSON badge files for benchmark metrics.
045 * <p>
046 * This generator creates three types of badges:
047 * <ul>
048 *   <li>Performance badge - Shows the current performance grade</li>
049 *   <li>Trend badge - Shows the trend direction with percentage change</li>
050 *   <li>Last run badge - Shows when the benchmarks were last executed</li>
051 * </ul>
052 * <p>
053 * All badges follow the shields.io JSON endpoint schema for easy integration
054 * with GitHub README files and documentation.
055 */
056public class BadgeGenerator {
057
058    private static final CuiLogger LOGGER = new CuiLogger(BadgeGenerator.class);
059    private static final DateTimeFormatter DATE_FORMAT =
060            DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC);
061
062    private final Gson gson = new GsonBuilder()
063            .setPrettyPrinting()
064            .create();
065
066    /**
067     * Generates a performance badge JSON based on the benchmark metrics.
068     * Format: "Grade A (45k ops/s, 0.15ms)"
069     *
070     * @param metrics the current benchmark metrics
071     * @return JSON string for the performance badge
072     */
073    public String generatePerformanceBadge(BenchmarkMetrics metrics) {
074        Map<String, Object> badge = new LinkedHashMap<>();
075        badge.put(SCHEMA_VERSION, 1);
076        badge.put(LABEL, Labels.PERFORMANCE);
077
078        String formattedThroughput = formatThroughput(metrics.throughput());
079        String formattedLatency = formatLatency(metrics.latency());
080        String message = String.format(Locale.US, "Grade %s (%s ops/s, %sms)",
081                metrics.performanceGrade(), formattedThroughput, formattedLatency);
082
083        badge.put(MESSAGE, message);
084        badge.put(COLOR, getGradeColor(metrics.performanceGrade()));
085
086        return gson.toJson(badge);
087    }
088
089    /**
090     * Generates a trend badge JSON based on the trend metrics.
091     *
092     * @param trendMetrics the calculated trend metrics
093     * @return JSON string for the trend badge
094     */
095    public String generateTrendBadge(TrendDataProcessor.TrendMetrics trendMetrics) {
096        Map<String, Object> badge = new LinkedHashMap<>();
097        badge.put(SCHEMA_VERSION, 1);
098        badge.put(LABEL, Labels.TREND);
099
100        String arrow = getTrendArrow(trendMetrics.direction());
101        String percentage = String.format(Locale.US, "%.1f%%", Math.abs(trendMetrics.changePercentage()));
102        badge.put(MESSAGE, arrow + " " + percentage);
103        badge.put(COLOR, getTrendColor(trendMetrics.direction()));
104
105        return gson.toJson(badge);
106    }
107
108    /**
109     * Generates a last run timestamp badge.
110     *
111     * @param timestamp the benchmark run timestamp
112     * @return JSON string for the last run badge
113     */
114    public String generateLastRunBadge(Instant timestamp) {
115        Map<String, Object> badge = new LinkedHashMap<>();
116        badge.put(SCHEMA_VERSION, 1);
117        badge.put(LABEL, Labels.LAST_RUN);
118        badge.put(MESSAGE, DATE_FORMAT.format(timestamp));
119        badge.put(COLOR, BLUE);
120
121        return gson.toJson(badge);
122    }
123
124    /**
125     * Generates a default trend badge when no historical data is available.
126     *
127     * @return JSON string for the default trend badge
128     */
129    public String generateDefaultTrendBadge() {
130        Map<String, Object> badge = new LinkedHashMap<>();
131        badge.put(SCHEMA_VERSION, 1);
132        badge.put(LABEL, Labels.TREND);
133        badge.put(MESSAGE, Messages.NO_HISTORY);
134        badge.put(COLOR, LIGHT_GRAY);
135
136        return gson.toJson(badge);
137    }
138
139    /**
140     * Writes all badge files to the specified directory using default MICRO benchmark type.
141     * Convenience overload that defaults to {@link BenchmarkType#MICRO}.
142     *
143     * @param metrics the current benchmark metrics
144     * @param trendMetrics the trend metrics (can be null)
145     * @param outputDir the output directory path
146     * @throws IOException if writing badge files fails
147     */
148    public void writeBadgeFiles(BenchmarkMetrics metrics,
149            TrendDataProcessor.TrendMetrics trendMetrics,
150            String outputDir) throws IOException {
151        writeBadgeFiles(metrics, trendMetrics, BenchmarkType.MICRO, outputDir);
152    }
153
154    /**
155     * Writes all badge files to the specified directory.
156     *
157     * @param metrics the current benchmark metrics
158     * @param trendMetrics the trend metrics (can be null)
159     * @param type the benchmark type (MICRO or INTEGRATION)
160     * @param outputDir the output directory path
161     * @throws IOException if writing badge files fails
162     */
163    public void writeBadgeFiles(BenchmarkMetrics metrics,
164            TrendDataProcessor.TrendMetrics trendMetrics,
165            BenchmarkType type,
166            String outputDir) throws IOException {
167        Path badgesDir = Path.of(outputDir, "badges");
168        Files.createDirectories(badgesDir);
169
170        // Write performance badge with appropriate file name based on benchmark type
171        String perfBadge = generatePerformanceBadge(metrics);
172        Path perfBadgePath = badgesDir.resolve(type.getPerformanceBadgeFileName());
173        Files.writeString(perfBadgePath, perfBadge);
174        LOGGER.info(INFO.BADGE_GENERATED, "performance", perfBadgePath);
175
176        // Write trend badge with appropriate file name based on benchmark type
177        String trendBadge = trendMetrics != null
178                ? generateTrendBadge(trendMetrics)
179                : generateDefaultTrendBadge();
180        Path trendBadgePath = badgesDir.resolve(type.getTrendBadgeFileName());
181        Files.writeString(trendBadgePath, trendBadge);
182        LOGGER.info(INFO.BADGE_GENERATED, "trend", trendBadgePath);
183
184        // Write last run badge (same for both types)
185        String lastRunBadge = generateLastRunBadge(Instant.now());
186        Path lastRunBadgePath = badgesDir.resolve(FileNames.LAST_RUN_BADGE_JSON);
187        Files.writeString(lastRunBadgePath, lastRunBadge);
188        LOGGER.info(INFO.BADGE_GENERATED, "last-run", lastRunBadgePath);
189    }
190
191    /**
192     * Determines the color for a performance grade.
193     */
194    private String getGradeColor(String grade) {
195        return switch (grade) {
196            case Grades.A_PLUS -> BRIGHT_GREEN;
197            case Grades.A -> GREEN;
198            case Grades.B -> YELLOW_GREEN;
199            case Grades.C -> YELLOW;
200            case Grades.D -> ORANGE;
201            case Grades.F -> RED;
202            default -> LIGHT_GRAY;
203        };
204    }
205
206    /**
207     * Determines the color for a trend direction.
208     */
209    private String getTrendColor(String direction) {
210        return switch (direction) {
211            case TrendDirection.UP -> GREEN;
212            case TrendDirection.DOWN -> RED;
213            case TrendDirection.STABLE -> BLUE;
214            default -> LIGHT_GRAY;
215        };
216    }
217
218    /**
219     * Gets the arrow symbol for a trend direction.
220     */
221    private String getTrendArrow(String direction) {
222        return switch (direction) {
223            case TrendDirection.UP -> Arrows.UP;
224            case TrendDirection.DOWN -> Arrows.DOWN;
225            case TrendDirection.STABLE -> Arrows.RIGHT;
226            default -> Arrows.BULLET;
227        };
228    }
229
230    /**
231     * Formats throughput value with appropriate unit (k for thousands).
232     * Examples: 500 -> "500", 1500 -> "1.5k", 45000 -> "45k"
233     */
234    private String formatThroughput(double throughput) {
235        if (throughput >= 1000) {
236            double kThroughput = throughput / 1000.0;
237            if (kThroughput >= 10) {
238                // For values >= 10k, no decimal places
239                return String.format(Locale.US, "%.0fk", kThroughput);
240            } else {
241                // For values < 10k, show one decimal if not .0
242                String formatted = String.format(Locale.US, "%.1fk", kThroughput);
243                return formatted.endsWith(".0k") ?
244                        formatted.substring(0, formatted.length() - 3) + "k" : formatted;
245            }
246        } else {
247            // For values < 1000, show as integer
248            return String.format(Locale.US, "%.0f", throughput);
249        }
250    }
251
252    /**
253     * Formats latency value with appropriate precision.
254     * For values < 1ms: 2 decimals, for values >= 1ms: 2 decimals with trailing zeros
255     */
256    private String formatLatency(double latency) {
257        return String.format(Locale.US, "%.2f", latency);
258    }
259}