import { describe, it, expect, vi, beforeEach } from 'vitest';
import { McpRouter } from '../src/router.js';
import type { UpstreamConnection, JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from '../src/types.js';
import type { McpdClient } from '../src/http/mcpd-client.js';
import { createGatePlugin } from '../src/proxymodel/plugins/gate.js';
import { MemoryCache } from '../src/proxymodel/cache.js';

function mockUpstream(name: string, opts?: {
  tools?: Array<{ name: string; description?: string; inputSchema?: unknown }>;
}): UpstreamConnection {
  return {
    name,
    isAlive: vi.fn(() => true),
    close: vi.fn(async () => {}),
    onNotification: vi.fn(),
    send: vi.fn(async (req: JsonRpcRequest): Promise<JsonRpcResponse> => {
      if (req.method === 'tools/list') {
        return { jsonrpc: '2.0', id: req.id, result: { tools: opts?.tools ?? [] } };
      }
      if (req.method === 'resources/list') {
        return { jsonrpc: '2.0', id: req.id, result: { resources: [] } };
      }
      return { jsonrpc: '2.0', id: req.id, result: {} };
    }),
  };
}

function mockMcpdClient(): McpdClient {
  return {
    get: vi.fn(async () => []),
    post: vi.fn(async () => ({})),
    put: vi.fn(async () => ({})),
    delete: vi.fn(async () => {}),
    forward: vi.fn(async () => ({ status: 200, body: {} })),
    withHeaders: vi.fn(function (this: McpdClient) { return this; }),
  } as unknown as McpdClient;
}

describe('McpRouter - Prompt Integration', () => {
  let router: McpRouter;
  let mcpdClient: McpdClient;

  beforeEach(() => {
    router = new McpRouter();
    mcpdClient = mockMcpdClient();
  });

  describe('propose_prompt tool', () => {
    /**
     * propose_prompt is a virtual tool registered by the gate plugin.
     * These tests set up a gate plugin to test the propose_prompt functionality.
     */
    function setupWithPlugin(projectName: string): void {
      router.setPromptConfig(mcpdClient, projectName);
      const plugin = createGatePlugin({ gated: false });
      router.setPlugin(plugin);
      router.setProxyModel('default', { complete: async () => '', available: () => false }, new MemoryCache());
    }

    it('should include propose_prompt in tools/list when plugin is set', async () => {
      setupWithPlugin('test-project');
      router.addUpstream(mockUpstream('server1'));

      // Initialize to create session context
      await router.route(
        { jsonrpc: '2.0', id: 0, method: 'initialize' },
        { sessionId: 'sess-1' },
      );

      const response = await router.route(
        { jsonrpc: '2.0', id: 1, method: 'tools/list' },
        { sessionId: 'sess-1' },
      );

      const tools = (response.result as { tools: Array<{ name: string }> }).tools;
      expect(tools.some((t) => t.name === 'propose_prompt')).toBe(true);
    });

    it('should NOT include propose_prompt when no plugin', async () => {
      router.addUpstream(mockUpstream('server1'));

      const response = await router.route({
        jsonrpc: '2.0',
        id: 1,
        method: 'tools/list',
      });

      const tools = (response.result as { tools: Array<{ name: string }> }).tools;
      expect(tools.some((t) => t.name === 'propose_prompt')).toBe(false);
    });

    it('should call mcpd to create a prompt request', async () => {
      setupWithPlugin('my-project');

      // Initialize to create session context with virtual tools
      await router.route(
        { jsonrpc: '2.0', id: 0, method: 'initialize' },
        { sessionId: 'sess-123' },
      );

      const response = await router.route(
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'propose_prompt',
            arguments: { name: 'my-prompt', content: 'Hello world' },
          },
        },
        { sessionId: 'sess-123' },
      );

      expect(response.error).toBeUndefined();
      expect(mcpdClient.post).toHaveBeenCalledWith(
        '/api/v1/projects/my-project/promptrequests',
        { name: 'my-prompt', content: 'Hello world', createdBySession: 'sess-123' },
      );
    });

    it('should return error when name or content missing', async () => {
      setupWithPlugin('proj');

      await router.route(
        { jsonrpc: '2.0', id: 0, method: 'initialize' },
        { sessionId: 'sess-1' },
      );

      const response = await router.route(
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'propose_prompt',
            arguments: { name: 'only-name' },
          },
        },
        { sessionId: 'sess-1' },
      );

      expect(response.error?.code).toBe(-32602);
      expect(response.error?.message).toContain('Missing required arguments');
    });

    it('should return error when mcpd call fails', async () => {
      setupWithPlugin('proj');
      vi.mocked(mcpdClient.post).mockRejectedValue(new Error('mcpd returned 409'));

      await router.route(
        { jsonrpc: '2.0', id: 0, method: 'initialize' },
        { sessionId: 'sess-1' },
      );

      const response = await router.route(
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'propose_prompt',
            arguments: { name: 'dup', content: 'x' },
          },
        },
        { sessionId: 'sess-1' },
      );

      expect(response.error?.code).toBe(-32603);
      expect(response.error?.message).toContain('mcpd returned 409');
    });
  });

  describe('prompt resources', () => {
    it('should include prompt resources in resources/list', async () => {
      router.setPromptConfig(mcpdClient, 'test-project');
      vi.mocked(mcpdClient.get).mockResolvedValue([
        { name: 'approved-prompt', content: 'Content A', type: 'prompt' },
        { name: 'pending-req', content: 'Content B', type: 'promptrequest' },
      ]);

      const response = await router.route(
        { jsonrpc: '2.0', id: 1, method: 'resources/list' },
        { sessionId: 'sess-1' },
      );

      const resources = (response.result as { resources: Array<{ uri: string; description?: string }> }).resources;
      expect(resources).toHaveLength(2);
      expect(resources[0]!.uri).toBe('mcpctl://prompts/approved-prompt');
      expect(resources[0]!.description).toContain('Approved');
      expect(resources[1]!.uri).toBe('mcpctl://prompts/pending-req');
      expect(resources[1]!.description).toContain('Pending');
    });

    it('should pass session ID when fetching visible prompts', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      vi.mocked(mcpdClient.get).mockResolvedValue([]);

      await router.route(
        { jsonrpc: '2.0', id: 1, method: 'resources/list' },
        { sessionId: 'my-session' },
      );

      expect(mcpdClient.get).toHaveBeenCalledWith(
        '/api/v1/projects/proj/prompts/visible?session=my-session',
      );
    });

    it('should read mcpctl resource content live from mcpd', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      vi.mocked(mcpdClient.get).mockResolvedValue([
        { name: 'my-prompt', content: 'The content here', type: 'prompt' },
      ]);

      // Read directly — no need to list first
      const response = await router.route({
        jsonrpc: '2.0',
        id: 2,
        method: 'resources/read',
        params: { uri: 'mcpctl://prompts/my-prompt' },
      });

      expect(response.error).toBeUndefined();
      const contents = (response.result as { contents: Array<{ text: string }> }).contents;
      expect(contents[0]!.text).toBe('The content here');
    });

    it('should return fresh content after prompt update', async () => {
      router.setPromptConfig(mcpdClient, 'proj');

      // First call returns old content
      vi.mocked(mcpdClient.get).mockResolvedValueOnce([
        { name: 'my-prompt', content: 'Old content', type: 'prompt' },
      ]);
      await router.route({
        jsonrpc: '2.0', id: 1, method: 'resources/read',
        params: { uri: 'mcpctl://prompts/my-prompt' },
      });

      // Second call returns updated content
      vi.mocked(mcpdClient.get).mockResolvedValueOnce([
        { name: 'my-prompt', content: 'Updated content', type: 'prompt' },
      ]);
      const response = await router.route({
        jsonrpc: '2.0', id: 2, method: 'resources/read',
        params: { uri: 'mcpctl://prompts/my-prompt' },
      });

      const contents = (response.result as { contents: Array<{ text: string }> }).contents;
      expect(contents[0]!.text).toBe('Updated content');
    });

    it('should fall back to cache when mcpd is unreachable on read', async () => {
      router.setPromptConfig(mcpdClient, 'proj');

      // Populate cache via list
      vi.mocked(mcpdClient.get).mockResolvedValueOnce([
        { name: 'cached-prompt', content: 'Cached content', type: 'prompt' },
      ]);
      await router.route({ jsonrpc: '2.0', id: 1, method: 'resources/list' });

      // mcpd goes down for read
      vi.mocked(mcpdClient.get).mockRejectedValueOnce(new Error('Connection refused'));
      const response = await router.route({
        jsonrpc: '2.0', id: 2, method: 'resources/read',
        params: { uri: 'mcpctl://prompts/cached-prompt' },
      });

      expect(response.error).toBeUndefined();
      const contents = (response.result as { contents: Array<{ text: string }> }).contents;
      expect(contents[0]!.text).toBe('Cached content');
    });

    it('should return error for unknown mcpctl resource', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      vi.mocked(mcpdClient.get).mockResolvedValue([]);

      const response = await router.route({
        jsonrpc: '2.0',
        id: 3,
        method: 'resources/read',
        params: { uri: 'mcpctl://prompts/nonexistent' },
      });

      expect(response.error?.code).toBe(-32602);
      expect(response.error?.message).toContain('Resource not found');
    });

    it('should not fail when mcpd is unavailable', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      vi.mocked(mcpdClient.get).mockRejectedValue(new Error('Connection refused'));

      const response = await router.route({ jsonrpc: '2.0', id: 1, method: 'resources/list' });

      // Should succeed with empty resources (upstream errors are swallowed)
      expect(response.error).toBeUndefined();
      const resources = (response.result as { resources: unknown[] }).resources;
      expect(resources).toEqual([]);
    });
  });

  describe('session isolation', () => {
    it('should not include session parameter when no sessionId in context', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      vi.mocked(mcpdClient.get).mockResolvedValue([]);

      await router.route({ jsonrpc: '2.0', id: 1, method: 'resources/list' });

      expect(mcpdClient.get).toHaveBeenCalledWith(
        '/api/v1/projects/proj/prompts/visible',
      );
    });

    it('should include session in propose when context is provided', async () => {
      router.setPromptConfig(mcpdClient, 'proj');
      const plugin = createGatePlugin({ gated: false });
      router.setPlugin(plugin);
      router.setProxyModel('default', { complete: async () => '', available: () => false }, new MemoryCache());

      // Initialize to create session context
      await router.route(
        { jsonrpc: '2.0', id: 0, method: 'initialize' },
        { sessionId: 'sess-99' },
      );

      await router.route(
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'propose_prompt',
            arguments: { name: 'test', content: 'stuff' },
          },
        },
        { sessionId: 'sess-99' },
      );

      expect(mcpdClient.post).toHaveBeenCalledWith(
        '/api/v1/projects/proj/promptrequests',
        { name: 'test', content: 'stuff', createdBySession: 'sess-99' },
      );
    });
  });
});
