/*
 * Copyright 1999-2025 Alibaba Group Holding Ltd.
 *
 * 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.alibaba.nacos.mcpregistry.service;

import com.alibaba.nacos.ai.constant.Constants;
import com.alibaba.nacos.ai.index.McpServerIndex;
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
import com.alibaba.nacos.ai.service.McpServerOperationService;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
import com.alibaba.nacos.api.ai.model.mcp.registry.McpRegistryServer;
import com.alibaba.nacos.api.ai.model.mcp.registry.McpRegistryServerDetail;
import com.alibaba.nacos.api.ai.model.mcp.registry.McpRegistryServerList;
import com.alibaba.nacos.api.ai.model.mcp.registry.ServerVersionDetail;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.model.Page;
import com.alibaba.nacos.api.model.response.Namespace;
import com.alibaba.nacos.core.service.NamespaceOperationService;
import com.alibaba.nacos.mcpregistry.form.GetServerForm;
import com.alibaba.nacos.mcpregistry.form.ListServerForm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class NacosMcpRegistryServiceTest {
    
    private static final String RANDOM_NAMESPACE_ID = UUID.randomUUID().toString();
    
    @Mock
    private McpServerOperationService mcpServerOperationService;
    
    @Mock
    private NamespaceOperationService namespaceOperationService;
    
    @Mock
    private McpServerIndex mcpServerIndex;
    
    NacosMcpRegistryService mcpRegistryService;
    
    @BeforeEach
    void setUp() {
        mcpRegistryService = new NacosMcpRegistryService(mcpServerOperationService, namespaceOperationService,
                mcpServerIndex);
    }
    
    @AfterEach
    void tearDown() {
    }
    
    @Test
    void listMcpServersWithZeroOffset() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(0);
        listServerForm.setLimit(0);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 1, 2, 0);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 0, 1, 10, 0);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertTrue(actual.getServers().isEmpty());
    }
    
    @Test
    void listMcpServersWithOffsetLargeThenTotalCount() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(100);
        listServerForm.setLimit(10);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 90, 10, 2, 0);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 100, 10, 10, 0);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertTrue(actual.getServers().isEmpty());
    }
    
    @Test
    void listMcpServersWithoutOffsetAndLargeOffset() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(0);
        listServerForm.setLimit(100);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 90, 2, 2);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 0, 100, 10, 10);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(12, actual.getServers().size());
    }
    
    @Test
    void listMcpServerWithoutOffsetAndSmallLimit() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(0);
        listServerForm.setLimit(5);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 1, 2, 0);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 0, 5, 10, 5);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(5, actual.getServers().size());
        for (McpRegistryServer each : actual.getServers()) {
            assertTrue(each.getDescription().endsWith(RANDOM_NAMESPACE_ID));
        }
    }
    
    @Test
    void listMcpServerWithoutOffsetAndLimitOverNamespace() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(0);
        listServerForm.setLimit(11);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 1, 2, 1);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 0, 11, 10, 10);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(11, actual.getServers().size());
        for (int i = 0; i < 10; i++) {
            assertTrue(actual.getServers().get(i).getDescription().endsWith(RANDOM_NAMESPACE_ID));
        }
        assertTrue(actual.getServers().get(10).getDescription().endsWith(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE));
    }
    
    @Test
    void listMcpServerWithOffsetAndLargeLimit() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(5);
        listServerForm.setLimit(100);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 90, 2, 2);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 5, 100, 10, 10);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(12, actual.getServers().size());
    }
    
    @Test
    void listMcpServerWithOffsetAndSmallLimit() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(5);
        listServerForm.setLimit(4);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 1, 2, 1);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 5, 4, 10, 4);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(4, actual.getServers().size());
        for (McpRegistryServer each : actual.getServers()) {
            assertTrue(each.getDescription().endsWith(RANDOM_NAMESPACE_ID));
        }
    }
    
    @Test
    void listMcpServerWithOffsetAndLimitOverNamespace() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(5);
        listServerForm.setLimit(6);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 1, 2, 1);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 5, 6, 10, 5);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(6, actual.getServers().size());
        for (int i = 0; i < 5; i++) {
            assertTrue(actual.getServers().get(i).getDescription().endsWith(RANDOM_NAMESPACE_ID));
        }
        assertTrue(actual.getServers().get(5).getDescription().endsWith(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE));
    }
    
    @Test
    void listMcpServerWithOffsetOverNamespace() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(10);
        listServerForm.setLimit(10);
        mockMultipleNamespace();
        mockListMcpServerWithOffset(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, 0, 10, 2, 2);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 10, 10, 10, 0);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(2, actual.getServers().size());
        for (McpRegistryServer each : actual.getServers()) {
            assertTrue(each.getDescription().endsWith(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE));
        }
    }
    
    @Test
    void listMcpServerForTargetNamespace() {
        ListServerForm listServerForm = new ListServerForm();
        listServerForm.setOffset(0);
        listServerForm.setLimit(30);
        listServerForm.setNamespaceId(RANDOM_NAMESPACE_ID);
        listServerForm.setServerName(null);
        mockListMcpServerWithOffset(RANDOM_NAMESPACE_ID, 0, 30, 10, 10);
        McpRegistryServerList actual = mcpRegistryService.listMcpServers(listServerForm);
        assertEquals(10, actual.getServers().size());
    }
    
    @Test
    void getServerNotFound() throws NacosException {
        String id = UUID.randomUUID().toString();
        assertNull(mcpRegistryService.getServer(id, new GetServerForm()));
    }
    
    @Test
    void getServerWithoutBackendEndpoints() throws NacosException {
        String id = UUID.randomUUID().toString();
        McpServerIndexData mcpServerIndexData = new McpServerIndexData();
        mcpServerIndexData.setId(id);
        mcpServerIndexData.setNamespaceId(RANDOM_NAMESPACE_ID);
        when(mcpServerIndex.getMcpServerById(id)).thenReturn(mcpServerIndexData);
        when(mcpServerOperationService.getMcpServerDetail(RANDOM_NAMESPACE_ID, id, null, null)).thenReturn(
                mockMcpServerDetailInfo(id, RANDOM_NAMESPACE_ID, false, false));
        McpRegistryServerDetail result = mcpRegistryService.getServer(id, new GetServerForm());
        assertEquals("mockMcpServer", result.getName());
        assertEquals("Description:" + RANDOM_NAMESPACE_ID, result.getDescription());
        assertNull(result.getRepository());
        assertEquals("1.0.0", result.getVersion());
        assertNotNull(result.getMeta());
        assertNotNull(result.getMeta().getOfficial());
        assertEquals("2025-06-10T02:29:17Z", result.getMeta().getOfficial().getPublishedAt());
        assertNull(result.getRemotes());
    }
    
    @Test
    void getServerWithBackendEndpoints() throws NacosException {
        String id = UUID.randomUUID().toString();
        McpServerIndexData mcpServerIndexData = new McpServerIndexData();
        mcpServerIndexData.setId(id);
        mcpServerIndexData.setNamespaceId(RANDOM_NAMESPACE_ID);
        when(mcpServerIndex.getMcpServerById(id)).thenReturn(mcpServerIndexData);
        when(mcpServerOperationService.getMcpServerDetail(RANDOM_NAMESPACE_ID, id, null, null)).thenReturn(
                mockMcpServerDetailInfo(id, RANDOM_NAMESPACE_ID, true, false));
        McpRegistryServerDetail result = mcpRegistryService.getServer(id, new GetServerForm());
        assertEquals("mockMcpServer", result.getName());
        assertEquals("Description:" + RANDOM_NAMESPACE_ID, result.getDescription());
        assertNull(result.getRepository());
        assertEquals("1.0.0", result.getVersion());
        assertNotNull(result.getMeta());
        assertNotNull(result.getMeta().getOfficial());
        assertEquals("2025-06-10T02:29:17Z", result.getMeta().getOfficial().getPublishedAt());
        assertNotNull(result.getRemotes());
        assertEquals(1, result.getRemotes().size());
        assertEquals("sse", result.getRemotes().get(0).getType());
        assertEquals("http://127.0.0.1:8080/api/path", result.getRemotes().get(0).getUrl());
    }
    
    @Test
    void getToolsNotFound() throws NacosException {
        String id = UUID.randomUUID().toString();
        assertNull(mcpRegistryService.getTools(id, null));
    }
    
    @Test
    void getTools() throws NacosException {
        String id = UUID.randomUUID().toString();
        McpServerIndexData mcpServerIndexData = new McpServerIndexData();
        mcpServerIndexData.setId(id);
        mcpServerIndexData.setNamespaceId(RANDOM_NAMESPACE_ID);
        when(mcpServerIndex.getMcpServerById(id)).thenReturn(mcpServerIndexData);
        when(mcpServerOperationService.getMcpServerDetail(RANDOM_NAMESPACE_ID, id, null, null)).thenReturn(
                mockMcpServerDetailInfo(id, RANDOM_NAMESPACE_ID, false, true));
        assertNotNull(mcpRegistryService.getTools(id, null));
    }
    
    private void mockMultipleNamespace() {
        Namespace namespace1 = new Namespace(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE,
                AiConstants.Mcp.MCP_DEFAULT_NAMESPACE);
        Namespace namespace2 = new Namespace(RANDOM_NAMESPACE_ID, "test");
        List<Namespace> namespaces = List.of(namespace1, namespace2);
        when(namespaceOperationService.getNamespaceList()).thenReturn(namespaces);
    }
    
    private void mockListMcpServerWithOffset(String namespaceId, int offset, int limit, int totalCount,
            int actualSize) {
        Page<McpServerBasicInfo> mockPage = new Page<>();
        mockPage.setTotalCount(totalCount);
        List<McpServerBasicInfo> mcpServerBasicInfos = new LinkedList<>();
        mockPage.setPageItems(mcpServerBasicInfos);
        for (int i = 0; i < actualSize; i++) {
            McpServerBasicInfo basicInfo = mockMcpServerBasicInfo(i, namespaceId);
            mockPage.getPageItems().add(basicInfo);
            // ensure getServer won't return null by mocking index and detail lookup for
            // each generated id
            McpServerIndexData indexData = new McpServerIndexData();
            indexData.setId(basicInfo.getId());
            indexData.setNamespaceId(namespaceId);
            Mockito.lenient().when(mcpServerIndex.getMcpServerById(basicInfo.getId())).thenReturn(indexData);
            // default detail mocks without backend endpoints or tools
            try {
                Mockito.lenient()
                        .when(mcpServerOperationService.getMcpServerDetail(namespaceId, basicInfo.getId(), null, null))
                        .thenReturn(mockMcpServerDetailInfo(basicInfo.getId(), namespaceId, false, false));
            } catch (NacosException e) {
                throw new RuntimeException(e);
            }
        }
        when(mcpServerOperationService.listMcpServerWithOffset(namespaceId, null, Constants.MCP_LIST_SEARCH_BLUR,
                offset, limit)).thenReturn(mockPage);
    }
    
    private McpServerBasicInfo mockMcpServerBasicInfo(int number, String namespaceId) {
        String id = UUID.randomUUID().toString();
        String actualServerName = "mockMcpServer:" + number;
        McpServerBasicInfo result = new McpServerBasicInfo();
        result.setId(id);
        result.setName(actualServerName);
        result.setDescription("Description:" + namespaceId);
        result.setVersionDetail(new ServerVersionDetail());
        result.getVersionDetail().setVersion("1.0.0");
        result.getVersionDetail().setIs_latest(true);
        result.getVersionDetail().setRelease_date("2025-06-10T02:29:17Z");
        return result;
    }
    
    private McpServerDetailInfo mockMcpServerDetailInfo(String id, String namespaceId, boolean withBackendEndpoints,
            boolean withTools) {
        McpServerDetailInfo result = new McpServerDetailInfo();
        result.setId(id);
        result.setName("mockMcpServer");
        result.setDescription("Description:" + namespaceId);
        result.setVersionDetail(new ServerVersionDetail());
        result.getVersionDetail().setVersion("1.0.0");
        result.getVersionDetail().setIs_latest(true);
        result.getVersionDetail().setRelease_date("2025-06-10T02:29:17Z");
        result.setFrontProtocol(
                withBackendEndpoints ? AiConstants.Mcp.MCP_PROTOCOL_SSE : AiConstants.Mcp.MCP_PROTOCOL_STDIO);
        if (withBackendEndpoints) {
            McpEndpointInfo mcpEndpointInfo = new McpEndpointInfo();
            mcpEndpointInfo.setAddress("127.0.0.1");
            mcpEndpointInfo.setPort(8080);
            mcpEndpointInfo.setPath("/api/path");
            List<McpEndpointInfo> endpoints = new LinkedList<>();
            endpoints.add(mcpEndpointInfo);
            result.setBackendEndpoints(endpoints);
        }
        if (withTools) {
            result.setToolSpec(new McpToolSpecification());
        }
        return result;
    }
}