import { expect, type Page, test } from '@playwright/test';

import {
  bootRuntimeReadyGuestPage,
  callCoreRpc,
  dismissWalkthroughIfPresent,
  signInViaCallbackToken,
  waitForAppReady,
} from '../helpers/core-rpc';

const CONNECTOR_NAME = 'Gmail';
const TOOLKIT_SLUG = 'gmail';
const CONNECTION_ID = 'c-gmail-1';
const ACTION = 'GMAIL_FETCH_EMAILS';
const MOCK_BASE = 'http://127.0.0.1:' + (process.env.E2E_MOCK_PORT || '18473');

type RequestLogEntry = { method?: string; url?: string; body?: string };

async function mockFetch(path: string, init?: RequestInit) {
  const response = await fetch(MOCK_BASE + path, init);
  if (!response.ok) throw new Error('mock request failed: ' + response.status + ' ' + path);
  return response.json() as Promise<{ data?: unknown }>;
}

async function resetMock() {
  await mockFetch('/__admin/reset', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ keepBehavior: false, keepRequests: false }),
  });
}

async function setMockBehavior(behavior: Record<string, unknown>) {
  await mockFetch('/__admin/behavior', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ behavior }),
  });
}

async function getRequestLog(): Promise<RequestLogEntry[]> {
  const payload = await mockFetch('/__admin/requests');
  return (payload.data as RequestLogEntry[]) ?? [];
}

async function seedConnector(status: 'ACTIVE' | 'FAILED' | 'EXPIRED' = 'ACTIVE') {
  await setMockBehavior({
    composioToolkits: JSON.stringify([TOOLKIT_SLUG]),
    composioConnections: JSON.stringify([{ id: CONNECTION_ID, toolkit: TOOLKIT_SLUG, status }]),
  });
}

async function bootSkillsPage(page: Page, userId: string) {
  await resetMock();
  await seedConnector();
  await bootRuntimeReadyGuestPage(page);
  try {
    await signInViaCallbackToken(page, userId);
  } catch {
    await bootRuntimeReadyGuestPage(page);
    await signInViaCallbackToken(page, userId);
  }
  await page.evaluate(() => {
    try {
      localStorage.setItem('openhuman:walkthrough_completed', 'true');
      localStorage.removeItem('openhuman:walkthrough_pending');
    } catch {}
  });
  // Phase 2: /skills → /connections
  await page.evaluate(() => {
    window.location.hash = '/connections';
  });
  await expect
    .poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
    .toContain('/connections');
  await waitForAppReady(page);
  await dismissWalkthroughIfPresent(page);
  // Navigate to the Composio tab
  await page.getByTestId('two-pane-nav-composio').click();
  const heading = page.getByTestId('composio-integrations-card');
  if (!(await heading.isVisible().catch(() => false))) {
    const connectionsButton = page.getByRole('button', { name: 'Connections' });
    if (await connectionsButton.isVisible().catch(() => false)) {
      await connectionsButton.click({ force: true });
      await expect
        .poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
        .toContain('/connections');
      await waitForAppReady(page);
      await dismissWalkthroughIfPresent(page);
    }
  }
  await expect(page.getByTestId('composio-integrations-card')).toBeVisible({ timeout: 20_000 });
}

async function reloadSkills(page: Page) {
  await ensureComposioSurface(page);
}

async function ensureComposioSurface(page: Page) {
  // Navigate to /connections and click the Composio tab
  const heading = page.getByTestId('composio-integrations-card');
  for (let attempt = 0; attempt < 3; attempt++) {
    await page.evaluate(() => {
      window.location.hash = '/connections';
    });
    await expect
      .poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
      .toContain('/connections');
    await waitForAppReady(page);
    await dismissWalkthroughIfPresent(page);
    await page.getByTestId('two-pane-nav-composio').click();
    if (await heading.isVisible().catch(() => false)) {
      return;
    }
    const connectionsButton = page.getByRole('button', { name: 'Connections' });
    if (await connectionsButton.isVisible().catch(() => false)) {
      await connectionsButton.click({ force: true });
      await waitForAppReady(page);
      await dismissWalkthroughIfPresent(page);
      await page.getByTestId('two-pane-nav-composio').click();
      if (await heading.isVisible().catch(() => false)) {
        return;
      }
    }
  }
  await expect(heading).toBeVisible({ timeout: 20_000 });
}

async function assertSessionNotNuked(page: Page) {
  // Phase 2: /skills → /connections
  await expect
    .poll(async () =>
      page.evaluate(() => {
        const win = window as typeof window & {
          __OPENHUMAN_CORE_STATE__?: () => {
            snapshot?: {
              sessionToken?: string | null;
              currentUser?: { _id?: string | null } | null;
            };
          };
        };
        const snapshot = win.__OPENHUMAN_CORE_STATE__?.()?.snapshot;
        return {
          // The connections page now appends an active-tab query (e.g.
          // `#/connections?tab=composio`); strip it so we assert we're still on
          // the connections route (session not nuked), not the exact sub-tab.
          hash: window.location.hash.replace(/\?.*$/, ''),
          hasToken: Boolean(snapshot?.sessionToken),
          hasUser: Boolean(snapshot?.currentUser?._id),
        };
      })
    )
    .toEqual({ hash: '#/connections', hasToken: true, hasUser: true });
}

async function openModal(page: Page) {
  await page.getByTestId('skill-install-composio-gmail').click();
  const dialog = page.getByRole('dialog', { name: /(Connect|Manage|Reconnect) Gmail/i });
  await expect(dialog).toBeVisible();
  return dialog;
}

function unwrapConnections(payload: unknown): Array<{ toolkit?: string; status?: string }> {
  const root = payload as {
    result?: { connections?: Array<{ toolkit?: string; status?: string }> };
    connections?: Array<{ toolkit?: string; status?: string }>;
  };
  return root.result?.connections ?? root.connections ?? [];
}

test.describe('Gmail connector', () => {
  test.beforeEach(async ({ page }, testInfo) => {
    const testSlug = testInfo.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
    await bootSkillsPage(page, 'pw-gmail-' + testSlug);
  });

  test('renders the connector card', async ({ page }) => {
    await expect(page.getByTestId('skill-install-composio-gmail')).toContainText(CONNECTOR_NAME);
  });

  test('routes authorize through the mock backend', async () => {
    await callCoreRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
    const requests = await getRequestLog();
    const authReq = requests.find(
      request =>
        request.method === 'POST' && request.url?.includes('/agent-integrations/composio/authorize')
    );
    expect(authReq).toBeDefined();
    expect(JSON.parse(authReq?.body || '{}')).toMatchObject({ toolkit: TOOLKIT_SLUG });
  });

  test('persists connected state through list_connections', async () => {
    const payload = await callCoreRpc<unknown>('openhuman.composio_list_connections', {});
    const hit = unwrapConnections(payload).find(
      connection => connection.toolkit?.toLowerCase() === TOOLKIT_SLUG
    );
    expect(hit?.status).toBe('ACTIVE');
  });

  test.skip('keeps the session alive after composio_sync', async ({ page }) => {
    await callCoreRpc('openhuman.composio_sync', { connection_id: CONNECTION_ID });
    await assertSessionNotNuked(page);
  });

  test('routes composio_execute without blanking the app', async ({ page }) => {
    await callCoreRpc('openhuman.composio_execute', { tool: ACTION, arguments: {} });
    await assertSessionNotNuked(page);
  });

  test('survives a 400 fetch emails error and keeps the skills page usable', async ({ page }) => {
    await setMockBehavior({ composioExecuteFails: '1' });
    await expect(
      callCoreRpc('openhuman.composio_execute', {
        connection_id: CONNECTION_ID,
        tool: ACTION,
        arguments: {},
      })
    ).rejects.toThrow(/failed/i);
    await assertSessionNotNuked(page);
    await reloadSkills(page);
    await expect(page.getByTestId('skill-install-composio-gmail')).toContainText(CONNECTOR_NAME);
  });

  test('survives failed connector state on the skills page', async ({ page }) => {
    await seedConnector('FAILED');
    await reloadSkills(page);
    await expect(page.getByTestId('skill-install-composio-gmail')).toContainText(CONNECTOR_NAME);
    await assertSessionNotNuked(page);
  });

  test('shows expired-auth state without logging out', async ({ page }) => {
    await seedConnector('EXPIRED');
    await reloadSkills(page);
    await expect(page.getByTestId('skill-install-composio-' + TOOLKIT_SLUG)).toContainText(
      /Auth expired|Reconnect/i
    );
    const dialog = await openModal(page);
    await expect(dialog).toContainText(CONNECTOR_NAME);
    await assertSessionNotNuked(page);
  });

  test('survives a 4xx composio execute error', async ({ page }) => {
    await setMockBehavior({ composioExecuteFails: '400' });
    await expect(
      callCoreRpc('openhuman.composio_execute', {
        connection_id: CONNECTION_ID,
        tool: ACTION,
        arguments: {},
      })
    ).rejects.toThrow(/failed/i);
    await assertSessionNotNuked(page);
  });

  test('routes disconnect through the mock backend', async ({ page }) => {
    await callCoreRpc('openhuman.composio_delete_connection', { connection_id: CONNECTION_ID });
    const requests = await getRequestLog();
    const deleteReq = requests.find(
      request =>
        request.method === 'DELETE' &&
        request.url?.includes('/agent-integrations/composio/connections/')
    );
    expect(deleteReq).toBeDefined();
    await assertSessionNotNuked(page);
  });
});
