/**
 * favourite-index plugin tests — presentation (favourite/ + all/), rewrite
 * routing back to canonical server/tool, gated no-op, and instruction merge.
 *
 * Driven through a real McpRouter (gate composed with favourite-index, mirroring
 * project-mcp-endpoint wiring) so routing is exercised end-to-end.
 */
import { describe, it, expect, vi } from 'vitest';
import { McpRouter } from '../src/router.js';
import type { UpstreamConnection, JsonRpcRequest, JsonRpcResponse } from '../src/types.js';
import type { McpdClient } from '../src/http/mcpd-client.js';
import { createGatePlugin } from '../src/proxymodel/plugins/gate.js';
import { composePlugins } from '../src/proxymodel/plugins/compose.js';
import {
  createFavouriteIndexPlugin,
  FAVOURITE_INDEX_INSTRUCTION,
} from '../src/proxymodel/plugins/favourite-index.js';
import { LLMProviderAdapter } from '../src/proxymodel/llm-adapter.js';
import { MemoryCache } from '../src/proxymodel/cache.js';

function mockUpstream(
  name: string,
  tools: Array<{ name: string; description?: string }>,
): 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 } };
      }
      if (req.method === 'tools/call') {
        // Echo the name the UPSTREAM received (proves canonical routing).
        return {
          jsonrpc: '2.0',
          id: req.id,
          result: { content: [{ type: 'text', text: `Called ${(req.params as Record<string, unknown>)?.['name']}` }] },
        };
      }
      if (req.method === 'resources/list') return { jsonrpc: '2.0', id: req.id, result: { resources: [] } };
      if (req.method === 'prompts/list') return { jsonrpc: '2.0', id: req.id, result: { prompts: [] } };
      return { jsonrpc: '2.0', id: req.id, error: { code: -32601, message: 'Not found' } };
    }),
  } as UpstreamConnection;
}

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;
}

function setup(opts: { gated?: boolean; favourites?: string[]; maxFavourites?: number } = {}) {
  const router = new McpRouter();
  router.setPromptConfig(mockMcpdClient(), 'test-project');
  const plugin = composePlugins([
    createGatePlugin({ gated: opts.gated ?? false, providerRegistry: null }),
    createFavouriteIndexPlugin({
      favourites: opts.favourites ?? [],
      ...(opts.maxFavourites !== undefined ? { maxFavourites: opts.maxFavourites } : {}),
    }),
  ]);
  router.setPlugin(plugin);
  router.setProxyModel('default', { complete: async () => '', available: () => false } as unknown as LLMProviderAdapter, new MemoryCache());
  return router;
}

async function initAndList(router: McpRouter, sessionId = 's1'): Promise<Array<{ name: string; description?: string }>> {
  await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId });
  const res = await router.route({ jsonrpc: '2.0', id: 2, method: 'tools/list' }, { sessionId });
  return (res.result as { tools: Array<{ name: string; description?: string }> }).tools;
}

const CATALOG = {
  k8s: [{ name: 'get_pods' }, { name: 'get_secret' }, { name: 'scale_deployment' }],
  vault: [{ name: 'read_secret' }, { name: 'list_secrets' }],
};

describe('favourite-index plugin', () => {
  it('presents favourite/ first (curated) then all/ (full catalog)', async () => {
    const router = setup({ favourites: ['k8s/get_pods', 'vault/read_secret'] });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    router.addUpstream(mockUpstream('vault', CATALOG.vault));

    const names = (await initAndList(router)).map((t) => t.name);

    // Favourites, in configured order, appear before any all/ entry.
    expect(names.indexOf('favourite/get_pods')).toBe(0);
    expect(names.indexOf('favourite/read_secret')).toBe(1);
    expect(names.indexOf('favourite/get_pods')).toBeLessThan(names.indexOf('all/k8s/get_pods'));

    // Full catalog present under all/.
    for (const t of [...CATALOG.k8s.map((x) => `all/k8s/${x.name}`), ...CATALOG.vault.map((x) => `all/vault/${x.name}`)]) {
      expect(names).toContain(t);
    }
    // Gate virtual tools pass through untouched (not re-namespaced).
    expect(names).toContain('read_prompts');
    expect(names).not.toContain('favourite/read_prompts');
    // Non-favourite tools are NOT duplicated into favourite/.
    expect(names).not.toContain('favourite/get_secret');
  });

  it('routes a favourite/ call back to the canonical upstream tool', async () => {
    const router = setup({ favourites: ['vault/read_secret'] });
    router.addUpstream(mockUpstream('vault', CATALOG.vault));
    await initAndList(router);

    const res = await router.route(
      { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'favourite/read_secret', arguments: {} } },
      { sessionId: 's1' },
    );
    const text = (res.result as { content: Array<{ text: string }> }).content[0]!.text;
    // Upstream sees the stripped canonical tool name.
    expect(text).toBe('Called read_secret');
  });

  it('routes an all/<server>/<tool> call back to the canonical upstream tool', async () => {
    const router = setup({ favourites: [] });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    await initAndList(router);

    const res = await router.route(
      { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'all/k8s/scale_deployment', arguments: {} } },
      { sessionId: 's1' },
    );
    const text = (res.result as { content: Array<{ text: string }> }).content[0]!.text;
    expect(text).toBe('Called scale_deployment');
  });

  it('routes a favourite/ call even before tools/list (lazy resolver rebuild)', async () => {
    const router = setup({ favourites: ['vault/read_secret'] });
    router.addUpstream(mockUpstream('vault', CATALOG.vault));
    // initialize only — deliberately NO tools/list, so the resolver is unset.
    await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });

    const res = await router.route(
      { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'favourite/read_secret', arguments: {} } },
      { sessionId: 's1' },
    );
    expect((res.result as { content: Array<{ text: string }> }).content[0]!.text).toBe('Called read_secret');
  });

  it('no-ops while the session is gated (only begin_session visible)', async () => {
    const router = setup({ gated: true, favourites: ['k8s/get_pods'] });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    const names = (await initAndList(router)).map((t) => t.name);
    expect(names).toEqual(['begin_session']);
  });

  it('skips stale favourites not in the catalog but keeps them in all/', async () => {
    const router = setup({ favourites: ['k8s/get_pods', 'gone/missing_tool'] });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    const names = (await initAndList(router)).map((t) => t.name);
    expect(names).toContain('favourite/get_pods');
    expect(names).not.toContain('favourite/missing_tool');
  });

  it('caps favourites at maxFavourites', async () => {
    const router = setup({ favourites: ['k8s/get_pods', 'k8s/get_secret', 'vault/read_secret'], maxFavourites: 2 });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    router.addUpstream(mockUpstream('vault', CATALOG.vault));
    const names = (await initAndList(router)).map((t) => t.name).filter((n) => n.startsWith('favourite/'));
    expect(names).toHaveLength(2);
    expect(names).toEqual(['favourite/get_pods', 'favourite/get_secret']);
  });

  it('disambiguates short-name collisions across servers', async () => {
    // Both servers expose a `status` tool; both are favourites.
    const router = setup({ favourites: ['k8s/status', 'vault/status'] });
    router.addUpstream(mockUpstream('k8s', [{ name: 'status' }]));
    router.addUpstream(mockUpstream('vault', [{ name: 'status' }]));
    const names = (await initAndList(router)).map((t) => t.name).filter((n) => n.startsWith('favourite/'));
    expect(names).toContain('favourite/status');
    expect(names).toContain('favourite/vault-status');

    // The disambiguated favourite still routes to the right server.
    const res = await router.route(
      { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'favourite/vault-status', arguments: {} } },
      { sessionId: 's1' },
    );
    expect((res.result as { content: Array<{ text: string }> }).content[0]!.text).toBe('Called status');
  });

  it('injects the load-bearing prefer-favourite instruction on initialize', async () => {
    const router = setup({ favourites: ['k8s/get_pods'] });
    router.addUpstream(mockUpstream('k8s', CATALOG.k8s));
    const res = await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
    const instructions = (res.result as { instructions?: string }).instructions ?? '';
    expect(instructions).toContain(FAVOURITE_INDEX_INSTRUCTION);
  });
});
