import { describe, expect, test, mock } from 'bun:test'
import { discoverTools, createCachedToolDiscovery } from '../discovery.js'
import type { DiscoveryOptions } from '../discovery.js'
import type { ConnectedMCPServer } from '../types.js'
import type { McpClientDependencies } from '../interfaces.js'

function createMockDeps(): McpClientDependencies {
  return {
    logger: {
      debug: mock(() => {}),
      info: mock(() => {}),
      warn: mock(() => {}),
      error: mock(() => {}),
    },
    httpConfig: {
      getUserAgent: () => 'test-agent/1.0',
    },
  }
}

describe('discoverTools', () => {
  test('returns empty array when capabilities.tools is missing', async () => {
    const result = await discoverTools({
      serverName: 'test',
      client: {} as any,
      capabilities: {},
      deps: createMockDeps(),
    })
    expect(result).toEqual([])
  })

  test('fetches and transforms tools from server', async () => {
    const mockClient = {
      request: mock(() =>
        Promise.resolve({
          tools: [
            {
              name: 'search',
              description: 'Search for items',
              inputSchema: { type: 'object' },
              annotations: { readOnlyHint: true, title: 'Search Items' },
            },
          ],
        }),
      ),
    }

    const result = await discoverTools({
      serverName: 'my-server',
      client: mockClient as any,
      capabilities: { tools: {} },
      deps: createMockDeps(),
    })

    expect(result).toHaveLength(1)
    const tool = result[0]
    expect(tool.name).toBe('mcp__my-server__search')
    expect(tool.mcpInfo).toEqual({
      serverName: 'my-server',
      toolName: 'search',
    })
    expect(tool.isMcp).toBe(true)
    expect(tool.isReadOnly({} as any)).toBe(true)
    expect(tool.userFacingName(undefined)).toBe('Search Items')
    expect(
      await tool.description({} as any, {
        isNonInteractiveSession: false,
        toolPermissionContext: {},
        tools: [],
      }),
    ).toBe('Search for items')
  })

  test('respects skipPrefix option', async () => {
    const mockClient = {
      request: mock(() =>
        Promise.resolve({
          tools: [{ name: 'search', description: 'Search' }],
        }),
      ),
    }

    const result = await discoverTools({
      serverName: 'my-server',
      client: mockClient as any,
      capabilities: { tools: {} },
      skipPrefix: true,
      deps: createMockDeps(),
    })

    expect(result[0].name).toBe('search')
  })

  test('returns empty array on fetch error', async () => {
    const mockClient = {
      request: mock(() => Promise.reject(new Error('Connection lost'))),
    }
    const deps = createMockDeps()

    const result = await discoverTools({
      serverName: 'failing-server',
      client: mockClient as any,
      capabilities: { tools: {} },
      deps,
    })

    expect(result).toEqual([])
    expect(deps.logger.warn).toHaveBeenCalled()
  })

  test('sanitizes tool data', async () => {
    const mockClient = {
      request: mock(() =>
        Promise.resolve({
          tools: [
            {
              name: 'tool\x00with\x07control',
              description: 'desc',
            },
          ],
        }),
      ),
    }

    const result = await discoverTools({
      serverName: 'test',
      client: mockClient as any,
      capabilities: { tools: {} },
      deps: createMockDeps(),
    })

    expect(result[0].name).not.toContain('\x00')
  })
})

describe('createCachedToolDiscovery', () => {
  test('caches results by server name', async () => {
    const deps = createMockDeps()
    const { discover, cache } = createCachedToolDiscovery(deps)

    const mockConn = {
      type: 'connected' as const,
      name: 'cached-server',
      client: {
        request: mock(() =>
          Promise.resolve({
            tools: [{ name: 'tool1', description: 'Tool 1' }],
          }),
        ),
      },
      capabilities: { tools: {} },
    } as unknown as ConnectedMCPServer

    // First call — should fetch
    const result1 = await discover(mockConn)
    expect(result1).toHaveLength(1)

    // Second call — should use cache
    const result2 = await discover(mockConn)
    expect(result2).toHaveLength(1)

    // Request was called only once
    expect(mockConn.client.request).toHaveBeenCalledTimes(1)

    // Cache delete works
    cache.delete('cached-server')
    const result3 = await discover(mockConn)
    expect(result3).toHaveLength(1)
    expect(mockConn.client.request).toHaveBeenCalledTimes(2)
  })
})
