/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import assert from 'node:assert';
import path from 'node:path';
import {afterEach, describe, it} from 'node:test';

import type {Dialog} from 'puppeteer-core';
import sinon from 'sinon';

import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import {
  listPages,
  newPage,
  closePage,
  selectPage,
  navigatePage,
  resizePage,
  handleDialog,
  getTabId,
} from '../../src/tools/pages.js';
import {assertNoServiceWorkerReported, html, withMcpContext} from '../utils.js';

const EXTENSION_SW_PATH = path.join(
  import.meta.dirname,
  '../../../tests/tools/fixtures/extension-sw',
);
const EXTENSION_PATH = path.join(
  import.meta.dirname,
  '../../../tests/tools/fixtures/extension',
);
const EXTENSION_SIDE_PANEL_PATH = path.join(
  import.meta.dirname,
  '../../../tests/tools/fixtures/extension-side-panel',
);

describe('pages', () => {
  afterEach(() => {
    sinon.restore();
  });

  describe('list_pages', () => {
    it('list pages', async () => {
      await withMcpContext(async (response, context) => {
        await listPages().handler({params: {}}, response, context);
        assert.ok(response.includePages);
      });
    });
    it('list pages after selected page is closed', async () => {
      await withMcpContext(async (response, context) => {
        // Create a second page and select it.
        const page2 = await context.newPage();
        assert.strictEqual(context.getSelectedMcpPage(), page2);

        // Close the selected page via puppeteer (simulating external close).
        await page2.pptrPage.close();

        // list_pages should still work even though the selected page is gone.
        await listPages().handler({params: {}}, response, context);
        assert.ok(response.includePages);
      });
    });
    it(`list pages for extension pages with --category-extensions`, async t => {
      await withMcpContext(
        async (response, context) => {
          const extensionId = await context.installExtension(EXTENSION_PATH);

          assert.ok(extensionId);

          await context.triggerExtensionAction(extensionId);

          const _popupTarget = await context.browser.waitForTarget(
            t => t.type() === 'page' && t.url().includes('chrome-extension://'),
          );

          response.resetResponseLineForTesting();
          const listPageDef = listPages({
            categoryExtensions: true,
          } as ParsedArguments);
          await listPageDef.handler({params: {}}, response, context);

          const result = await response.handle(listPageDef.name, context);
          const textContent = result.content.find(c => c.type === 'text') as {
            type: 'text';
            text: string;
          };
          assert.ok(textContent);

          const text = textContent.text.replaceAll(
            extensionId,
            '<extension-id>',
          );
          t.assert.snapshot(text);
          await context.uninstallExtension(extensionId);
        },
        {},
        {
          categoryExtensions: true,
        },
      );
    });

    for (const categoryExtensions of [true, false]) {
      it(`list pages for extension service workers ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async t => {
        await withMcpContext(
          async (response, context) => {
            const extensionId =
              await context.installExtension(EXTENSION_SW_PATH);
            assert.ok(extensionId);

            const swTarget = await context.browser.waitForTarget(
              target =>
                target.type() === 'service_worker' &&
                target.url().includes('chrome-extension://'),
            );
            const swUrl = swTarget.url();

            const listPageDef = listPages({
              categoryExtensions,
            } as ParsedArguments);
            await listPageDef.handler({params: {}}, response, context);

            const result = await response.handle(listPageDef.name, context);
            const textContent = result.content.find(c => c.type === 'text') as {
              type: 'text';
              text: string;
            };
            assert.ok(textContent);

            if (categoryExtensions) {
              const structured = result.structuredContent as {
                extensionServiceWorkers: Array<{url: string}>;
              };
              assert.deepStrictEqual(
                structured.extensionServiceWorkers.map(sw => sw.url),
                [swUrl],
              );
            }

            const text = textContent.text.replaceAll(
              extensionId,
              '<extension-id>',
            );
            t.assert.snapshot(text);
            await context.uninstallExtension(extensionId);
            const targets = context.browser.targets();
            assertNoServiceWorkerReported(targets, extensionId);
          },
          {},
          {
            categoryExtensions,
          },
        );
      });
    }

    it('list pages for side panels with --category-extensions', async t => {
      await withMcpContext(
        async (response, context) => {
          const extensionId = await context.installExtension(
            EXTENSION_SIDE_PANEL_PATH,
          );

          assert.ok(extensionId);

          const sidePanelPage = await context.newPage();
          await sidePanelPage.pptrPage.goto(
            `chrome-extension://${extensionId}/sidepanel.html`,
          );

          await context.getSelectedMcpPage().waitForTextOnPage(['Side Panel']);

          // Wait for service worker used in the snapshot.
          await context.browser.waitForTarget(
            target => target.type() === 'service_worker',
          );

          const listPageDef = listPages({
            categoryExtensions: true,
          } as ParsedArguments);
          await listPageDef.handler({params: {}}, response, context);

          const result = await response.handle(listPageDef.name, context);
          const textContent = result.content.find(c => c.type === 'text') as {
            type: 'text';
            text: string;
          };
          assert.ok(textContent);

          const text = textContent.text.replaceAll(
            extensionId,
            '<extension-id>',
          );
          t.assert.snapshot(text);
          await context.uninstallExtension(extensionId);
          const targets = context.browser.targets();
          assertNoServiceWorkerReported(targets, extensionId);
        },
        {},
        {
          categoryExtensions: true,
        },
      );
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;

        const dialogPromise = new Promise<Dialog>(resolve => {
          page.on('dialog', dialog => {
            resolve(dialog);
          });
        });

        const evalPromise = page.evaluate(() => {
          alert('test dialog');
        });
        const dialog = await dialogPromise;

        await listPages().handler({params: {}}, response, context);

        const result = await response.handle('list_pages', context);
        t.assert.snapshot(JSON.stringify(result));
        await dialog.dismiss();
        await evalPromise;
      });
    });
  });
  describe('new_page', () => {
    it('create a page', async () => {
      await withMcpContext(async (response, context) => {
        assert.strictEqual(
          context.getPageById(1),
          context.getSelectedMcpPage(),
        );
        await newPage().handler(
          {params: {url: 'about:blank'}},
          response,
          context,
        );
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.ok(response.includePages);
      });
    });
    it('create a page in the background', async () => {
      await withMcpContext(async (response, context) => {
        const originalPage = context.getPageById(1);
        assert.strictEqual(originalPage, context.getSelectedMcpPage());
        // Ensure original page has focus
        await originalPage.pptrPage.bringToFront();
        assert.strictEqual(
          await originalPage.pptrPage.evaluate(() => document.hasFocus()),
          true,
        );
        await newPage().handler(
          {params: {url: 'about:blank', background: true}},
          response,
          context,
        );
        // New page should be selected but original should retain focus
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(
          await originalPage.pptrPage.evaluate(() => document.hasFocus()),
          true,
        );
        assert.ok(response.includePages);
      });
    });
  });
  describe('new_page with isolatedContext', () => {
    it('creates a page in an isolated context', async () => {
      await withMcpContext(async (response, context) => {
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const mcpPage = context.getSelectedMcpPage();
        assert.strictEqual(mcpPage.isolatedContextName, 'session-a');
        assert.ok(response.includePages);
      });
    });

    it('reuses the same context for the same isolatedContext name', async () => {
      await withMcpContext(async (response, context) => {
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const mcpPage1 = context.getSelectedMcpPage();
        const page1 = mcpPage1.pptrPage;
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const mcpPage2 = context.getSelectedMcpPage();
        const page2 = mcpPage2.pptrPage;
        assert.notStrictEqual(page1, page2);
        assert.strictEqual(mcpPage1.isolatedContextName, 'session-a');
        assert.strictEqual(mcpPage2.isolatedContextName, 'session-a');
        assert.strictEqual(page1.browserContext(), page2.browserContext());
      });
    });

    it('creates separate contexts for different isolatedContext names', async () => {
      await withMcpContext(async (response, context) => {
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const mcpPageA = context.getSelectedMcpPage();
        const pageA = mcpPageA.pptrPage;
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-b'}},
          response,
          context,
        );
        const mcpPageB = context.getSelectedMcpPage();
        const pageB = mcpPageB.pptrPage;
        assert.strictEqual(mcpPageA.isolatedContextName, 'session-a');
        assert.strictEqual(mcpPageB.isolatedContextName, 'session-b');
        assert.notStrictEqual(pageA.browserContext(), pageB.browserContext());
      });
    });

    it('includes isolatedContext in page listing', async () => {
      await withMcpContext(async (response, context) => {
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const result = await response.handle('new_page', context);
        const pages = (
          result.structuredContent as {pages: Array<{isolatedContext?: string}>}
        ).pages;
        const isolatedPage = pages.find(p => p.isolatedContext === 'session-a');
        assert.ok(isolatedPage);
      });
    });

    it('does not set isolatedContext for pages in the default context', async () => {
      await withMcpContext(async (response, context) => {
        const mcpPage = context.getSelectedMcpPage();
        assert.strictEqual(mcpPage.isolatedContextName, undefined);
        await newPage().handler(
          {params: {url: 'about:blank'}},
          response,
          context,
        );
        assert.strictEqual(
          context.getSelectedMcpPage().isolatedContextName,
          undefined,
        );
      });
    });

    it('closes an isolated page without errors', async () => {
      await withMcpContext(async (response, context) => {
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'session-a'}},
          response,
          context,
        );
        const page = context.getSelectedMcpPage().pptrPage;
        const pageId = context.getSelectedMcpPage().id;
        assert.ok(!page.isClosed());
        await closePage.handler({params: {pageId}}, response, context);
        assert.ok(page.isClosed());
      });
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;

        const dialogPromise = new Promise<Dialog>(resolve => {
          page.on('dialog', dialog => {
            resolve(dialog);
          });
        });

        const evalPromise = page.evaluate(() => {
          alert('test dialog');
        });
        const dialog = await dialogPromise;

        await newPage().handler(
          {params: {url: 'about:blank'}},
          response,
          context,
        );

        const result = await response.handle('new_page', context);
        t.assert.snapshot(JSON.stringify(result));
        await dialog.dismiss();
        await evalPromise;
      });
    });
  });

  it('navigate_page targets the pageId page, not the global selection', async () => {
    await withMcpContext(async (response, context) => {
      await newPage().handler(
        {
          params: {
            url: 'data:text/html,<h1>Initial</h1>',
            isolatedContext: 'nav-ctx',
          },
        },
        response,
        context,
      );
      const isolatedPage = context.getSelectedMcpPage();

      // Switch global selection back to the default page.
      await selectPage.handler({params: {pageId: 1}}, response, context);
      assert.notStrictEqual(context.getSelectedMcpPage(), isolatedPage);

      // Navigate using page; should target the isolated page.
      await navigatePage().handler(
        {
          params: {
            url: 'data:text/html,<h1>Navigated</h1>',
          },
          page: isolatedPage,
        },
        response,
        context,
      );

      // Verify the isolated page was navigated.
      const content = await isolatedPage.pptrPage.evaluate(
        () => document.querySelector('h1')?.textContent,
      );
      assert.strictEqual(content, 'Navigated');

      // Verify the default page was NOT affected.
      const defaultContent = await context
        .getSelectedMcpPage()
        .pptrPage.evaluate(() => document.querySelector('h1')?.textContent);
      assert.notStrictEqual(defaultContent, 'Navigated');
    });
  });

  describe('close_page', () => {
    it('closes a page', async () => {
      await withMcpContext(async (response, context) => {
        const page = await context.newPage();
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(context.getPageById(2), page);
        await closePage.handler({params: {pageId: 2}}, response, context);
        assert.ok(page.pptrPage.isClosed());
        assert.ok(response.includePages);
      });
    });
    it('cannot close the last page', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await closePage.handler({params: {pageId: 1}}, response, context);
        assert.deepStrictEqual(
          response.responseLines[0],
          `The last open page cannot be closed. It is fine to keep it open.`,
        );
        assert.ok(response.includePages);
        assert.ok(!page.isClosed());
      });
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = await context.newPage();
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(context.getPageById(2), page);

        const dialogPromise = new Promise<void>(resolve => {
          page.pptrPage.on('dialog', () => resolve());
        });

        page.pptrPage
          .evaluate(() => {
            alert('test dialog');
          })
          .catch(() => {
            // Ignore TargetCloseError when page is closed with open dialog
          });
        await dialogPromise;

        await closePage.handler({params: {pageId: 2}}, response, context);

        const result = await response.handle('close_page', context);
        t.assert.snapshot(JSON.stringify(result));
      });
    });
  });
  describe('select_page', () => {
    it('selects a page', async () => {
      await withMcpContext(async (response, context) => {
        await context.newPage();
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        await selectPage.handler({params: {pageId: 1}}, response, context);
        assert.strictEqual(
          context.getPageById(1),
          context.getSelectedMcpPage(),
        );
        assert.ok(response.includePages);
      });
    });
    it('selects a page and keeps it focused in the background', async () => {
      await withMcpContext(async (response, context) => {
        await context.newPage();
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(
          await context
            .getPageById(1)
            .pptrPage.evaluate(() => document.hasFocus()),
          true,
        );
        await selectPage.handler({params: {pageId: 1}}, response, context);
        assert.strictEqual(
          context.getPageById(1),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(
          await context
            .getPageById(1)
            .pptrPage.evaluate(() => document.hasFocus()),
          true,
        );
        assert.ok(response.includePages);
      });
    });
    it('preserves focus across different browser contexts', async () => {
      await withMcpContext(async (response, context) => {
        // Create pages in separate isolated contexts.
        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'ctx-a'}},
          response,
          context,
        );
        const pageA = context.getSelectedMcpPage().pptrPage;
        const pageAId = context.getSelectedMcpPage().id;

        await newPage().handler(
          {params: {url: 'about:blank', isolatedContext: 'ctx-b'}},
          response,
          context,
        );
        const pageB = context.getSelectedMcpPage().pptrPage;

        // Selecting pageB (ctx-b) should not defocus pageA (ctx-a).
        assert.strictEqual(
          await pageA.evaluate(() => document.hasFocus()),
          true,
        );
        assert.strictEqual(
          await pageB.evaluate(() => document.hasFocus()),
          true,
        );

        // Switching back to pageA should preserve pageB's focus.
        await selectPage.handler(
          {params: {pageId: pageAId}},
          response,
          context,
        );
        assert.strictEqual(
          await pageA.evaluate(() => document.hasFocus()),
          true,
        );
        assert.strictEqual(
          await pageB.evaluate(() => document.hasFocus()),
          true,
        );
      });
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;

        const dialogPromise = new Promise<Dialog>(resolve => {
          page.on('dialog', dialog => {
            resolve(dialog);
          });
        });

        const evalPromise = page.evaluate(() => {
          alert('test dialog');
        });
        const dialog = await dialogPromise;

        await selectPage.handler({params: {pageId: 1}}, response, context);

        const result = await response.handle('select_page', context);
        t.assert.snapshot(JSON.stringify(result));
        await dialog.dismiss();
        await evalPromise;
      });
    });
  });
  describe('navigate_page', () => {
    it('navigates to correct page', async () => {
      await withMcpContext(async (response, context) => {
        await navigatePage().handler(
          {
            params: {url: 'data:text/html,<div>Hello MCP</div>'},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        const page = context.getSelectedMcpPage().pptrPage;
        assert.equal(
          await page.evaluate(() => document.querySelector('div')?.textContent),
          'Hello MCP',
        );
        assert.ok(response.includePages);
      });
    });

    it('throws an error if the page was closed not by the MCP server', async () => {
      await withMcpContext(async (response, context) => {
        const page = await context.newPage();
        assert.strictEqual(
          context.getPageById(2),
          context.getSelectedMcpPage(),
        );
        assert.strictEqual(context.getPageById(2), page);

        await page.pptrPage.close();

        try {
          await navigatePage().handler(
            {
              params: {url: 'data:text/html,<div>Hello MCP</div>'},
              page: context.getSelectedMcpPage(),
            },
            response,
            context,
          );
          assert.fail('should not reach here');
        } catch (err) {
          assert.strictEqual(
            err.message,
            'The selected page has been closed. Call list_pages to see open pages.',
          );
        }
      });
    });

    it('respects the timeout parameter', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const stub = sinon.stub(page, 'waitForNavigation').resolves(null);

        try {
          await navigatePage().handler(
            {
              params: {
                url: 'about:blank',
                timeout: 12345,
              },
              page: context.getSelectedMcpPage(),
            },
            response,
            context,
          );
        } finally {
          stub.restore();
        }

        assert.strictEqual(
          stub.firstCall.args[0]?.timeout,
          12345,
          'The timeout parameter should be passed to waitForNavigation',
        );
      });
    });
    it('go back', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await page.goto('data:text/html,<div>Hello MCP</div>');
        await navigatePage().handler(
          {params: {type: 'back'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.equal(
          await page.evaluate(() => document.location.href),
          'about:blank',
        );
        assert.ok(response.includePages);
      });
    });
    it('go forward', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await page.goto('data:text/html,<div>Hello MCP</div>');
        await page.goBack();
        await navigatePage().handler(
          {params: {type: 'forward'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.equal(
          await page.evaluate(() => document.querySelector('div')?.textContent),
          'Hello MCP',
        );
        assert.ok(response.includePages);
      });
    });
    it('reload', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await page.goto('data:text/html,<div>Hello MCP</div>');
        await navigatePage().handler(
          {params: {type: 'reload'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.equal(
          await page.evaluate(() => document.location.href),
          'data:text/html,<div>Hello MCP</div>',
        );
        assert.ok(response.includePages);
      });
    });

    it('reload with accpeting the beforeunload dialog', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await page.setContent(
          html` <script>
            window.addEventListener('beforeunload', e => {
              e.preventDefault();
              e.returnValue = '';
            });
          </script>`,
        );

        await navigatePage().handler(
          {params: {type: 'reload'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.strictEqual(context.getSelectedMcpPage().getDialog(), undefined);
        assert.ok(response.includePages);
        assert.strictEqual(
          response.responseLines.join('\n'),
          'Successfully reloaded the page.\nAccepted a beforeunload dialog.',
        );
      });
    });

    it('reload with declining the beforeunload dialog', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        await page.setContent(
          html` <script>
            window.addEventListener('beforeunload', e => {
              e.preventDefault();
              e.returnValue = '';
            });
          </script>`,
        );

        await navigatePage().handler(
          {
            params: {
              type: 'reload',
              handleBeforeUnload: 'dismiss',
              timeout: 500,
            },
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );

        assert.strictEqual(context.getSelectedMcpPage().getDialog(), undefined);
        assert.ok(response.includePages);
        assert.strictEqual(
          response.responseLines.join('\n'),
          'Unable to reload the selected page: Navigation timeout of 500 ms exceeded.\nDismissed a beforeunload dialog.',
        );
      });
    });

    it('go forward with error', async () => {
      await withMcpContext(async (response, context) => {
        await navigatePage().handler(
          {params: {type: 'forward'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.ok(
          response.responseLines
            .at(0)
            ?.startsWith('Unable to navigate forward in the selected page:'),
        );
        assert.ok(response.includePages);
      });
    });
    it('go back with error', async () => {
      await withMcpContext(async (response, context) => {
        await navigatePage().handler(
          {params: {type: 'back'}, page: context.getSelectedMcpPage()},
          response,
          context,
        );

        assert.ok(
          response.responseLines
            .at(0)
            ?.startsWith('Unable to navigate back in the selected page:'),
        );
        assert.ok(response.includePages);
      });
    });
    it('navigates to correct page with initScript', async () => {
      await withMcpContext(async (response, context) => {
        await navigatePage().handler(
          {
            params: {
              url: 'data:text/html,<div>Hello MCP</div>',
              initScript: 'window.initScript = "completed"',
            },
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        const page = context.getSelectedMcpPage().pptrPage;

        // wait for up to 1s for the global variable to set by the initScript to exist
        await page.waitForFunction("window.initScript==='completed'", {
          timeout: 1000,
        });

        assert.ok(response.includePages);
      });
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const dialogPromise = new Promise<void>(resolve => {
          page.on('dialog', () => resolve());
        });

        page
          .evaluate(() => {
            alert('test dialog');
          })
          .catch(() => {
            // Ignore error when navigation destroys the execution context
          });
        await dialogPromise;

        await navigatePage().handler(
          {
            params: {url: 'data:text/html,<div>Navigated</div>'},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );

        const result = await response.handle('navigate_page', context);
        t.assert.snapshot(JSON.stringify(result));
      });
    });
  });
  describe('resize', () => {
    it('resize the page', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const resizePromise = page.evaluate(() => {
          return new Promise(resolve => {
            window.addEventListener('resize', resolve, {once: true});
          });
        });
        await resizePage.handler(
          {
            params: {width: 700, height: 500},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        await resizePromise;
        await page.waitForFunction(
          () => window.innerWidth === 700 && window.innerHeight === 500,
        );
        const dimensions = await page.evaluate(() => {
          return [window.innerWidth, window.innerHeight];
        });
        assert.deepStrictEqual(dimensions, [700, 500]);
      });
    });

    it('resize when window state is normal', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const browser = page.browser();
        const windowId = await page.windowId();
        await browser.setWindowBounds(windowId, {windowState: 'normal'});

        const {windowState} = await browser.getWindowBounds(windowId);
        assert.strictEqual(windowState, 'normal');

        const resizePromise = page.evaluate(() => {
          return new Promise(resolve => {
            window.addEventListener('resize', resolve, {once: true});
          });
        });
        await resizePage.handler(
          {
            params: {width: 650, height: 450},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        await resizePromise;
        await page.waitForFunction(
          () => window.innerWidth === 650 && window.innerHeight === 450,
        );
        const dimensions = await page.evaluate(() => {
          return [window.innerWidth, window.innerHeight];
        });
        assert.deepStrictEqual(dimensions, [650, 450]);
      });
    });

    it('resize when window state is minimized', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const browser = page.browser();
        const windowId = await page.windowId();
        await browser.setWindowBounds(windowId, {windowState: 'minimized'});

        const {windowState} = await browser.getWindowBounds(windowId);
        assert.strictEqual(windowState, 'minimized');

        const resizePromise = page.evaluate(() => {
          return new Promise(resolve => {
            window.addEventListener('resize', resolve, {once: true});
          });
        });
        await resizePage.handler(
          {
            params: {width: 750, height: 550},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        await resizePromise;
        await page.waitForFunction(
          () => window.innerWidth === 750 && window.innerHeight === 550,
        );
        const dimensions = await page.evaluate(() => {
          return [window.innerWidth, window.innerHeight];
        });
        assert.deepStrictEqual(dimensions, [750, 550]);
      });
    });

    it('resize when window state is maximized', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const browser = page.browser();
        const windowId = await page.windowId();
        await browser.setWindowBounds(windowId, {windowState: 'maximized'});

        const {windowState} = await browser.getWindowBounds(windowId);
        assert.strictEqual(windowState, 'maximized');

        const resizePromise = page.evaluate(() => {
          return new Promise(resolve => {
            window.addEventListener('resize', resolve, {once: true});
          });
        });
        await resizePage.handler(
          {
            params: {width: 725, height: 525},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        await resizePromise;
        await page.waitForFunction(
          () => window.innerWidth === 725 && window.innerHeight === 525,
        );
        const dimensions = await page.evaluate(() => {
          return [window.innerWidth, window.innerHeight];
        });
        assert.deepStrictEqual(dimensions, [725, 525]);
      });
    });

    it('resize when window state is fullscreen', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const browser = page.browser();
        const windowId = await page.windowId();
        await browser.setWindowBounds(windowId, {windowState: 'fullscreen'});

        const {windowState} = await browser.getWindowBounds(windowId);
        assert.strictEqual(windowState, 'fullscreen');

        const resizePromise = page.evaluate(() => {
          return new Promise(resolve => {
            window.addEventListener('resize', resolve, {once: true});
          });
        });
        await resizePage.handler(
          {
            params: {width: 850, height: 650},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        await resizePromise;
        await page.waitForFunction(
          () => window.innerWidth === 850 && window.innerHeight === 650,
        );
        const dimensions = await page.evaluate(() => {
          return [window.innerWidth, window.innerHeight];
        });
        assert.deepStrictEqual(dimensions, [850, 650]);
      });
    });

    it('when dialog is open', async t => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const dialogPromise = new Promise<Dialog>(resolve => {
          page.on('dialog', dialog => {
            resolve(dialog);
          });
        });

        const evalPromise = page.evaluate(() => {
          alert('test dialog');
        });
        const dialog = await dialogPromise;

        await resizePage.handler(
          {
            params: {width: 1600, height: 1400},
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );

        const result = await response.handle('resize_page', context);
        t.assert.snapshot(JSON.stringify(result));
        await dialog.dismiss();
        await evalPromise;
      });
    });
  });

  describe('dialogs', () => {
    it('can accept dialogs', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const dialogPromise = new Promise<void>(resolve => {
          page.on('dialog', () => {
            resolve();
          });
        });
        const evalPromise = page.evaluate(() => {
          alert('test');
        });
        await dialogPromise;
        await handleDialog.handler(
          {
            params: {
              action: 'accept',
            },
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        assert.strictEqual(context.getSelectedMcpPage().getDialog(), undefined);
        assert.strictEqual(
          response.responseLines[0],
          'Successfully accepted the dialog',
        );
        await evalPromise;
      });
    });
    it('can dismiss dialogs', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const dialogPromise = new Promise<void>(resolve => {
          page.on('dialog', () => {
            resolve();
          });
        });
        const evalPromise = page.evaluate(() => {
          alert('test');
        });
        await dialogPromise;
        await handleDialog.handler(
          {
            params: {
              action: 'dismiss',
            },
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        assert.strictEqual(context.getSelectedMcpPage().getDialog(), undefined);
        assert.strictEqual(
          response.responseLines[0],
          'Successfully dismissed the dialog',
        );
        await evalPromise;
      });
    });
    it('can dismiss already dismissed dialog dialogs', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        const dialogPromise = new Promise<Dialog>(resolve => {
          page.on('dialog', dialog => {
            resolve(dialog);
          });
        });
        const evalPromise = page.evaluate(() => {
          alert('test');
        });
        const dialog = await dialogPromise;
        await dialog.dismiss();
        await handleDialog.handler(
          {
            params: {
              action: 'dismiss',
            },
            page: context.getSelectedMcpPage(),
          },
          response,
          context,
        );
        assert.strictEqual(context.getSelectedMcpPage().getDialog(), undefined);
        assert.strictEqual(
          response.responseLines[0],
          'Successfully dismissed the dialog',
        );
        await evalPromise;
      });
    });
    it('can handle a dialog on a non-selected page via pageId', async () => {
      await withMcpContext(async (response, context) => {
        const page1 = context.getSelectedMcpPage();
        await context.newPage(); // page2 is now selected

        const dialogPromise = new Promise<void>(resolve => {
          page1.pptrPage.once('dialog', () => {
            resolve();
          });
        });
        const evalPromise = page1.pptrPage.evaluate(() => {
          alert('test');
        });
        await dialogPromise;

        // page1 is not selected, but its dialog should be accessible via page.
        await handleDialog.handler(
          {
            params: {
              action: 'accept',
            },
            page: page1,
          },
          response,
          context,
        );
        assert.strictEqual(page1.getDialog(), undefined);
        assert.strictEqual(
          response.responseLines[0],
          'Successfully accepted the dialog',
        );
        await evalPromise;
      });
    });
    it('tracks dialogs independently per page', async () => {
      await withMcpContext(async (response, context) => {
        const page1 = context.getSelectedMcpPage();
        await context.newPage();
        const page2 = context.getSelectedMcpPage();

        // Trigger dialog on page1.
        const dialog1Promise = new Promise<void>(resolve => {
          page1.pptrPage.once('dialog', () => {
            resolve();
          });
        });
        const eval1Promise = page1.pptrPage.evaluate(() => {
          alert('dialog1');
        });
        await dialog1Promise;

        // Trigger dialog on page2.
        const dialog2Promise = new Promise<void>(resolve => {
          page2.pptrPage.once('dialog', () => {
            resolve();
          });
        });
        const eval2Promise = page2.pptrPage.evaluate(() => {
          alert('dialog2');
        });
        await dialog2Promise;

        // Both dialogs should be tracked.
        assert.ok(page1.getDialog());
        assert.ok(page2.getDialog());

        // Handle page1's dialog; page2's should remain.
        await handleDialog.handler(
          {params: {action: 'accept'}, page: page1},
          response,
          context,
        );
        assert.strictEqual(page1.getDialog(), undefined);
        assert.ok(page2.getDialog());

        // Handle page2's dialog.
        await handleDialog.handler(
          {params: {action: 'dismiss'}, page: page2},
          response,
          context,
        );
        assert.strictEqual(page2.getDialog(), undefined);

        await Promise.all([eval1Promise, eval2Promise]);
      });
    });
  });

  describe('get_tab_id', () => {
    it('returns the tab id', async () => {
      await withMcpContext(async (response, context) => {
        const page = context.getSelectedMcpPage().pptrPage;
        // @ts-expect-error _tabId is internal.
        assert.ok(typeof page._tabId === 'string');
        // @ts-expect-error _tabId is internal.
        page._tabId = 'test-tab-id';
        await getTabId.handler(
          {params: {pageId: 1}, page: context.getSelectedMcpPage()},
          response,
          context,
        );
        const result = await response.handle('get_tab_id', context);
        // @ts-expect-error _tabId is internal.
        assert.strictEqual(result.structuredContent.tabId, 'test-tab-id');
        assert.deepStrictEqual(response.responseLines, []);
      });
    });
  });
});
