import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
  fetchMcpServers,
  toggleMcpServer,
  deleteMcpServer,
  createMcpServer,
  updateMcpServer,
  fetchCodexMcpServers,
  crossCliCopy,
  fetchAllProjects,
  fetchOtherProjectsServers,
  isStdioMcpServer,
  type McpServer,
} from './api';

function jsonResponse(body: unknown, init: ResponseInit = {}) {
  return new Response(JSON.stringify(body), {
    status: 200,
    headers: { 'content-type': 'application/json' },
    ...init,
  });
}

function getLastFetchCall(fetchMock: any) {
  const calls = fetchMock.mock.calls;
  return calls[calls.length - 1] as [RequestInfo | URL, RequestInit | undefined];
}

const TEST_CSRF_TOKEN = 'test-csrf-token';

function mockFetchWithCsrf(
  handler: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>
) {
  return vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
    if (input === '/api/csrf-token') {
      return jsonResponse({ csrfToken: TEST_CSRF_TOKEN });
    }

    return handler(input, init);
  });
}

describe('MCP API (frontend ↔ backend contract)', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.spyOn(console, 'warn').mockImplementation(() => {});
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });
  it('fetchMcpServers derives lists from /api/mcp-config and computes enabled from disabledMcpServers', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      jsonResponse({
        projects: {
          'D:/ws': {
            mcpServers: {
              projOnly: { command: 'node', args: ['x'], env: { A: '1' } },
              globalDup: { command: 'should-not-appear-in-project' },
              entDup: { command: 'should-not-appear-in-project' },
            },
            disabledMcpServers: ['global1'],
          },
        },
        userServers: {
          global1: { command: 'npx', args: ['-y', 'foo'] },
          globalDup: { command: 'npx', args: ['-y', 'bar'] },
        },
        enterpriseServers: {
          entDup: { command: 'enterprise-tool' },
        },
        globalServers: {},
        configSources: [],
      })
    );

    const result = await fetchMcpServers('D:\\ws');

    expect(fetchMock).toHaveBeenCalledTimes(1);
    expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/mcp-config');

    expect(result.global.map((s) => s.name).sort()).toEqual(['global1', 'globalDup']);
    expect(result.project.map((s) => s.name)).toEqual(['projOnly', 'globalDup', 'entDup']);
    expect(result.conflicts.map((c) => c.name)).toEqual(['globalDup']);

    const global1 = result.global.find((s) => s.name === 'global1');
    expect(global1?.enabled).toBe(false);
    expect(global1?.scope).toBe('global');

    const projOnly = result.project[0];
    expect(isStdioMcpServer(projOnly)).toBe(true);
    if (isStdioMcpServer(projOnly)) {
      expect(projOnly.command).toBe('node');
      expect(projOnly.env).toEqual({ A: '1' });
      expect(projOnly.args).toEqual(['x']);
    }
    expect(projOnly?.enabled).toBe(true);
    expect(projOnly?.scope).toBe('project');
  });

  it('toggleMcpServer uses /api/mcp-toggle with { projectPath, serverName, enable }', async () => {
    const fetchMock = mockFetchWithCsrf(async (input, _init) => {
        if (input === '/api/mcp-toggle') {
          return jsonResponse({ success: true, serverName: 'global1', enabled: false });
        }
        if (input === '/api/mcp-config') {
          return jsonResponse({
            projects: {
              'D:/ws': { mcpServers: {}, disabledMcpServers: ['global1'] },
            },
            userServers: {
              global1: { command: 'npx', args: ['-y', 'foo'] },
            },
            enterpriseServers: {},
            globalServers: {},
            configSources: [],
          });
        }
        throw new Error(`Unexpected fetch: ${String(input)}`);
      });

    const updated = await toggleMcpServer('global1', false, { projectPath: 'D:/ws' });

    const toggleCall = fetchMock.mock.calls.find((c) => c[0] === '/api/mcp-toggle');
    expect(toggleCall).toBeTruthy();
    const [, init] = toggleCall!;
    expect(init?.method).toBe('POST');
    expect(JSON.parse(String(init?.body))).toEqual({ projectPath: 'D:/ws', serverName: 'global1', enable: false });

    expect(updated.enabled).toBe(false);
    expect(updated.name).toBe('global1');
  });

  it('deleteMcpServer calls the correct backend endpoint for project/global scopes', async () => {
    const fetchMock = mockFetchWithCsrf(async (input) => {
      if (input === '/api/mcp-remove-global-server') {
        return jsonResponse({ success: true });
      }
      if (input === '/api/mcp-remove-server') {
        return jsonResponse({ success: true });
      }
      throw new Error(`Unexpected fetch: ${String(input)}`);
    });

    await deleteMcpServer('g1', 'global');
    expect(getLastFetchCall(fetchMock)[0]).toBe('/api/mcp-remove-global-server');

    await deleteMcpServer('p1', 'project', { projectPath: 'D:/ws' });
    expect(getLastFetchCall(fetchMock)[0]).toBe('/api/mcp-remove-server');
  });

  it('createMcpServer (project) uses /api/mcp-copy-server and includes serverName + serverConfig', async () => {
    const fetchMock = mockFetchWithCsrf(async (input) => {
        if (input === '/api/mcp-copy-server') {
          return jsonResponse({ success: true });
        }
        if (input === '/api/mcp-config') {
          return jsonResponse({
            projects: {
              'D:/ws': {
                mcpServers: { s1: { command: 'node', args: ['a'], env: { K: 'V' } } },
                disabledMcpServers: [],
              },
            },
            userServers: {},
            enterpriseServers: {},
            globalServers: {},
            configSources: [],
          });
        }
        throw new Error(`Unexpected fetch: ${String(input)}`);
      });

    const inputServer: McpServer = {
      name: 's1',
      transport: 'stdio',
      command: 'node',
      args: ['a'],
      env: { K: 'V' },
      enabled: true,
      scope: 'project',
    };

    const created = await createMcpServer(inputServer, { projectPath: 'D:/ws', configType: 'mcp' });

    const copyCall = fetchMock.mock.calls.find((c) => c[0] === '/api/mcp-copy-server');
    expect(copyCall).toBeTruthy();
    const [, init] = copyCall!;
    expect(init?.method).toBe('POST');
    expect(JSON.parse(String(init?.body))).toEqual({
      projectPath: 'D:/ws',
      serverName: 's1',
      serverConfig: { command: 'node', args: ['a'], env: { K: 'V' } },
      configType: 'mcp',
    });

    expect(created.name).toBe('s1');
    expect(created.scope).toBe('project');
    expect(created.enabled).toBe(true);
  });

  it('updateMcpServer (global) upserts via /api/mcp-add-global-server', async () => {
    const fetchMock = mockFetchWithCsrf(async (input) => {
      if (input === '/api/mcp-add-global-server') {
        return jsonResponse({ success: true });
      }
      return jsonResponse({
        projects: {},
        userServers: { g1: { command: 'npx' } },
        enterpriseServers: {},
        globalServers: {},
        configSources: [],
      });
    });

    const updated = await updateMcpServer(
      'g1',
      { scope: 'global', command: 'npx', args: ['-y', 'x'], env: { A: '1' }, enabled: true },
      { projectPath: 'D:/ws' }
    );

    const addCall = fetchMock.mock.calls.find((c) => c[0] === '/api/mcp-add-global-server');
    expect(addCall).toBeTruthy();
    const [, init] = addCall!;
    expect(JSON.parse(String(init?.body))).toEqual({
      serverName: 'g1',
      serverConfig: { command: 'npx', args: ['-y', 'x'], env: { A: '1' } },
    });

    expect(updated.name).toBe('g1');
  });

  it('fetchCodexMcpServers maps /api/codex-mcp-config servers record into array', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      jsonResponse({
        servers: {
          s1: { command: 'node', args: ['a'], env: { K: 'V' }, enabled: true },
          s2: { command: 'python', enabled: false },
        },
        configPath: 'C:/Users/me/.codex/config.toml',
        exists: true,
      })
    );

    const result = await fetchCodexMcpServers();
    expect(fetchMock).toHaveBeenCalledWith('/api/codex-mcp-config', expect.anything());
    expect(result.configPath).toContain('config.toml');

    const s2 = result.servers.find((s) => s.name === 's2');
    expect(s2?.enabled).toBe(false);
  });

  it('crossCliCopy codex->claude copies via /api/mcp-copy-server per server', async () => {
    const fetchMock = mockFetchWithCsrf(async (input) => {
      if (input === '/api/codex-mcp-config') {
        return jsonResponse({ servers: { s1: { command: 'node' } }, configPath: 'x', exists: true });
      }
      if (input === '/api/mcp-copy-server') {
        return jsonResponse({ success: true });
      }
      throw new Error(`Unexpected fetch: ${String(input)}`);
    });

    const res = await crossCliCopy({
      source: 'codex',
      target: 'claude',
      serverNames: ['s1'],
      projectPath: 'D:/ws',
    });

    expect(res.success).toBe(true);
    expect(res.copied).toEqual(['s1']);
    expect(res.failed).toEqual([]);

    const copyCall = fetchMock.mock.calls.find((c) => c[0] === '/api/mcp-copy-server');
    expect(copyCall).toBeTruthy();
  });

  it('fetchAllProjects derives project list from /api/mcp-config (no /api/projects/all)', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      jsonResponse({
        projects: { 'D:/a': { mcpServers: {} }, 'D:/b': { mcpServers: {} } },
        userServers: {},
        enterpriseServers: {},
        globalServers: {},
        configSources: [],
      })
    );

    const res = await fetchAllProjects();
    expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/mcp-config');
    expect(res.projects).toEqual(['D:/a', 'D:/b']);
  });

  it('fetchOtherProjectsServers derives per-project servers from /api/mcp-config', async () => {
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      jsonResponse({
        projects: {
          'D:/a': {
            mcpServers: { p1: { command: 'node' } },
            disabledMcpServers: ['p1'],
          },
        },
        userServers: { g1: { command: 'npx' } },
        enterpriseServers: {},
        globalServers: {},
        configSources: [],
      })
    );

    const res = await fetchOtherProjectsServers(['D:/a']);
    expect(Object.keys(res.servers)).toEqual(['D:/a']);
    expect(res.servers['D:/a']?.[0]?.name).toBe('p1');
    expect(res.servers['D:/a']?.[0]?.enabled).toBe(false);
  });
});
