@GetMapping(
        path = "/api/-/search",
        produces = MediaType.APPLICATION_JSON_VALUE
    )
    @CrossOrigin
    @Operation(summary = "Search extensions via text entered by a user")
    @ApiResponses({
        @ApiResponse(
            responseCode = "200",
            description = "The search results are returned in JSON format"
        ),
        @ApiResponse(
            responseCode = "400",
            description = "The request contains an invalid parameter value",
            content = @Content(
                mediaType = MediaType.APPLICATION_JSON_VALUE,
                examples = @ExampleObject(value = "{\"error\": \"The parameter 'size' must not be negative.\"}")
            )
        ),
        @ApiResponse(
            responseCode = "429",
            description = "A client has sent too many requests in a given amount of time",
            content = @Content(),
            headers = {
                @Header(
                    name = "X-Rate-Limit-Retry-After-Seconds",
                    description = "Number of seconds to wait after receiving a 429 response",
                    schema = @Schema(type = "integer", format = "int32")
                ),
                @Header(
                    name = "X-Rate-Limit-Remaining",
                    description = "Remaining number of requests left",
                    schema = @Schema(type = "integer", format = "int32")
                )
            }
        )
    })
    public ResponseEntity<SearchResultJson> search(
            @RequestParam(required = false)
            @Parameter(description = "Query text for searching", example = "javascript")
            String query,
            @RequestParam(required = false)
            @Parameter(description = "Extension category as shown in the UI", example = "Programming Languages")
            String category,
            @RequestParam(required = false)
            @Parameter(
                description = "Target platform",
                example = TargetPlatform.NAME_LINUX_ARM64,
                schema = @Schema(type = "string", allowableValues = {
                    NAME_WIN32_X64, NAME_WIN32_IA32, NAME_WIN32_ARM64,
                    NAME_LINUX_X64, NAME_LINUX_ARM64, NAME_LINUX_ARMHF,
                    NAME_ALPINE_X64, NAME_ALPINE_ARM64,
                    NAME_DARWIN_X64, NAME_DARWIN_ARM64,
                    NAME_WEB, NAME_UNIVERSAL
                })
            )
            String targetPlatform,
            @RequestParam(defaultValue = "18")
            @Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "18"))
            int size,
            @RequestParam(defaultValue = "0")
            @Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
            int offset,
            @RequestParam(defaultValue = "desc") 
            @Parameter(description = "Descending or ascending sort order", schema = @Schema(type = "string", allowableValues = {"asc", "desc"}))
            String sortOrder,
            @RequestParam(defaultValue = "relevance")
            @Parameter(description = "Sort key (relevance is a weighted mix of various properties)", schema = @Schema(type = "string", allowableValues = {"relevance", "timestamp", "averageRating", "downloadCount"}))
            String sortBy,
            @RequestParam(required = false)
            @Parameter(description = "Whether to include information on all available versions for each returned entry")
            boolean includeAllVersions
    ) {
        if (size < 0) {
            var json = SearchResultJson.error("The parameter 'size' must not be negative.");
            return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
        }
        if (offset < 0) {
            var json = SearchResultJson.error("The parameter 'offset' must not be negative.");
            return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
        }

        var options = new ISearchService.Options(query, category, targetPlatform, size, offset, sortOrder, sortBy, includeAllVersions);
        var result = new SearchResultJson();
        result.extensions = new ArrayList<>(size);
        for (var registry : getRegistries()) {
            if (result.extensions.size() >= size) {
                return ResponseEntity.ok(result);
            }
            try {
                var subResult = registry.search(options);
                if(result.extensions.isEmpty() && subResult.extensions != null) {
                    result.extensions.addAll(subResult.extensions);
                } else if (subResult.extensions != null && !subResult.extensions.isEmpty()) {
                    int limit = size - result.extensions.size();
                    var subResultSize = mergeSearchResults(result, subResult.extensions, limit);
                    result.offset += subResult.offset;
                    offset = Math.max(offset - subResult.offset - subResultSize, 0);
                }
                result.totalSize += subResult.totalSize;
            } catch (NotFoundException exc) {
                // Try the next registry
            } catch (ErrorResultException exc) {
                return exc.toResponseEntity(SearchResultJson.class);
            }
        }

        return ResponseEntity.ok()
                .cacheControl(CacheControl.noCache().cachePublic())
                .body(result);
    }
@Test
    public void testSearch() throws Exception {
        var extVersions = mockSearch();
        extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
        Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
                .thenReturn(extVersions);

        mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "foo", "10", "0"))
                .andExpect(status().isOk())
                .andExpect(content().json(searchJson(s -> {
                    s.offset = 0;
                    s.totalSize = 1;
                    var e1 = new SearchEntryJson();
                    e1.namespace = "foo";
                    e1.name = "bar";
                    e1.version = "1.0.0";
                    e1.timestamp = "2000-01-01T10:00Z";
                    e1.displayName = "Foo Bar";
                    s.extensions.add(e1);
                })));
    }