import { describe, expect, it } from 'bun:test';
import { bundledServerPath, runMcpRequests, getResponseText, getMockDropzoneState, mkdtempSync, readFileSync, writeFileSync, tmpdir, join } from './browser-mcp-test-harness';
import type { MockPageState } from './browser-mcp-test-harness';

describe('ccs-browser MCP server - advanced interactions', () => {
  it('drags local files onto normal, frame, and shadow dropzones', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-drag-files-'));
    const invoicePath = join(tempDir, 'invoice.pdf');
    const receiptPath = join(tempDir, 'receipt.png');
    writeFileSync(invoicePath, 'invoice');
    writeFileSync(receiptPath, 'receipt');

    const pages: MockPageState[] = [
      {
        id: 'page-1',
        title: 'Uploads',
        currentUrl: 'https://example.com/uploads',
        dropzones: {
          '#dropzone': { accepted: true },
          '#cancel-dropzone': { acceptedByCancel: true },
        },
        frames: [
          {
            selector: '#upload-frame',
            dropzones: {
              '#frame-dropzone': { accepted: true },
            },
          },
        ],
        shadowRoots: [
          {
            hostSelector: 'upload-panel',
            dropzones: {
              '#shadow-dropzone': { accepted: true },
            },
          },
        ],
      },
    ];

    const responses = await runMcpRequests(pages, [
      {
        jsonrpc: '2.0',
        id: 901,
        method: 'tools/call',
        params: {
          name: 'browser_drag_files',
          arguments: { selector: '#dropzone', files: [invoicePath, receiptPath] },
        },
      },
      {
        jsonrpc: '2.0',
        id: 902,
        method: 'tools/call',
        params: {
          name: 'browser_drag_files',
          arguments: {
            selector: '#frame-dropzone',
            files: [invoicePath],
            frameSelector: '#upload-frame',
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 903,
        method: 'tools/call',
        params: {
          name: 'browser_drag_files',
          arguments: {
            selector: '#shadow-dropzone',
            files: [receiptPath],
            pierceShadow: true,
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 904,
        method: 'tools/call',
        params: {
          name: 'browser_drag_files',
          arguments: {
            selector: '#cancel-dropzone',
            files: [invoicePath],
          },
        },
      },
    ]);

    expect(getResponseText(responses.find((message) => message.id === 901))).toContain('status: files-dropped');
    expect(getMockDropzoneState(pages[0]!, '#dropzone')?.receivedEventTypes).toEqual([
      'dragenter',
      'dragover',
      'drop',
    ]);
    expect(getMockDropzoneState(pages[0]!, '#dropzone')?.receivedFiles).toEqual([
      { name: 'invoice.pdf', size: 7, type: 'application/pdf' },
      { name: 'receipt.png', size: 7, type: 'image/png' },
    ]);
    expect(
      getMockDropzoneState(pages[0]!, '#frame-dropzone', { frameSelector: '#upload-frame' })?.receivedFiles
    ).toEqual([{ name: 'invoice.pdf', size: 7, type: 'application/pdf' }]);
    expect(
      getMockDropzoneState(pages[0]!, '#shadow-dropzone', { pierceShadow: true })?.receivedFiles
    ).toEqual([{ name: 'receipt.png', size: 7, type: 'image/png' }]);
    expect(getResponseText(responses.find((message) => message.id === 904))).toContain(
      'status: files-dropped'
    );
    expect(getMockDropzoneState(pages[0]!, '#cancel-dropzone')?.receivedFiles).toEqual([
      { name: 'invoice.pdf', size: 7, type: 'application/pdf' },
    ]);
  });

  it('rejects browser_drag_files for missing files, page conflicts, missing targets, and rejected drops', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-drag-files-'));
    const okPath = join(tempDir, 'ok.txt');
    const missingPath = join(tempDir, 'missing.txt');
    writeFileSync(okPath, 'ok');

    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Uploads',
          currentUrl: 'https://example.com/uploads',
          dropzones: {
            '#reject-dropzone': { accepted: false },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 904,
          method: 'tools/call',
          params: {
            name: 'browser_drag_files',
            arguments: { selector: '#reject-dropzone', files: [missingPath] },
          },
        },
        {
          jsonrpc: '2.0',
          id: 905,
          method: 'tools/call',
          params: {
            name: 'browser_drag_files',
            arguments: { pageIndex: 0, pageId: 'page-1', selector: '#reject-dropzone', files: [okPath] },
          },
        },
        {
          jsonrpc: '2.0',
          id: 906,
          method: 'tools/call',
          params: {
            name: 'browser_drag_files',
            arguments: { selector: '#missing-dropzone', files: [okPath] },
          },
        },
        {
          jsonrpc: '2.0',
          id: 907,
          method: 'tools/call',
          params: {
            name: 'browser_drag_files',
            arguments: { selector: '#reject-dropzone', files: [okPath] },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 904))).toContain('file does not exist');
    expect(getResponseText(responses.find((message) => message.id === 905))).toContain(
      'pageIndex and pageId cannot be used together'
    );
    expect(getResponseText(responses.find((message) => message.id === 906))).toContain(
      'element not found for selector: #missing-dropzone'
    );
    expect(getResponseText(responses.find((message) => message.id === 907))).toContain(
      'drop target rejected files'
    );
  });

  it('drags an element to another element and to explicit coordinates', async () => {
    const pages: MockPageState[] = [
      {
        id: 'page-1',
        title: 'Drag Page',
        currentUrl: 'https://example.com/drag',
        query: {
          '#card-a': {
            exists: true,
            connected: true,
            rect: {
              x: 20,
              y: 40,
              width: 100,
              height: 60,
              top: 40,
              right: 120,
              bottom: 100,
              left: 20,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
          '#lane-b': {
            exists: true,
            connected: true,
            rect: {
              x: 240,
              y: 60,
              width: 120,
              height: 80,
              top: 60,
              right: 360,
              bottom: 140,
              left: 240,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
        },
      },
      {
        id: 'page-2',
        title: 'Second Drag Page',
        currentUrl: 'https://example.com/drag-2',
        query: {
          '#card-a': {
            exists: true,
            connected: true,
            rect: {
              x: 10,
              y: 20,
              width: 40,
              height: 20,
              top: 20,
              right: 50,
              bottom: 40,
              left: 10,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
          '#lane-b': {
            exists: true,
            connected: true,
            rect: {
              x: 110,
              y: 80,
              width: 60,
              height: 30,
              top: 80,
              right: 170,
              bottom: 110,
              left: 110,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
        },
      },
    ];

    const responses = await runMcpRequests(pages, [
      {
        jsonrpc: '2.0',
        id: 908,
        method: 'tools/call',
        params: {
          name: 'browser_drag_element',
          arguments: { selector: '#card-a', targetSelector: '#lane-b', steps: 3 },
        },
      },
      {
        jsonrpc: '2.0',
        id: 909,
        method: 'tools/call',
        params: {
          name: 'browser_drag_element',
          arguments: { selector: '#card-a', targetX: 420, targetY: 180, steps: 2 },
        },
      },
      {
        jsonrpc: '2.0',
        id: 916,
        method: 'tools/call',
        params: {
          name: 'browser_drag_element',
          arguments: { pageId: 'page-2', selector: '#card-a', targetSelector: '#lane-b', steps: 1 },
        },
      },
    ]);

    expect(getResponseText(responses.find((message) => message.id === 908))).toContain('status: dragged');
    expect(getResponseText(responses.find((message) => message.id === 908))).toContain('targetSelector: #lane-b');
    expect(getResponseText(responses.find((message) => message.id === 909))).toContain('status: dragged');
    expect(getResponseText(responses.find((message) => message.id === 909))).toContain('targetX: 420');
    expect(getResponseText(responses.find((message) => message.id === 909))).toContain('targetY: 180');
    expect(getResponseText(responses.find((message) => message.id === 916))).toContain('pageIndex: 1');
    expect(getResponseText(responses.find((message) => message.id === 916))).toContain('targetSelector: #lane-b');
    expect(pages[0]!.drag?.recordedActions).toEqual([
      { type: 'mouseMoved', x: 70, y: 70, button: 'none' },
      { type: 'mousePressed', x: 70, y: 70, button: 'left' },
      { type: 'mouseMoved', x: 147, y: 80, button: 'left' },
      { type: 'mouseMoved', x: 223, y: 90, button: 'left' },
      { type: 'mouseMoved', x: 300, y: 100, button: 'left' },
      { type: 'mouseReleased', x: 300, y: 100, button: 'left' },
      { type: 'mouseMoved', x: 70, y: 70, button: 'none' },
      { type: 'mousePressed', x: 70, y: 70, button: 'left' },
      { type: 'mouseMoved', x: 245, y: 125, button: 'left' },
      { type: 'mouseMoved', x: 420, y: 180, button: 'left' },
      { type: 'mouseReleased', x: 420, y: 180, button: 'left' },
    ]);
    expect(pages[1]!.drag?.recordedActions).toEqual([
      { type: 'mouseMoved', x: 30, y: 30, button: 'none' },
      { type: 'mousePressed', x: 30, y: 30, button: 'left' },
      { type: 'mouseMoved', x: 140, y: 95, button: 'left' },
      { type: 'mouseReleased', x: 140, y: 95, button: 'left' },
    ]);
  });

  it('rejects invalid browser_drag_element target combinations, page conflicts, and missing elements', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Drag Failures',
          currentUrl: 'https://example.com/drag',
          query: {
            '#card-a': {
              exists: true,
              connected: true,
              rect: {
                x: 20,
                y: 40,
                width: 100,
                height: 60,
                top: 40,
                right: 120,
                bottom: 100,
                left: 20,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 910,
          method: 'tools/call',
          params: {
            name: 'browser_drag_element',
            arguments: { selector: '#card-a', targetSelector: '#lane-b', targetX: 400, targetY: 200 },
          },
        },
        {
          jsonrpc: '2.0',
          id: 911,
          method: 'tools/call',
          params: {
            name: 'browser_drag_element',
            arguments: { selector: '#missing-source', targetX: 400, targetY: 200 },
          },
        },
        {
          jsonrpc: '2.0',
          id: 912,
          method: 'tools/call',
          params: {
            name: 'browser_drag_element',
            arguments: { selector: '#card-a', targetSelector: '#missing-target' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 917,
          method: 'tools/call',
          params: {
            name: 'browser_drag_element',
            arguments: { pageIndex: 0, pageId: 'page-1', selector: '#card-a', targetX: 400, targetY: 200 },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 910))).toContain(
      'targetSelector and targetX/targetY cannot be used together'
    );
    expect(getResponseText(responses.find((message) => message.id === 911))).toContain(
      'source element not found'
    );
    expect(getResponseText(responses.find((message) => message.id === 912))).toContain(
      'target element not found'
    );
    expect(getResponseText(responses.find((message) => message.id === 917))).toContain(
      'pageIndex and pageId cannot be used together'
    );
  });

  it('runs minimal pointer sequences with browser_pointer_action', async () => {
    const pages: MockPageState[] = [
      {
        id: 'page-1',
        title: 'Pointer Page',
        currentUrl: 'https://example.com/pointer',
        query: {
          '#handle': {
            exists: true,
            connected: true,
            rect: {
              x: 30,
              y: 50,
              width: 40,
              height: 20,
              top: 50,
              right: 70,
              bottom: 70,
              left: 30,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
        },
      },
      {
        id: 'page-2',
        title: 'Second Pointer Page',
        currentUrl: 'https://example.com/pointer-2',
        query: {
          '#handle': {
            exists: true,
            connected: true,
            rect: {
              x: 100,
              y: 200,
              width: 20,
              height: 20,
              top: 200,
              right: 120,
              bottom: 220,
              left: 100,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
        },
      },
    ];

    const responses = await runMcpRequests(pages, [
      {
        jsonrpc: '2.0',
        id: 913,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            actions: [
              { type: 'move', selector: '#handle' },
              { type: 'down' },
              { type: 'move', x: 220, y: 160 },
              { type: 'pause', durationMs: 5 },
              { type: 'up' },
            ],
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 918,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            pageId: 'page-2',
            actions: [
              { type: 'move', selector: '#handle' },
              { type: 'down' },
              { type: 'up' },
            ],
          },
        },
      },
    ]);

    expect(getResponseText(responses.find((message) => message.id === 913))).toContain('status: pointer-actions-completed');
    expect(getResponseText(responses.find((message) => message.id === 918))).toContain('pageIndex: 1');
    expect(getResponseText(responses.find((message) => message.id === 918))).toContain('status: pointer-actions-completed');
    expect(pages[0]!.drag?.recordedActions).toEqual([
      { type: 'mouseMoved', x: 50, y: 60, button: 'none' },
      { type: 'mousePressed', x: 50, y: 60, button: 'left' },
      { type: 'mouseMoved', x: 220, y: 160, button: 'left' },
      { type: 'mouseReleased', x: 220, y: 160, button: 'left' },
    ]);
    expect(pages[1]!.drag?.recordedActions).toEqual([
      { type: 'mouseMoved', x: 110, y: 210, button: 'none' },
      { type: 'mousePressed', x: 110, y: 210, button: 'left' },
      { type: 'mouseReleased', x: 110, y: 210, button: 'left' },
    ]);
  });

  it('rejects invalid browser_pointer_action sequences and page conflicts', async () => {
    const pages: MockPageState[] = [
      {
        id: 'page-1',
        title: 'Pointer Failures',
        currentUrl: 'https://example.com/pointer',
        query: {
          '#handle': {
            exists: true,
            connected: true,
            rect: {
              x: 30,
              y: 50,
              width: 40,
              height: 20,
              top: 50,
              right: 70,
              bottom: 70,
              left: 30,
            },
            display: 'block',
            visibility: 'visible',
            opacity: '1',
          },
        },
      },
    ];

    const responses = await runMcpRequests(pages, [
      {
        jsonrpc: '2.0',
        id: 914,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            actions: [{ type: 'up' }],
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 915,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            actions: [{ type: 'move', selector: '#missing-handle' }],
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 919,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            pageIndex: 0,
            pageId: 'page-1',
            actions: [{ type: 'move', x: 10, y: 10 }],
          },
        },
      },
      {
        jsonrpc: '2.0',
        id: 920,
        method: 'tools/call',
        params: {
          name: 'browser_pointer_action',
          arguments: {
            actions: [
              { type: 'move', selector: '#handle' },
              { type: 'down', button: 'left' },
              { type: 'up', button: 'right' },
            ],
          },
        },
      },
    ]);

    expect(getResponseText(responses.find((message) => message.id === 914))).toContain('pointer state error');
    expect(getResponseText(responses.find((message) => message.id === 915))).toContain('drag coordinates unavailable');
    expect(getResponseText(responses.find((message) => message.id === 919))).toContain(
      'pageIndex and pageId cannot be used together'
    );
    expect(getResponseText(responses.find((message) => message.id === 920))).toContain(
      'pointer state error'
    );
  });

  it('rejects mutating browser tools when the selected page becomes stale', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-stale-selection-'));
    const patchedServerPath = join(tempDir, 'ccs-browser-server.cjs');
    writeFileSync(
      patchedServerPath,
      readFileSync(bundledServerPath, 'utf8').replace(
        "let selectedPageId = '';",
        "let selectedPageId = 'page-2';"
      )
    );

    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Primary Page',
          currentUrl: 'https://example.com/primary',
          query: {
            '#handle': {
              exists: true,
              connected: true,
              rect: {
                x: 30,
                y: 50,
                width: 40,
                height: 20,
                top: 50,
                right: 70,
                bottom: 70,
                left: 30,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 923,
          method: 'tools/call',
          params: {
            name: 'browser_pointer_action',
            arguments: {
              actions: [{ type: 'move', selector: '#handle' }],
            },
          },
        },
      ],
      { serverPath: patchedServerPath }
    );

    expect(getResponseText(responses.find((message) => message.id === 923))).toContain(
      'Selected page is no longer available; specify pageIndex or pageId explicitly.'
    );
  });

  it('waits for a matching navigation event with browser_wait_for_event', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Event Page',
          currentUrl: 'https://example.com/',
          events: {
            navigations: [{ url: 'https://example.com/checkout' }],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 69,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for_event',
            arguments: {
              timeoutMs: 1000,
              event: { kind: 'navigation', urlIncludes: '/checkout' },
            },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 69))).toContain(
      'status: observed'
    );
  });

  it('ignores child-frame navigations when waiting for a page navigation event', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Event Page',
          currentUrl: 'https://example.com/',
          events: {
            navigations: [
              { url: 'https://example.com/embedded-checkout', parentId: 'frame-1' },
              { url: 'https://example.com/checkout' },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 58,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for_event',
            arguments: {
              timeoutMs: 1000,
              event: { kind: 'navigation', urlIncludes: '/checkout' },
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 58));
    expect(text).toContain('status: observed');
    expect(text).toContain('"url":"https://example.com/checkout"');
    expect(text).not.toContain('embedded-checkout');
  });

  it('filters download events by the selected page frame when pageIndex is provided', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'First Page',
          currentUrl: 'https://example.com/one',
          events: {
            downloads: [
              {
                url: 'https://example.com/first-report.csv',
                suggestedFilename: 'first-report.csv',
                frameId: 'frame-page-1',
              },
            ],
          },
        },
        {
          id: 'page-2',
          title: 'Second Page',
          currentUrl: 'https://example.com/two',
          events: {
            downloads: [
              {
                url: 'https://example.com/second-report.csv',
                suggestedFilename: 'second-report.csv',
                frameId: 'frame-page-2',
              },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 59,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for_event',
            arguments: {
              pageIndex: 1,
              timeoutMs: 1000,
              event: { kind: 'download', suggestedFilenameIncludes: 'second-report' },
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 59));
    expect(text).toContain('status: observed');
    expect(text).toContain('"suggestedFilename":"second-report.csv"');
    expect(text).not.toContain('first-report.csv');
  });

  it('matches download events from child frames within the selected page', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Framed Page',
          currentUrl: 'https://example.com/frame-host',
          frameTree: {
            frame: { id: 'frame-page-1' },
            childFrames: [{ frame: { id: 'frame-page-1-child' } }],
          },
          events: {
            downloads: [
              {
                url: 'https://example.com/embedded-report.csv',
                suggestedFilename: 'embedded-report.csv',
                frameId: 'frame-page-1-child',
              },
            ],
          },
        },
        {
          id: 'page-2',
          title: 'Other Page',
          currentUrl: 'https://example.com/other',
          events: {
            downloads: [
              {
                url: 'https://example.com/other-report.csv',
                suggestedFilename: 'other-report.csv',
                frameId: 'frame-page-2',
              },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 60,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for_event',
            arguments: {
              pageIndex: 0,
              timeoutMs: 1000,
              event: { kind: 'download', suggestedFilenameIncludes: 'embedded-report' },
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 60));
    expect(text).toContain('status: observed');
    expect(text).toContain('"suggestedFilename":"embedded-report.csv"');
    expect(text).not.toContain('other-report.csv');
  });

  it('refreshes frame ids when a download comes from a newly attached child frame', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Dynamic Frame Page',
          currentUrl: 'https://example.com/dynamic',
          frameTreeSequence: [
            { frame: { id: 'frame-page-1' } },
            {
              frame: { id: 'frame-page-1' },
              childFrames: [{ frame: { id: 'frame-page-1-late-child' } }],
            },
          ],
          events: {
            downloads: [
              {
                url: 'https://example.com/late-report.csv',
                suggestedFilename: 'late-report.csv',
                frameId: 'frame-page-1-late-child',
              },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 61,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for_event',
            arguments: {
              pageIndex: 0,
              timeoutMs: 1000,
              event: { kind: 'download', suggestedFilenameIncludes: 'late-report' },
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 61));
    expect(text).toContain('status: observed');
    expect(text).toContain('"suggestedFilename":"late-report.csv"');
  });

  it('captures element screenshots using the post-scroll visible clip and reports failures', async () => {
    const screenshotPlan: MockPageState['screenshot'] = {
      data: 'ZWxlbWVudC1zaG90',
      requireScrolledMeasurement: true,
      expectedClip: {
        x: 0,
        y: 12,
        width: 50,
        height: 28,
        scale: 1,
      },
    };
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Element Screenshot Page',
          currentUrl: 'https://example.com/',
          query: {
            '.edit-button': {
              exists: true,
              connected: true,
              innerText: 'Edit',
              textContent: 'Edit',
              rect: {
                x: -14,
                y: 12,
                width: 64,
                height: 28,
                top: 12,
                right: 50,
                bottom: 40,
                left: -14,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
            '.zero-sized': {
              exists: true,
              connected: true,
              rect: {
                x: 10,
                y: 10,
                width: 0,
                height: 0,
                top: 10,
                right: 10,
                bottom: 10,
                left: 10,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
          },
          screenshot: screenshotPlan,
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_take_element_screenshot',
            arguments: { selector: '.edit-button' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'browser_take_element_screenshot',
            arguments: { selector: '.missing-button' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'browser_take_element_screenshot',
            arguments: { selector: '.zero-sized' },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 2));
    expect(text).toContain('selector: .edit-button');
    expect(text).toContain('format: png');
    expect(text).toContain('data: ZWxlbWVudC1zaG90');

    expect(getResponseText(responses.find((message) => message.id === 3))).toContain(
      'Browser MCP failed: element not found for selector: .missing-button'
    );
    expect(getResponseText(responses.find((message) => message.id === 4))).toContain(
      'Browser MCP failed: element has empty bounds for selector: .zero-sized'
    );

    const emptyPayloadResponses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Element Screenshot Empty Payload Page',
          currentUrl: 'https://example.com/',
          query: {
            '.edit-button': {
              exists: true,
              connected: true,
              rect: {
                x: 220,
                y: 80,
                width: 64,
                height: 28,
                top: 80,
                right: 284,
                bottom: 108,
                left: 220,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
          },
          screenshot: { data: '' },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_take_element_screenshot',
            arguments: { selector: '.edit-button' },
          },
        },
      ]
    );

    expect(getResponseText(emptyPayloadResponses.find((message) => message.id === 2))).toContain(
      'Browser MCP failed: screenshot capture failed'
    );

    const iframeScreenshotResponses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Iframe Screenshot Page',
          currentUrl: 'https://example.com/',
          frames: [
            {
              selector: '#details-frame',
              query: {
                '#details-frame': {
                  exists: true,
                  connected: true,
                  rect: {
                    x: 40,
                    y: 100,
                    width: 300,
                    height: 200,
                    top: 100,
                    right: 340,
                    bottom: 300,
                    left: 40,
                  },
                  display: 'block',
                  visibility: 'visible',
                  opacity: '1',
                },
                '.save-button': {
                  exists: true,
                  connected: true,
                  rect: {
                    x: 10,
                    y: 20,
                    width: 60,
                    height: 24,
                    top: 20,
                    right: 70,
                    bottom: 44,
                    left: 10,
                  },
                  display: 'block',
                  visibility: 'visible',
                  opacity: '1',
                },
              },
            },
          ],
          screenshot: {
            data: 'aWZyYW1lLXNob3Q=',
            requireScrolledMeasurement: true,
            expectedClip: {
              x: 50,
              y: 120,
              width: 60,
              height: 24,
              scale: 1,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: {
            name: 'browser_take_element_screenshot',
            arguments: { selector: '.save-button', frameSelector: '#details-frame' },
          },
        },
      ]
    );

    expect(
      getResponseText(iframeScreenshotResponses.find((message) => message.id === 5))
    ).toContain('data: aWZyYW1lLXNob3Q=');
  });

  it('captures screenshots and reports empty payload failures', async () => {
    const screenshotPlan: MockPageState['screenshot'] = { data: 'c2NyZWVuc2hvdA==' };
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          screenshot: screenshotPlan,
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_take_screenshot',
            arguments: { fullPage: true },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 2));
    expect(text).toContain('pageIndex: 0');
    expect(text).toContain('format: png');
    expect(text).toContain('fullPage: true');
    expect(text).toContain('data: c2NyZWVuc2hvdA==');
    expect(screenshotPlan.lastCaptureBeyondViewport).toBe(true);

    const errorResponses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          screenshot: { data: '' },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_take_screenshot',
            arguments: {},
          },
        },
      ]
    );

    const errorResponse = errorResponses.find((message) => message.id === 2);
    expect((errorResponse?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(errorResponse)).toContain(
      'Browser MCP failed: screenshot capture failed'
    );
  });

  it('types into supported editable targets with explicit focus and final-value verification, and rejects unsupported targets', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          type: {
            'input[name="email"]': {
              kind: 'input',
              inputType: 'email',
              value: 'old@example.com',
              expectedValueWhenAppend: 'old@example.comhi@example.com',
              requireFocus: true,
            },
            '#notes': {
              kind: 'textarea',
              value: 'old note',
              expectedValueWhenClearFirst: '',
              requireFocus: true,
            },
            '#editor': {
              kind: 'contenteditable',
              value: 'old content',
              expectedValueWhenAppend: 'old contentrich text',
              requireFocus: true,
            },
            '#color': { kind: 'unsupported', inputType: 'color', value: '#ff0000' },
            '#plain': { kind: 'noneditable', value: 'plain' },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_type',
            arguments: { selector: 'input[name="email"]', text: 'hi@example.com' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'browser_type',
            arguments: { selector: '#notes', text: '', clearFirst: true },
          },
        },
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'browser_type',
            arguments: { selector: '#editor', text: 'rich text' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: {
            name: 'browser_type',
            arguments: { selector: '#color', text: 'ignored' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 6,
          method: 'tools/call',
          params: {
            name: 'browser_type',
            arguments: { selector: '#plain', text: 'ignored' },
          },
        },
      ]
    );

    const typed = getResponseText(responses.find((message) => message.id === 2));
    expect(typed).toContain('status: typed');
    expect(typed).toContain('selector: input[name="email"]');
    expect(typed).toContain('typedLength: 29');

    const clearFirst = getResponseText(responses.find((message) => message.id === 3));
    expect(clearFirst).toContain('status: typed');
    expect(clearFirst).toContain('selector: #notes');
    expect(clearFirst).toContain('typedLength: 0');

    const contenteditable = getResponseText(responses.find((message) => message.id === 4));
    expect(contenteditable).toContain('status: typed');
    expect(contenteditable).toContain('selector: #editor');
    expect(contenteditable).toContain('typedLength: 20');

    const unsupported = responses.find((message) => message.id === 5);
    expect((unsupported?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(unsupported)).toContain(
      'element is not text-editable for selector: #color'
    );

    const nonEditable = responses.find((message) => message.id === 6);
    expect((nonEditable?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(nonEditable)).toContain(
      'element is not text-editable for selector: #plain'
    );
  });
});
