import { describe, expect, it } from 'bun:test';
import { bundledServerPath, cpSync, getMockFrame, getMockShadowRoot, getResponseText, join, mkdtempSync, resolveNodeModulesPath, rmSync, runMcpRequests, tmpdir, writeFileSync } from './browser-mcp-test-harness';

describe('ccs-browser MCP server - navigation and query', () => {
  it('works from an installed copy when global WebSocket is unavailable and NODE_PATH supplies package dependencies', async () => {
    const installDir = mkdtempSync(join(tmpdir(), 'ccs-browser-installed-copy-'));
    const installedServerPath = join(installDir, 'ccs-browser-server.cjs');
    const bootstrapServerPath = join(installDir, 'bootstrap.cjs');

    try {
      cpSync(bundledServerPath, installedServerPath);
      writeFileSync(
        bootstrapServerPath,
        'delete globalThis.WebSocket;\nrequire("./ccs-browser-server.cjs");\n',
        'utf8'
      );

      const responses = await runMcpRequests(
        [{ id: 'page-1', title: 'Installed Copy', currentUrl: 'https://example.com/' }],
        [
          {
            jsonrpc: '2.0',
            id: 2,
            method: 'tools/call',
            params: { name: 'browser_get_url_and_title', arguments: {} },
          },
        ],
        {
          serverPath: bootstrapServerPath,
          childEnv: {
            NODE_PATH: resolveNodeModulesPath(),
          },
          responseTimeoutMs: 12000,
        }
      );

      const response = responses.find((message) => message.id === 2);
      expect((response?.result as { isError?: boolean }).isError).not.toBe(true);
      expect(getResponseText(response)).toContain('title: Installed Copy');
      expect(getResponseText(response)).toContain('url: https://example.com/');
    } finally {
      rmSync(installDir, { recursive: true, force: true });
    }
  }, 10000);

  it('navigates successfully after readiness polling', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          navigate: {
            'https://example.com/next': {
              finalUrl: 'https://example.com/next',
              readyStates: ['loading', 'interactive'],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { url: 'https://example.com/next' },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 2));
    expect(text).toContain('pageIndex: 0');
    expect(text).toContain('url: https://example.com/next');
    expect(text).toContain('status: navigated');
  });

  it('returns a handled error when navigation readiness times out', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          navigate: {
            'https://example.com/slow': {
              finalUrl: 'https://example.com/',
              readyStates: new Array(60).fill('loading'),
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { url: 'https://example.com/slow' },
          },
        },
      ]
    );

    const response = responses.find((message) => message.id === 2);
    expect((response?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(response)).toContain('Browser MCP failed: navigation did not complete');
  }, 8000);

  it('returns handled errors for missing URL, malformed URL, invalid page index, and out-of-range page index', async () => {
    const responses = await runMcpRequests(
      [{ id: 'page-1', title: 'Example Page', currentUrl: 'https://example.com/' }],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_navigate', arguments: {} },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: { name: 'browser_navigate', arguments: { url: 'file:///tmp/example' } },
        },
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { pageIndex: 1.5, url: 'https://example.com/next' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { pageIndex: 9, url: 'https://example.com/next' },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'Browser MCP failed: url is required'
    );
    expect(getResponseText(responses.find((message) => message.id === 3))).toContain(
      'Browser MCP failed: url must be an absolute http or https URL'
    );
    expect(getResponseText(responses.find((message) => message.id === 4))).toContain(
      'Browser MCP failed: pageIndex must be a non-negative integer'
    );
    expect(getResponseText(responses.find((message) => message.id === 5))).toContain(
      'page index 9 is out of range'
    );
  });

  it('surfaces Page.navigate CDP failures as handled errors', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/start',
          navigate: {
            'https://example.com/fail': {
              finalUrl: 'https://example.com/start',
              errorText: 'net::ERR_ABORTED',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { url: 'https://example.com/fail' },
          },
        },
      ]
    );

    const response = responses.find((message) => message.id === 2);
    expect((response?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(response)).toContain(
      'Browser MCP failed: navigation failed for URL: https://example.com/fail: net::ERR_ABORTED'
    );
  });

  it('treats redirects as successful navigation', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/start',
          navigate: {
            'https://example.com/redirect': {
              finalUrl: 'https://example.com/final',
              readyStates: ['loading', 'interactive'],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_navigate',
            arguments: { url: 'https://example.com/redirect' },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 2));
    expect(text).toContain('status: navigated');
    expect(text).toContain('url: https://example.com/final');
  });

  it('clicks matching elements and reports selector failures', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#submit': {},
            '#disabled': { disabled: true },
            '#throws': { error: 'click exploded' },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#submit' } },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#missing' } },
        },
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#disabled' } },
        },
        {
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#throws' } },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'status: clicked'
    );
    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'selector: #submit'
    );

    const selectorMiss = responses.find((message) => message.id === 3);
    expect((selectorMiss?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(selectorMiss)).toContain(
      'element index 0 is out of range for selector: #missing'
    );

    const disabledError = responses.find((message) => message.id === 4);
    expect((disabledError?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(disabledError)).toContain('element is disabled for selector: #disabled');

    const pageSideError = responses.find((message) => message.id === 5);
    expect((pageSideError?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(pageSideError)).toContain('click exploded');
  });

  it('clicks the requested zero-based match and rejects out-of-range nth', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Click Page',
          currentUrl: 'https://example.com/',
          click: {
            '.menu-item': [{ label: 'first' }, { label: 'second' }],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 20,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '.menu-item', nth: 1 } },
        },
        {
          jsonrpc: '2.0',
          id: 21,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '.menu-item', nth: 3 } },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 20))).toContain(
      'status: clicked'
    );
    expect(getResponseText(responses.find((message) => message.id === 20))).toContain('nth: 1');
    expect(getResponseText(responses.find((message) => message.id === 21))).toContain(
      'Browser MCP failed: element index 3 is out of range for selector: .menu-item'
    );
  });

  it('reports detached and hidden click targets as handled errors', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#hidden': { hidden: true },
            '#detached': { detached: true },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#hidden' } },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#detached' } },
        },
      ]
    );

    const hiddenError = responses.find((message) => message.id === 2);
    expect((hiddenError?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(hiddenError)).toContain(
      'element is hidden or not interactable for selector: #hidden'
    );

    const detachedError = responses.find((message) => message.id === 3);
    expect((detachedError?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(detachedError)).toContain('element is detached for selector: #detached');
  });

  it('uses a mouse sequence when the target requires it', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#menu-trigger': { requireMouseSequence: true },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#menu-trigger' } },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'status: clicked'
    );
    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'selector: #menu-trigger'
    );
  });

  it('preserves mouse sequence preparation without dispatching a synthetic click event', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#click-event': {
              requireMouseSequence: true,
              forbidSyntheticClickEvent: true,
              requireNativeClick: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#click-event' } },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'status: clicked'
    );
    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'selector: #click-event'
    );
  });

  it('preserves native click activation after the mouse sequence', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#native-click': {
              requireMouseSequence: true,
              requireNativeClick: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#native-click' } },
        },
      ]
    );

    const clickResponse = responses.find((message) => message.id === 2);
    expect((clickResponse?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(clickResponse)).toContain('status: clicked');
    expect(getResponseText(clickResponse)).toContain('selector: #native-click');
  });

  it('does not force activation when mousedown cancels the interaction', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#cancel-mousedown': {
              requireMouseSequence: true,
              cancelMouseDown: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#cancel-mousedown' } },
        },
      ]
    );

    const clickResponse = responses.find((message) => message.id === 2);
    expect((clickResponse?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(clickResponse)).toContain('status: clicked');
    expect(getResponseText(clickResponse)).toContain('selector: #cancel-mousedown');
  });

  it('rechecks connectivity before native activation after the mouse sequence', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#detached-during-click': {
              requireMouseSequence: true,
              requireNativeClick: true,
              detachAfterMouseDown: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#detached-during-click' } },
        },
      ]
    );

    const clickResponse = responses.find((message) => message.id === 2);
    expect((clickResponse?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(clickResponse)).toContain('status: clicked');
    expect(getResponseText(clickResponse)).toContain('selector: #detached-during-click');
  });

  it('falls back to click when mouse sequence dispatch fails', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Example Page',
          currentUrl: 'https://example.com/',
          click: {
            '#fallback': { mouseSequenceError: 'mouse dispatch exploded' },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: { name: 'browser_click', arguments: { selector: '#fallback' } },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'status: clicked'
    );
    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'selector: #fallback'
    );
  });

  it('clicks with element-relative offsets and richer click options', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Click Page',
          currentUrl: 'https://example.com/',
          click: {
            '#offset-target': {
              requireMouseSequence: true,
              expectedOffset: { x: 12, y: 8 },
              expectedButton: 'right',
              expectedClickCount: 2,
            },
            '#double-left': {
              requireMouseSequence: true,
              expectedButton: 'left',
              expectedClickCount: 2,
              requireDoubleClickEvent: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 70,
          method: 'tools/call',
          params: {
            name: 'browser_click',
            arguments: {
              selector: '#offset-target',
              offsetX: 12,
              offsetY: 8,
              button: 'right',
              clickCount: 2,
            },
          },
        },
        {
          jsonrpc: '2.0',
          id: 700,
          method: 'tools/call',
          params: {
            name: 'browser_click',
            arguments: {
              selector: '#double-left',
              clickCount: 2,
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 70));
    expect(text).toContain('status: clicked');
    expect(text).toContain('offsetX: 12');
    expect(text).toContain('offsetY: 8');
    expect(text).toContain('button: right');
    expect(text).toContain('clickCount: 2');

    const doubleText = getResponseText(responses.find((message) => message.id === 700));
    expect(doubleText).toContain('status: clicked');
    expect(doubleText).toContain('button: left');
    expect(doubleText).toContain('clickCount: 2');
  });

  it('presses a key combination with browser_press_key', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Keyboard Page',
          currentUrl: 'https://example.com/',
          keyboard: {
            expectedKey: 'k',
            expectedModifiers: ['Meta'],
            expectedRepeat: 2,
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 71,
          method: 'tools/call',
          params: {
            name: 'browser_press_key',
            arguments: {
              key: 'k',
              modifiers: ['Meta'],
              repeat: 2,
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 71));
    expect(text).toContain('status: key-pressed');
    expect(text).toContain('key: k');
    expect(text).toContain('modifiers: Meta');
    expect(text).toContain('repeat: 2');
  });

  it('supports common special keys with browser_press_key', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Special Key Page',
          currentUrl: 'https://example.com/',
          keyboard: {
            expectedKey: 'Enter',
            expectedRepeat: 1,
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 711,
          method: 'tools/call',
          params: {
            name: 'browser_press_key',
            arguments: {
              key: 'Enter',
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 711));
    expect(text).toContain('status: key-pressed');
    expect(text).toContain('key: Enter');
  });

  it('rejects unsupported modifiers for browser_press_key', async () => {
    const responses = await runMcpRequests(
      [{ id: 'page-1', title: 'Invalid Modifier Page', currentUrl: 'https://example.com/' }],
      [
        {
          jsonrpc: '2.0',
          id: 712,
          method: 'tools/call',
          params: {
            name: 'browser_press_key',
            arguments: {
              key: 'k',
              modifiers: ['Cmd'],
            },
          },
        },
      ]
    );

    const response = responses.find((message) => message.id === 712);
    expect((response?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(response)).toContain(
      'modifiers must only contain: Alt, Control, Meta, Shift'
    );
  });

  it('scrolls an element into view with browser_scroll', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Scroll Page',
          currentUrl: 'https://example.com/',
          scroll: {
            '#results': {
              expectedBehavior: 'into-view',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 72,
          method: 'tools/call',
          params: {
            name: 'browser_scroll',
            arguments: {
              selector: '#results',
              behavior: 'into-view',
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 72));
    expect(text).toContain('status: scrolled');
    expect(text).toContain('selector: #results');
    expect(text).toContain('behavior: into-view');
  });

  it('scrolls an iframe page by offset with browser_scroll', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Iframe Scroll Page',
          currentUrl: 'https://example.com/',
          frames: [
            {
              selector: '#preview-frame',
              query: {
                '#preview-frame': {
                  exists: true,
                },
              },
            },
          ],
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 73,
          method: 'tools/call',
          params: {
            name: 'browser_scroll',
            arguments: {
              frameSelector: '#preview-frame',
              behavior: 'by-offset',
              deltaX: 5,
              deltaY: 40,
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 73));
    expect(text).toContain('status: scrolled');
    expect(text).toContain('behavior: by-offset');
    expect(text).toContain('deltaX: 5');
    expect(text).toContain('deltaY: 40');
  });

  it('requires real mouse movement for hover-only targets and reports hover failures', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Hover Page',
          currentUrl: 'https://example.com/',
          hover: {
            '.draft-card': { requireCdpMouseMove: true },
            '.missing-bounds': { zeroSized: true },
          },
          query: {
            '.draft-card': {
              exists: true,
              connected: true,
              rect: {
                x: 100,
                y: 40,
                width: 120,
                height: 48,
                top: 40,
                right: 220,
                bottom: 88,
                left: 100,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
            '.missing-bounds': {
              exists: true,
              connected: true,
              rect: {
                x: 0,
                y: 0,
                width: 0,
                height: 0,
                top: 0,
                right: 0,
                bottom: 0,
                left: 0,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_hover',
            arguments: { selector: '.draft-card' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'browser_hover',
            arguments: { selector: '.missing-bounds' },
          },
        },
      ]
    );

    const hoverResponse = responses.find((message) => message.id === 2);
    expect((hoverResponse?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(hoverResponse)).toContain('status: hovered');
    expect(getResponseText(hoverResponse)).toContain('selector: .draft-card');

    const hoverError = responses.find((message) => message.id === 3);
    expect((hoverError?.result as { isError?: boolean }).isError).toBe(true);
    expect(getResponseText(hoverError)).toContain(
      'element is hidden or not interactable for selector: .missing-bounds'
    );
  });

  it('queries element diagnostics and validates query fields', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Query Page',
          currentUrl: 'https://example.com/',
          query: {
            '.edit-button': {
              exists: true,
              connected: true,
              innerText: 'Edit',
              textContent: 'Edit',
              rect: {
                x: 123,
                y: 45,
                width: 48,
                height: 24,
                top: 45,
                right: 171,
                bottom: 69,
                left: 123,
              },
              display: 'block',
              visibility: 'visible',
              opacity: '1',
            },
            '.missing-button': {
              exists: false,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: { selector: '.edit-button' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: '.edit-button',
              fields: ['display', 'visibility', 'opacity'],
            },
          },
        },
        {
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: { selector: '.missing-button' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: { selector: '.edit-button', fields: 'display' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 6,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: { selector: '.edit-button', fields: ['displayMode'] },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'exists: true'
    );
    expect(getResponseText(responses.find((message) => message.id === 2))).toContain(
      'boundingClientRect: {"x":123'
    );
    expect(getResponseText(responses.find((message) => message.id === 3))).toContain(
      'display: block'
    );
    expect(getResponseText(responses.find((message) => message.id === 3))).not.toContain(
      'innerText:'
    );

    const missingResponse = responses.find((message) => message.id === 4);
    expect((missingResponse?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(missingResponse)).toContain('exists: false');

    expect(getResponseText(responses.find((message) => message.id === 5))).toContain(
      'Browser MCP failed: fields must be an array of strings'
    );
    expect(getResponseText(responses.find((message) => message.id === 6))).toContain(
      'Browser MCP failed: unknown query field: displayMode'
    );
  });

  it('reports count, nth-aware fields, href, and onclick for multi-match selectors', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Query Page',
          currentUrl: 'https://example.com/',
          query: {
            '.hover-action': [
              {
                exists: true,
                connected: true,
                innerText: 'Open',
                textContent: 'Open',
                display: 'block',
                visibility: 'visible',
                opacity: '1',
                href: 'https://example.com/open',
                onclick: 'openCard()',
              },
              {
                exists: true,
                connected: true,
                innerText: 'Archive',
                textContent: 'Archive',
                display: 'block',
                visibility: 'visible',
                opacity: '0.95',
                href: 'https://example.com/archive',
                onclick: 'archiveCard()',
              },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 30,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: '.hover-action',
              nth: 1,
              fields: ['count', 'exists', 'innerText', 'href', 'onclick'],
            },
          },
        },
      ]
    );

    const text = getResponseText(responses.find((message) => message.id === 30));
    expect(text).toContain('count: 2');
    expect(text).toContain('exists: true');
    expect(text).toContain('innerText: Archive');
    expect(text).toContain('href: https://example.com/archive');
    expect(text).toContain('onclick: archiveCard()');
    expect(text).toContain('nth: 1');
  });

  it('keeps count and exists available when nth is out of range, but rejects target fields', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Query Page',
          currentUrl: 'https://example.com/',
          query: {
            '.hover-action': [
              {
                exists: true,
                connected: true,
                innerText: 'Open',
                textContent: 'Open',
                display: 'block',
                visibility: 'visible',
                opacity: '1',
              },
            ],
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 31,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: '.hover-action',
              nth: 3,
              fields: ['count', 'exists'],
            },
          },
        },
        {
          jsonrpc: '2.0',
          id: 32,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: '.hover-action',
              nth: 3,
              fields: ['count', 'innerText'],
            },
          },
        },
      ]
    );

    const countOnly = responses.find((message) => message.id === 31);
    expect((countOnly?.result as { isError?: boolean }).isError).not.toBe(true);
    expect(getResponseText(countOnly)).toContain('count: 1');
    expect(getResponseText(countOnly)).toContain('exists: false');

    expect(getResponseText(responses.find((message) => message.id === 32))).toContain(
      'Browser MCP failed: element index 3 is out of range for selector: .hover-action'
    );
  });

  it('waits for selector visibility with opacity threshold', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Wait Page',
          currentUrl: 'https://example.com/',
          wait: {
            selectorSnapshots: {
              '.hover-action': [
                { exists: false },
                {
                  exists: true,
                  connected: true,
                  display: 'block',
                  visibility: 'visible',
                  opacity: '0.95',
                  rect: {
                    x: 10,
                    y: 10,
                    width: 40,
                    height: 20,
                    top: 10,
                    right: 50,
                    bottom: 30,
                    left: 10,
                  },
                },
              ],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 40,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              selector: '.hover-action',
              timeoutMs: 1000,
              pollIntervalMs: 10,
              condition: { kind: 'visibility', visibility: 'visible', opacityGt: 0.9 },
            },
          },
        },
      ]
    );

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

  it('waits for page text includes and returns timeout summaries', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Wait Timeout',
          currentUrl: 'https://example.com/',
          wait: {
            pageTextSequence: ['loading', 'loading', 'loaded archive menu'],
            selectorSnapshots: {
              '.hover-action': [
                {
                  exists: true,
                  connected: true,
                  display: 'block',
                  visibility: 'visible',
                  opacity: '0.2',
                  rect: {
                    x: 10,
                    y: 10,
                    width: 40,
                    height: 20,
                    top: 10,
                    right: 50,
                    bottom: 30,
                    left: 10,
                  },
                },
              ],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 41,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              timeoutMs: 1000,
              pollIntervalMs: 10,
              condition: { kind: 'text', includes: 'archive menu' },
            },
          },
        },
        {
          jsonrpc: '2.0',
          id: 42,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              selector: '.hover-action',
              timeoutMs: 30,
              pollIntervalMs: 10,
              condition: { kind: 'visibility', visibility: 'visible', opacityGt: 0.9 },
            },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 41))).toContain(
      'status: satisfied'
    );
    expect(getResponseText(responses.find((message) => message.id === 42))).toContain(
      'Browser MCP failed: wait condition timed out'
    );
    expect(getResponseText(responses.find((message) => message.id === 42))).toContain(
      'opacity=0.2'
    );
  });

  it('waits for selector text includes', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Wait Text',
          currentUrl: 'https://example.com/',
          wait: {
            selectorSnapshots: {
              '.hover-action': [
                [
                  {
                    exists: true,
                    connected: true,
                    innerText: 'Open',
                    textContent: 'Open',
                    display: 'block',
                    visibility: 'visible',
                    opacity: '1',
                  },
                ],
                [
                  {
                    exists: true,
                    connected: true,
                    innerText: 'Archive',
                    textContent: 'Archive',
                    display: 'block',
                    visibility: 'visible',
                    opacity: '1',
                  },
                ],
              ],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 45,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              selector: '.hover-action',
              timeoutMs: 1000,
              pollIntervalMs: 10,
              condition: { kind: 'text', includes: 'Archive' },
            },
          },
        },
      ]
    );

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

  it('lets nth become valid later during polling and rejects unsupported page-level wait kinds', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Wait Nth',
          currentUrl: 'https://example.com/',
          wait: {
            selectorSnapshots: {
              '.hover-action': [
                [
                  {
                    exists: true,
                    connected: true,
                    innerText: 'Open',
                    textContent: 'Open',
                    display: 'block',
                    visibility: 'visible',
                    opacity: '1',
                  },
                ],
                [
                  {
                    exists: true,
                    connected: true,
                    innerText: 'Open',
                    textContent: 'Open',
                    display: 'block',
                    visibility: 'visible',
                    opacity: '1',
                  },
                  {
                    exists: true,
                    connected: true,
                    innerText: 'Archive',
                    textContent: 'Archive',
                    display: 'block',
                    visibility: 'visible',
                    opacity: '1',
                    rect: {
                      x: 10,
                      y: 10,
                      width: 40,
                      height: 20,
                      top: 10,
                      right: 50,
                      bottom: 30,
                      left: 10,
                    },
                  },
                ],
              ],
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 43,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              selector: '.hover-action',
              nth: 1,
              timeoutMs: 1000,
              pollIntervalMs: 10,
              condition: { kind: 'visibility', visibility: 'visible' },
            },
          },
        },
        {
          jsonrpc: '2.0',
          id: 44,
          method: 'tools/call',
          params: {
            name: 'browser_wait_for',
            arguments: {
              timeoutMs: 100,
              pollIntervalMs: 10,
              condition: { kind: 'visibility', visibility: 'visible' },
            },
          },
        },
      ]
    );

    expect(getResponseText(responses.find((message) => message.id === 43))).toContain(
      'status: satisfied'
    );
    expect(getResponseText(responses.find((message) => message.id === 44))).toContain(
      'Browser MCP failed: page-level wait only supports text conditions in Phase 1'
    );
  });

  it('defaults browser_eval to readonly and enforces readwrite gating', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Eval Page',
          currentUrl: 'https://example.com/',
          eval: {
            'JSON.stringify({ hovered: true, label: "Archive" })': {
              result: { hovered: true, label: 'Archive' },
            },
            'document.body.dataset.test = "1"': {
              error: 'EvalError: Possible side-effect in debug-evaluate',
            },
            'throw new Error("boom")': {
              error: 'boom',
            },
            Infinity: {
              unserializableValue: 'Infinity',
            },
            window: {
              nonSerializable: true,
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 50,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'JSON.stringify({ hovered: true, label: "Archive" })' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 51,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'document.body.dataset.test = "1"' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 52,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'document.body.dataset.test = "1"', mode: 'readwrite' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 53,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'throw new Error("boom")' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 54,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'Infinity' },
          },
        },
        {
          jsonrpc: '2.0',
          id: 55,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'window' },
          },
        },
      ],
      { childEnv: { ...process.env, CCS_BROWSER_EVAL_MODE: 'readonly' } }
    );

    expect(getResponseText(responses.find((message) => message.id === 50))).toContain(
      'mode: readonly'
    );
    expect(getResponseText(responses.find((message) => message.id === 50))).toContain(
      'value: {"hovered":true,"label":"Archive"}'
    );
    expect(getResponseText(responses.find((message) => message.id === 51))).toContain(
      'Browser MCP failed: EvalError: Possible side-effect in debug-evaluate'
    );
    expect(getResponseText(responses.find((message) => message.id === 52))).toContain(
      'Browser MCP failed: browser_eval readwrite mode is disabled by CCS_BROWSER_EVAL_MODE=readonly'
    );
    expect(getResponseText(responses.find((message) => message.id === 53))).toContain(
      'Browser MCP failed: boom'
    );
    expect(getResponseText(responses.find((message) => message.id === 54))).toContain(
      'value: "Infinity"'
    );
    expect(getResponseText(responses.find((message) => message.id === 55))).toContain(
      'Browser MCP failed: evaluation result is not JSON-serializable'
    );
  });

  it('allows readwrite browser_eval when configured', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Eval Page',
          currentUrl: 'https://example.com/',
          eval: {
            'document.body.dataset.test = "1"': {
              result: 'ok',
            },
          },
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 54,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: 'document.body.dataset.test = "1"', mode: 'readwrite' },
          },
        },
      ],
      { childEnv: { ...process.env, CCS_BROWSER_EVAL_MODE: 'readwrite' } }
    );

    expect(getResponseText(responses.find((message) => message.id === 54))).toContain(
      'mode: readwrite'
    );
    expect(getResponseText(responses.find((message) => message.id === 54))).toContain(
      'value: "ok"'
    );
  });

  it('hides browser_eval when eval mode is disabled', async () => {
    const responses = await runMcpRequests(
      [{ id: 'page-1', title: 'Eval Disabled Page', currentUrl: 'https://example.com/' }],
      [
        {
          jsonrpc: '2.0',
          id: 56,
          method: 'tools/list',
          params: {},
        },
        {
          jsonrpc: '2.0',
          id: 57,
          method: 'tools/call',
          params: {
            name: 'browser_eval',
            arguments: { expression: '1 + 1' },
          },
        },
      ],
      { childEnv: { ...process.env, CCS_BROWSER_EVAL_MODE: 'disabled' } }
    );

    const tools = (responses.find((message) => message.id === 56)?.result as { tools?: Array<{ name?: string }> })
      ?.tools || [];
    expect(tools.some((tool) => tool.name === 'browser_eval')).toBe(false);
    expect((responses.find((message) => message.id === 57)?.error as { message?: string })?.message).toBe(
      'Unknown tool: browser_eval'
    );
  });

  it('queries inside an iframe selected by frameSelector', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Frame Page',
          currentUrl: 'https://example.com/',
          frames: [
            {
              selector: '#details-frame',
              query: {
                '.save-button': {
                  exists: true,
                  connected: true,
                  innerText: 'Save',
                  textContent: 'Save',
                  display: 'block',
                  visibility: 'visible',
                  opacity: '1',
                },
              },
            },
          ],
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 55,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: '.save-button',
              frameSelector: '#details-frame',
              fields: ['exists', 'innerText'],
            },
          },
        },
      ]
    );

    expect(
      getMockFrame(
        {
          frames: [{ selector: '#details-frame' }],
          id: 'x',
          title: 'x',
          currentUrl: 'https://example.com/',
        },
        '#details-frame'
      )
    ).toBeDefined();
    expect(getResponseText(responses.find((message) => message.id === 55))).toContain(
      'innerText: Save'
    );
  });

  it('queries through open shadow roots when pierceShadow is true', async () => {
    const responses = await runMcpRequests(
      [
        {
          id: 'page-1',
          title: 'Shadow Page',
          currentUrl: 'https://example.com/',
          shadowRoots: [
            {
              hostSelector: 'app-toolbar',
              query: {
                'button[action="archive"]': {
                  exists: true,
                  connected: true,
                  innerText: 'Archive',
                  textContent: 'Archive',
                  display: 'block',
                  visibility: 'visible',
                  opacity: '1',
                },
              },
            },
          ],
        },
      ],
      [
        {
          jsonrpc: '2.0',
          id: 56,
          method: 'tools/call',
          params: {
            name: 'browser_query',
            arguments: {
              selector: 'button[action="archive"]',
              pierceShadow: true,
              fields: ['exists', 'innerText'],
            },
          },
        },
      ]
    );

    expect(
      getMockShadowRoot({
        shadowRoots: [{ hostSelector: 'app-toolbar' }],
        id: 'x',
        title: 'x',
        currentUrl: 'https://example.com/',
      })
    ).toBeDefined();
    expect(getResponseText(responses.find((message) => message.id === 56))).toContain(
      'innerText: Archive'
    );
  });

});
