/**
 * MCP Tab — full lifecycle e2e tests.
 *
 * Covers: browse catalog → search → install (env-key form) → verify in
 * installed list → manage detail view → connect/disconnect → uninstall →
 * verify removal. All RPC calls are mocked via page.route so no running
 * core is required.
 */
import { expect, type Page, test } from '@playwright/test';

import packageJson from '../../../package.json' with { type: 'json' };

// Derive from the build's real version, never hardcode. `update_version` feeds
// the bootCheck version-match gate; a stale literal makes the mock mismatch the
// app build, leaving BootCheckGate in "outdated" so `#root` never renders and
// the whole spec times out (the 0.57.18→0.57.19 bump blanked it this way).
const APP_VERSION = packageJson.version;

// ---------------------------------------------------------------------------
// Mock data
// ---------------------------------------------------------------------------

const REGISTRY_SERVERS = [
  {
    qualified_name: 'io.github.test/memory-server',
    display_name: 'Memory Server',
    description: 'A test MCP server for memory operations',
    icon_url: null,
    use_count: 1200,
    is_deployed: false,
    source: 'mcp_official',
  },
  {
    qualified_name: 'io.github.test/github-tools',
    display_name: 'GitHub Tools',
    description: 'MCP server for GitHub API integration',
    icon_url: null,
    use_count: 5600,
    is_deployed: true,
    source: 'mcp_official',
  },
  {
    qualified_name: 'io.github.test/notion-connector',
    display_name: 'Notion Connector',
    description: 'Connect to Notion workspaces via MCP',
    icon_url: null,
    use_count: 980,
    is_deployed: false,
    source: 'mcp_official',
  },
];

function makeInstalledServer(overrides: Partial<typeof INSTALLED_DEFAULT> = {}) {
  return { ...INSTALLED_DEFAULT, ...overrides };
}

const INSTALLED_DEFAULT = {
  server_id: 'srv_installed_1',
  qualified_name: 'io.github.test/memory-server',
  display_name: 'Memory Server',
  description: 'A test MCP server for memory operations',
  command_kind: 'node',
  command: 'npx',
  args: ['-y', '@modelcontextprotocol/server-memory'],
  env_keys: [],
  installed_at: 1700000000,
  enabled: true,
};

const STATUS_CONNECTED = {
  server_id: 'srv_installed_1',
  qualified_name: 'io.github.test/memory-server',
  display_name: 'Memory Server',
  status: 'connected' as const,
  tool_count: 5,
};

// Tools a connected server exposes — returned by the mocked connect so the
// detail's tool list (and the execution playground) have something to drive.
const MOCK_TOOLS = [
  { name: 'create_memory', description: 'Create a memory', input_schema: {} },
  { name: 'list_memories', description: 'List all memories', input_schema: {} },
];

const GITHUB_DETAIL = {
  ...REGISTRY_SERVERS[1],
  connections: [{ type: 'stdio', published: true }],
  required_env_keys: ['GITHUB_TOKEN'],
};

const GITHUB_INSTALLED = {
  server_id: 'srv_github_1',
  qualified_name: 'io.github.test/github-tools',
  display_name: 'GitHub Tools',
  description: 'MCP server for GitHub API integration',
  command_kind: 'node',
  command: 'npx',
  args: ['-y', '@modelcontextprotocol/server-github'],
  env_keys: ['GITHUB_TOKEN'],
  installed_at: 1700000100,
  enabled: true,
};

// ---------------------------------------------------------------------------
// RPC mock layer — mutable state so tests can drive lifecycle transitions
// ---------------------------------------------------------------------------

interface MockState {
  installed: (typeof INSTALLED_DEFAULT)[];
  // `status` is broadened to a string so tests can seed non-connected states
  // (e.g. `error`) that keep the status poll active.
  statuses: Array<Omit<typeof STATUS_CONNECTED, 'status'> & { status: string }>;
}

function rpcOk(id: number, result: unknown) {
  return {
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ jsonrpc: '2.0', id, result }),
  };
}

function rpcError(id: number, message: string) {
  return {
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message } }),
  };
}

async function setupMockRpc(page: Page, state: MockState) {
  await page.route('**/rpc', async (route, request) => {
    const body = JSON.parse(request.postData() || '{}');
    const method: string = body.method;
    const id: number = body.id;
    const params = body.params ?? {};

    switch (method) {
      case 'openhuman.update_version':
        return route.fulfill(
          rpcOk(id, {
            result: {
              version: APP_VERSION,
              target_triple: 'x86_64-apple-darwin',
              asset_prefix: '',
            },
          })
        );

      case 'openhuman.app_state_snapshot':
        return route.fulfill(
          rpcOk(id, {
            result: {
              auth: { isAuthenticated: true, userId: 'pw-mcp-user', user: null, profileId: null },
              sessionToken: 'fake-session-token',
              currentUser: { _id: 'pw-mcp-user', displayName: 'Test User' },
              onboardingCompleted: true,
              chatOnboardingCompleted: true,
              analyticsEnabled: false,
              meetAutoOrchestratorHandoff: false,
              localState: {},
              keyringStatus: { isUnlocked: true, hasPassphrase: false },
              runtime: {
                screenIntelligence: { enabled: false },
                localAi: { enabled: false },
                autocomplete: { enabled: false },
                service: { running: false },
              },
            },
          })
        );

      // ---- MCP registry ----
      case 'openhuman.mcp_clients_registry_search': {
        const query = (params.query ?? '').toLowerCase();
        const installedNames = new Set(state.installed.map(s => s.qualified_name));
        const queryFiltered = query
          ? REGISTRY_SERVERS.filter(
              s =>
                s.display_name.toLowerCase().includes(query) ||
                s.qualified_name.toLowerCase().includes(query)
            )
          : REGISTRY_SERVERS;
        // Exclude servers that are already installed — mirrors real backend behaviour
        const filtered = queryFiltered.filter(s => !installedNames.has(s.qualified_name));
        return route.fulfill(rpcOk(id, { servers: filtered, page: 1, total_pages: 1 }));
      }

      case 'openhuman.mcp_clients_registry_get':
        if (params.qualified_name === GITHUB_DETAIL.qualified_name) {
          return route.fulfill(rpcOk(id, { server: GITHUB_DETAIL }));
        }
        return route.fulfill(rpcError(id, `server not found: ${params.qualified_name}`));

      // ---- Installed servers (mutable) ----
      case 'openhuman.mcp_clients_installed_list':
        return route.fulfill(rpcOk(id, { installed: state.installed }));

      case 'openhuman.mcp_clients_status':
        return route.fulfill(rpcOk(id, { servers: state.statuses }));

      case 'openhuman.mcp_clients_install':
        if (!params.qualified_name) {
          return route.fulfill(rpcError(id, "missing required param 'qualified_name'"));
        }
        state.installed.push(GITHUB_INSTALLED);
        return route.fulfill(rpcOk(id, { server: GITHUB_INSTALLED }));

      // Auth probe for the upfront connect modal — these test servers need no
      // credentials, so report `none` and the modal shows a single Connect button.
      case 'openhuman.mcp_clients_detect_auth':
        return route.fulfill(rpcOk(id, { kind: 'none' }));

      case 'openhuman.mcp_clients_connect': {
        const sid = params.server_id;
        const inst = state.installed.find(s => s.server_id === sid);
        // Reject unknown server ids so the test can't pass while wired to the
        // wrong server.
        if (!inst) {
          return route.fulfill(rpcError(id, `server not installed: ${sid}`));
        }
        // Mark the server connected for subsequent status polls and hand back
        // its tool list (what `onConnected` feeds into the detail's tool list).
        state.statuses = [
          ...state.statuses.filter(s => s.server_id !== sid),
          {
            server_id: sid,
            qualified_name: inst.qualified_name,
            display_name: inst.display_name,
            status: 'connected',
            tool_count: MOCK_TOOLS.length,
          },
        ];
        return route.fulfill(rpcOk(id, { status: 'connected', tools: MOCK_TOOLS }));
      }

      // Tool execution — what the playground's "Run tool" calls. Unknown tools
      // come back as a tool error so the spec can't pass on a wrong tool name.
      case 'openhuman.mcp_clients_tool_call': {
        const known = MOCK_TOOLS.some(t => t.name === params.tool_name);
        if (!known) {
          return route.fulfill(
            rpcOk(id, { result: `unknown tool: ${params.tool_name}`, is_error: true })
          );
        }
        return route.fulfill(
          rpcOk(id, { result: `ran ${params.tool_name}: memory created id=42`, is_error: false })
        );
      }

      case 'openhuman.mcp_clients_disconnect':
        state.statuses = state.statuses.filter(s => s.server_id !== params.server_id);
        return route.fulfill(rpcOk(id, { status: 'disconnected' }));

      case 'openhuman.mcp_clients_uninstall':
        state.installed = state.installed.filter(s => s.server_id !== params.server_id);
        state.statuses = state.statuses.filter(s => s.server_id !== params.server_id);
        return route.fulfill(rpcOk(id, { success: true }));

      case 'openhuman.mcp_clients_tools':
        return route.fulfill(
          rpcOk(id, {
            tools: [
              { name: 'create_memory', description: 'Create a memory', input_schema: {} },
              { name: 'list_memories', description: 'List all memories', input_schema: {} },
            ],
          })
        );

      default:
        return route.fulfill(rpcOk(id, {}));
    }
  });
}

async function seedLocalStorage(page: Page) {
  await page.addInitScript(() => {
    window.localStorage.setItem('openhuman_core_mode', 'cloud');
    window.localStorage.setItem('openhuman_core_rpc_url', 'http://127.0.0.1:17788/rpc');
    window.localStorage.setItem('openhuman_core_rpc_token', 'test-token');
    window.localStorage.setItem('openhuman:walkthrough_completed', 'true');
    window.localStorage.removeItem('openhuman:walkthrough_pending');
  });
}

async function navigateToMcpTab(page: Page) {
  // Phase 2: /skills → /connections, ?tab=mcp → ?tab=tools (back-compat alias also works)
  await page.goto('/#/connections?tab=tools');
  await page.waitForSelector('#root', { state: 'visible', timeout: 20_000 });
  await page.locator('input[type="search"]').waitFor({ state: 'visible', timeout: 10_000 });
  await page.locator('table').waitFor({ state: 'visible', timeout: 10_000 });
}

// ==========================================================================
// Tests
// ==========================================================================

test.describe('MCP Tab — Table View & Filtering', () => {
  let state: MockState;

  test.beforeEach(async ({ page }) => {
    state = { installed: [makeInstalledServer()], statuses: [{ ...STATUS_CONNECTED }] };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);
    await navigateToMcpTab(page);
  });

  test('renders search bar and filter chips', async ({ page }) => {
    await expect(page.locator('input[type="search"]')).toBeVisible();
    await expect(page.getByRole('tab', { name: /^All$/ })).toBeVisible();
    await expect(page.getByRole('tab', { name: /Installed/ })).toBeVisible();
    await expect(page.getByRole('tab', { name: /Registry/ })).toBeVisible();
  });

  test('displays installed servers with status dot and Manage action', async ({ page }) => {
    const row = page.locator('table tbody tr').first();
    await expect(row.locator('td:first-child')).toContainText('Memory Server');
    await expect(row.locator('text=Manage')).toBeVisible();
  });

  test('displays registry servers as clickable rows', async ({ page }) => {
    const registryRow = page.locator('table tbody tr[role="button"]', {
      has: page.locator('text=GitHub Tools'),
    });
    await expect(registryRow).toBeVisible({ timeout: 10_000 });
    await expect(registryRow.locator('text=Install')).toBeVisible();
  });

  test('filter "Installed" hides registry rows', async ({ page }) => {
    await page.getByRole('tab', { name: /Installed/ }).click();
    const rows = page.locator('table tbody tr');
    const count = await rows.count();
    expect(count).toBeGreaterThan(0);
    for (let i = 0; i < count; i++) {
      await expect(rows.nth(i).locator('text=Manage')).toBeVisible();
    }
  });

  test('filter "Registry" hides installed rows', async ({ page }) => {
    await page.getByRole('tab', { name: /Registry/ }).click();
    const rows = page.locator('table tbody tr');
    const count = await rows.count();
    expect(count).toBeGreaterThan(0);
    for (let i = 0; i < count; i++) {
      await expect(rows.nth(i).locator('text=Install')).toBeVisible();
    }
  });

  test('already-installed servers are excluded from registry rows', async ({ page }) => {
    await page.getByRole('tab', { name: /Registry/ }).click();
    const rows = page.locator('table tbody tr');
    const count = await rows.count();
    for (let i = 0; i < count; i++) {
      const text = await rows.nth(i).locator('td:first-child').innerText();
      expect(text).not.toContain('Memory Server');
    }
  });

  test('search filters both installed and registry servers', async ({ page }) => {
    const search = page.locator('input[type="search"]');
    await search.fill('notion');
    // Wait for a Notion row to appear AND for non-matching rows (e.g. "Memory
    // Server") to disappear — the table re-renders asynchronously and a naive
    // count() immediately after the first visible check can race against the
    // previous state still being in the DOM.
    await expect(
      page.locator('table tbody tr', { has: page.locator('td:has-text("Notion")') })
    ).toBeVisible({ timeout: 5_000 });
    await expect(
      page.locator('table tbody tr', { has: page.locator('td:has-text("Memory Server")') })
    ).toHaveCount(0, { timeout: 5_000 });
    // The positive (Notion row present) + negative (Memory Server gone) checks
    // above already prove the filter works. Avoid iterating `td:first-child`
    // per row — the #3480 registry redesign changed the column layout, and the
    // table re-renders async (the old per-row loop raced + assumed name-first).
    await expect(page.locator('table tbody tr')).not.toHaveCount(0);
  });

  test('no Smithery branding visible anywhere', async ({ page }) => {
    // Wait for the table to be fully rendered before scanning body text
    await page.locator('table tbody tr').first().waitFor({ state: 'visible', timeout: 10_000 });
    const bodyText = await page.locator('body').innerText();
    expect(bodyText.toLowerCase()).not.toContain('smithery');
  });
});

test.describe('MCP Tab — Install Lifecycle', () => {
  let state: MockState;

  test.beforeEach(async ({ page }) => {
    state = { installed: [makeInstalledServer()], statuses: [{ ...STATUS_CONNECTED }] };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);
    await navigateToMcpTab(page);
  });

  test('install flow: click row → detail → configure → fill env → submit → appears installed', async ({
    page,
  }) => {
    // 1. Click the GitHub Tools registry row (entire row is clickable)
    const githubRow = page.locator('table tbody tr[role="button"]', {
      has: page.locator('td:first-child:has-text("GitHub Tools")'),
    });
    await expect(githubRow).toBeVisible({ timeout: 10_000 });
    await githubRow.click();

    // 2. Install dialog detail step — shows server info and "Configure & install"
    await expect(page.locator('text=GitHub Tools').first()).toBeVisible({ timeout: 5_000 });
    const configureBtn = page.locator('button:has-text("Configure & install")');
    await expect(configureBtn).toBeVisible({ timeout: 5_000 });
    await configureBtn.click();

    // 3. Configure step — env input appears
    const envInput = page.locator('input[id="env-GITHUB_TOKEN"]');
    await expect(envInput).toBeVisible({ timeout: 5_000 });

    // 4. Fill in the env value
    await envInput.fill('ghp_test_token_123');

    // 5. Click "Install" submit button
    const submitBtn = page.locator('button:has-text("Install")');
    await submitBtn.click();

    // 6. Should navigate to detail view (the installed server detail)
    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 10_000 });

    // 7. Go back and verify the server appears in the installed list
    await page.locator('button:has-text("Go back")').click();
    await expect(page.locator('table')).toBeVisible({ timeout: 5_000 });
    const installedGithub = page.locator('table tbody tr', {
      has: page.locator('td:has-text("GitHub Tools")'),
    });
    await expect(installedGithub).toBeVisible({ timeout: 5_000 });
  });

  test('cancel from install dialog returns to table', async ({ page }) => {
    // Click a registry row to open install dialog
    const registryRow = page.locator('table tbody tr[role="button"]', {
      has: page.locator('td:first-child:has-text("GitHub Tools")'),
    });
    await registryRow.click();

    // Cancel button should be visible on detail step
    await expect(page.locator('button:has-text("Cancel")')).toBeVisible({ timeout: 5_000 });
    await page.locator('button:has-text("Cancel")').click();
    await expect(page.locator('table')).toBeVisible({ timeout: 5_000 });
  });
});

test.describe('MCP Tab — Manage & Uninstall Lifecycle', () => {
  let state: MockState;

  test.beforeEach(async ({ page }) => {
    state = { installed: [makeInstalledServer()], statuses: [{ ...STATUS_CONNECTED }] };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);
    await navigateToMcpTab(page);
  });

  test('click installed server row → detail view shows server info', async ({ page }) => {
    const row = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await row.click();

    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 5_000 });
    await expect(page.locator('text=Memory Server')).toBeVisible();
  });

  test('detail view shows qualified name', async ({ page }) => {
    const row = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await row.click();
    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 5_000 });
    await expect(page.locator('text=io.github.test/memory-server')).toBeVisible();
  });

  test('uninstall flow: detail → confirm uninstall → returns to table', async ({ page }) => {
    const row = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await row.click();
    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 5_000 });

    const uninstallBtn = page.locator('button:has-text("Uninstall")');
    await expect(uninstallBtn.first()).toBeVisible({ timeout: 5_000 });
    await uninstallBtn.first().click();

    const confirmBtn = page.locator('button:has-text("Yes")');
    await expect(confirmBtn.first()).toBeVisible({ timeout: 5_000 });
    await confirmBtn.first().click();

    await expect(page.locator('table')).toBeVisible({ timeout: 10_000 });

    await page.getByRole('tab', { name: /Installed/ }).click();
    const removedRow = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await expect(removedRow).toHaveCount(0, { timeout: 5_000 });
  });

  test('back button from detail returns to table', async ({ page }) => {
    const row = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await row.click();
    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 5_000 });
    await page.locator('button:has-text("Go back")').click();
    await expect(page.locator('table')).toBeVisible({ timeout: 5_000 });
  });
});

test.describe('MCP Tab — Connect & Tool Execution', () => {
  let state: MockState;

  test.beforeEach(async ({ page }) => {
    // Seed the installed server in `error` status: the detail offers a Connect
    // affordance AND the status poll stays active (error is non-terminal), so
    // the status flips to connected once the modal connects.
    state = {
      installed: [makeInstalledServer()],
      statuses: [
        {
          server_id: 'srv_installed_1',
          qualified_name: 'io.github.test/memory-server',
          display_name: 'Memory Server',
          status: 'error',
          tool_count: 0,
        },
      ],
    };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);
    await navigateToMcpTab(page);
  });

  test('connect a server, then run one of its tools and see the result', async ({ page }) => {
    // Open the installed server's detail view.
    const row = page.locator('table tbody tr', {
      has: page.locator('td:first-child:has-text("Memory Server")'),
    });
    await row.click();
    await expect(page.locator('button:has-text("Go back")')).toBeVisible({ timeout: 5_000 });

    // Connect via the upfront auth modal (no-auth server → a single Connect
    // button). `exact` avoids the "Connections" sidebar nav button.
    await page.getByRole('button', { name: 'Connect', exact: true }).click();
    const connectDialog = page.getByRole('dialog');
    await expect(connectDialog).toBeVisible({ timeout: 5_000 });
    await connectDialog.getByRole('button', { name: /^Connect$/ }).click();

    // The status poll flips the server to connected → its (collapsed) tool list
    // appears. Expand it, then open the execution playground for a tool. This is
    // the connect → tool step of the install→connect→tool path.
    const toolsToggle = page.getByRole('button', { name: /tools available/ });
    await expect(toolsToggle).toBeVisible({ timeout: 15_000 });
    await toolsToggle.click();

    const tryButton = page.getByRole('button', {
      name: 'Open execution playground for create_memory',
    });
    await expect(tryButton).toBeVisible({ timeout: 5_000 });
    await tryButton.click();

    // The Tool Execution Playground opens; run the tool and assert the result
    // surfaced from the mocked `mcp_clients_tool_call`.
    const playground = page.getByRole('dialog');
    await expect(playground.getByText('Run create_memory')).toBeVisible({ timeout: 5_000 });
    await playground.getByRole('button', { name: 'Run tool' }).click();
    await expect(page.getByTestId('mcp-playground-result')).toContainText('memory created id=42', {
      timeout: 10_000,
    });
  });
});

test.describe('MCP Tab — Empty & Edge States', () => {
  test('empty installed list shows appropriate message', async ({ page }) => {
    const state: MockState = { installed: [], statuses: [] };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);
    await navigateToMcpTab(page);

    await page.getByRole('tab', { name: /Installed/ }).click();
    // Target the empty-state element directly: a broad `text=/no.*servers/i`
    // locator also matches ancestor containers (the root shell wraps the panel),
    // tripping Playwright strict mode.
    await expect(page.getByTestId('mcp-installed-empty')).toBeVisible({ timeout: 10_000 });
  });

  test('search with no results shows no-results message', async ({ page }) => {
    const state: MockState = { installed: [], statuses: [] };
    await seedLocalStorage(page);
    await setupMockRpc(page, state);

    // Registered after setupMockRpc — Playwright routes use LIFO ordering, so
    // this handler runs first and falls through to the base mock for all other
    // methods.
    await page.route('**/rpc', async (route, request) => {
      const body = JSON.parse(request.postData() || '{}');
      if (
        body.method === 'openhuman.mcp_clients_registry_search' &&
        body.params?.query === 'xyznonexistent999'
      ) {
        return route.fulfill(rpcOk(body.id, { servers: [], page: 1, total_pages: 1 }));
      }
      await route.fallback();
    });

    await navigateToMcpTab(page);
    await page.locator('input[type="search"]').fill('xyznonexistent999');

    // Target the catalog empty-state element directly — a broad text regex also
    // matches the root-shell ancestor container and trips strict mode.
    await expect(page.getByTestId('mcp-catalog-empty')).toBeVisible({ timeout: 10_000 });
  });
});
