import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ResolvedMcpServer } from './capability-service'

const mocks = vi.hoisted(() => {
  const client = {
    connect: vi.fn(),
    listTools: vi.fn(),
    getServerVersion: vi.fn(),
    close: vi.fn()
  }
  return {
    client,
    Client: vi.fn(function Client() {
      return client
    }),
    StdioClientTransport: vi.fn(function StdioClientTransport(
      options: unknown
    ) {
      return { kind: 'stdio', options }
    }),
    StreamableHTTPClientTransport: vi.fn(
      function StreamableHTTPClientTransport(
        url: URL,
        options: unknown
      ) {
        return { kind: 'http', url, options }
      }
    ),
    SSEClientTransport: vi.fn(function SSEClientTransport(
      url: URL,
      options: unknown
    ) {
      return { kind: 'sse', url, options }
    })
  }
})

vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
  Client: mocks.Client
}))
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
  StdioClientTransport: mocks.StdioClientTransport
}))
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
  StreamableHTTPClientTransport: mocks.StreamableHTTPClientTransport
}))
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
  SSEClientTransport: mocks.SSEClientTransport
}))

import { testMcpServer } from './mcp-tester'

const common = {
  id: 'd2ef774b-146c-4467-a909-6feb112a9c2c',
  name: 'Test MCP',
  description: '',
  enabled: true,
  assignments: ['model'] as Array<'model' | 'opencode' | 'continue'>,
  secretConfigured: false
}

describe('testMcpServer', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.client.connect.mockResolvedValue(undefined)
    mocks.client.listTools.mockResolvedValue({
      tools: [
        {
          name: 'search',
          description: 'Search documents'
        }
      ]
    })
    mocks.client.getServerVersion.mockReturnValue({
      name: 'test-server',
      version: '1.0.0'
    })
    mocks.client.close.mockResolvedValue(undefined)
  })

  it('uses separated stdio command arguments and closes the client', async () => {
    const result = await testMcpServer({
      ...common,
      transport: 'stdio',
      command: 'node',
      args: ['server.js', '--safe']
    } satisfies ResolvedMcpServer)

    expect(mocks.StdioClientTransport).toHaveBeenCalledWith({
      command: 'node',
      args: ['server.js', '--safe'],
      stderr: 'ignore',
      maxBufferSize: 2 * 1024 * 1024
    })
    expect(mocks.client.connect).toHaveBeenCalledOnce()
    expect(mocks.client.listTools).toHaveBeenCalledOnce()
    expect(mocks.client.close).toHaveBeenCalledOnce()
    expect(result).toEqual({
      serverName: 'test-server',
      serverVersion: '1.0.0',
      toolCount: 1,
      tools: [{ name: 'search', description: 'Search documents' }]
    })
  })

  it('injects a bearer token only into the remote transport', async () => {
    await testMcpServer({
      ...common,
      transport: 'http',
      url: 'https://mcp.example.com/mcp',
      secretConfigured: true,
      secret: 'test-secret'
    } satisfies ResolvedMcpServer)

    expect(mocks.StreamableHTTPClientTransport).toHaveBeenCalledOnce()
    const [url, options] =
      mocks.StreamableHTTPClientTransport.mock.calls[0] ?? []
    expect(url).toEqual(new URL('https://mcp.example.com/mcp'))
    expect(options).toMatchObject({
      requestInit: {
        headers: { Authorization: 'Bearer test-secret' }
      },
      reconnectionOptions: { maxRetries: 0 }
    })
    expect(options).not.toHaveProperty('fetch')
  })

  it('closes the client and returns a controlled error on failure', async () => {
    mocks.client.connect.mockRejectedValue(
      new Error('server included sensitive diagnostics')
    )

    await expect(
      testMcpServer({
        ...common,
        transport: 'sse',
        url: 'https://mcp.example.com/sse'
      } satisfies ResolvedMcpServer)
    ).rejects.toThrow(
      'MCP Server 连接失败，请检查地址、命令和服务状态'
    )
    expect(mocks.client.close).toHaveBeenCalledOnce()
  })

  it('accepts cancellation, closes the client, and hides abort details', async () => {
    mocks.client.connect.mockImplementation(
      async (
        _transport: unknown,
        options: { signal: AbortSignal }
      ) =>
        new Promise((_resolve, reject) => {
          options.signal.addEventListener(
            'abort',
            () => reject(new Error('sensitive abort reason')),
            { once: true }
          )
        })
    )
    const controller = new AbortController()
    const pending = testMcpServer(
      {
        ...common,
        transport: 'stdio',
        command: 'node',
        args: ['server.js']
      } satisfies ResolvedMcpServer,
      controller.signal
    )

    controller.abort(new Error('caller private context'))

    await expect(pending).rejects.toThrow('MCP 连接测试已取消')
    expect(mocks.client.listTools).not.toHaveBeenCalled()
    expect(mocks.client.close).toHaveBeenCalledOnce()
  })
})
