/// <reference types="jest" />
/**
 * Tests for Read Page Tool
 */

import { createMockSessionManager, createMockRefIdManager } from '../utils/mock-session';
import { cdpFixtures, sampleAccessibilityTree } from '../utils/test-helpers';

jest.mock('../../src/session-manager', () => ({
  getSessionManager: jest.fn(),
}));

jest.mock('../../src/utils/ref-id-manager', () => ({
  getRefIdManager: jest.fn(),
}));

import { getSessionManager } from '../../src/session-manager';
import { getRefIdManager } from '../../src/utils/ref-id-manager';

describe('ReadPageTool', () => {
  let mockSessionManager: ReturnType<typeof createMockSessionManager>;
  let mockRefIdManager: ReturnType<typeof createMockRefIdManager>;
  let testSessionId: string;
  let testTargetId: string;

  const getReadPageHandler = async (serializeDOMMock?: jest.Mock) => {
    jest.resetModules();
    jest.doMock('../../src/session-manager', () => ({
      getSessionManager: () => mockSessionManager,
    }));
    jest.doMock('../../src/utils/ref-id-manager', () => ({
      getRefIdManager: () => mockRefIdManager,
    }));
    if (serializeDOMMock) {
      jest.doMock('../../src/dom', () => ({
        serializeDOM: serializeDOMMock,
      }));
    }

    const { registerReadPageTool } = await import('../../src/tools/read-page');

    const tools: Map<string, { handler: (sessionId: string, args: Record<string, unknown>) => Promise<unknown> }> = new Map();
    const mockServer = {
      registerTool: (name: string, handler: unknown) => {
        tools.set(name, { handler: handler as (sessionId: string, args: Record<string, unknown>) => Promise<unknown> });
      },
    };

    registerReadPageTool(mockServer as unknown as Parameters<typeof registerReadPageTool>[0]);
    return tools.get('read_page')!.handler;
  };

  beforeEach(async () => {
    mockSessionManager = createMockSessionManager();
    mockRefIdManager = createMockRefIdManager();
    (getSessionManager as jest.Mock).mockReturnValue(mockSessionManager);
    (getRefIdManager as jest.Mock).mockReturnValue(mockRefIdManager);

    testSessionId = 'test-session-123';
    const { targetId } = await mockSessionManager.createTarget(testSessionId, 'about:blank');
    testTargetId = targetId;

    // Set up default CDP response for accessibility tree (default depth for 'all' filter)
    mockSessionManager.mockCDPClient.setCDPResponse(
      'Accessibility.getFullAXTree',
      { depth: 8 },
      sampleAccessibilityTree
    );

    // Set up CDP response for depth 5 (used with interactive filter)
    mockSessionManager.mockCDPClient.setCDPResponse(
      'Accessibility.getFullAXTree',
      { depth: 5 },
      sampleAccessibilityTree
    );

    // Set up DOM.getDocument response for DOM mode (now the default)
    mockSessionManager.mockCDPClient.setCDPResponse(
      'DOM.getDocument',
      { depth: -1, pierce: true },
      {
        root: {
          nodeId: 1, backendNodeId: 1, nodeType: 9, nodeName: '#document', localName: '',
          children: [{
            nodeId: 2, backendNodeId: 2, nodeType: 1, nodeName: 'HTML', localName: 'html',
            attributes: [],
            children: [{
              nodeId: 3, backendNodeId: 3, nodeType: 1, nodeName: 'BODY', localName: 'body',
              attributes: [],
              children: [
                {
                  nodeId: 4, backendNodeId: 100, nodeType: 1, nodeName: 'BUTTON', localName: 'button',
                  attributes: ['type', 'submit'],
                  children: [{ nodeId: 5, backendNodeId: 5, nodeType: 3, nodeName: '#text', localName: '', nodeValue: 'Submit' }],
                },
              ],
            }],
          }],
        },
      }
    );

    // Set up page.evaluate for page stats (AX mode now calls evaluate for page metadata)
    const page = mockSessionManager.pages.get(testTargetId);
    if (page) {
      (page.evaluate as jest.Mock).mockResolvedValue({
        url: 'https://example.com',
        title: 'Test Page',
        scrollX: 0,
        scrollY: 0,
        scrollWidth: 1920,
        scrollHeight: 3000,
        viewportWidth: 1920,
        viewportHeight: 1080,
      });
    }
  });

  afterEach(() => {
    delete process.env.OPENCHROME_PROFILE;
    jest.clearAllMocks();
    // Keep delta snapshot cache isolated between read_page tests.
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    require('../../src/compression/snapshot-store').SnapshotStore.getInstance().clear();
  });

  describe('Accessibility Tree', () => {
    test('markdown exposes nextCursor for truncated output without changing text output shape', async () => {
      process.env.OPENCHROME_STATE_HEADER = 'off';
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId)!;
      (page.content as jest.Mock).mockResolvedValue(`<main><p>${'A'.repeat(60000)}</p></main>`);

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        boundaryMarkers: false,
      }) as {
        content: Array<{ type: string; text: string }>;
        structuredContent?: { hasMore: boolean; nextCursor?: string; total: number; offset: number; text: string };
      };

      expect(result.content[0].text).toContain('[Output truncated — exceeded MAX_OUTPUT_CHARS]');
      expect(result.structuredContent?.hasMore).toBe(true);
      expect(result.structuredContent?.offset).toBe(0);
      expect(result.structuredContent?.total).toBeGreaterThan(result.content[0].text.length);
      expect(result.structuredContent?.nextCursor).toEqual(expect.any(String));
      expect(result.structuredContent?.text).toBe(result.content[0].text);
    });

    test('markdown cursor returns subsequent page as structuredContent JSON', async () => {
      process.env.OPENCHROME_STATE_HEADER = 'off';
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId)!;
      (page.content as jest.Mock).mockResolvedValue(`<main><p>${'A'.repeat(60000)}</p></main>`);

      const first = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        boundaryMarkers: false,
      }) as { structuredContent?: { nextCursor?: string } };

      const second = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        boundaryMarkers: false,
        cursor: first.structuredContent?.nextCursor,
      }) as {
        content: Array<{ type: string; text: string }>;
        structuredContent?: { action: string; mode: string; hasMore: boolean; offset: number; text: string };
      };

      expect(JSON.parse(second.content[0].text)).toEqual(second.structuredContent);
      expect(second.structuredContent?.action).toBe('read_page');
      expect(second.structuredContent?.mode).toBe('markdown');
      expect(second.structuredContent?.offset).toBeGreaterThan(0);
      expect(second.structuredContent?.hasMore).toBe(true);
      expect(second.structuredContent?.text).not.toContain('[Output truncated — exceeded MAX_OUTPUT_CHARS]');
    });

    test('markdown cursor rejects malformed and stale cursors', async () => {
      process.env.OPENCHROME_STATE_HEADER = 'off';
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId)!;
      (page.content as jest.Mock).mockResolvedValue(`<main><p>${'A'.repeat(60000)}</p></main>`);

      const malformed = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        cursor: 'not-a-cursor',
      }) as { isError?: boolean; structuredContent?: { error?: { code: string } } };
      expect(malformed.isError).toBe(true);
      expect(malformed.structuredContent?.error?.code).toBe('invalid_cursor');

      const first = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        boundaryMarkers: false,
      }) as { structuredContent?: { nextCursor?: string } };
      (page.content as jest.Mock).mockResolvedValue(`<main><p>${'B'.repeat(60000)}</p></main>`);

      const stale = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
        cursor: first.structuredContent?.nextCursor,
      }) as { isError?: boolean; structuredContent?: { error?: { code: string; retry: string } } };
      expect(stale.isError).toBe(true);
      expect(stale.structuredContent?.error).toEqual({ code: 'stale_cursor', retry: 'restart_from_no_cursor' });
    });

    test('semantic include_metrics reports the final serialized payload size', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'semantic',
        include_metrics: true,
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      const payload = JSON.parse(text) as { _metrics: { returned_chars: number; estimated_tokens: number } };
      expect(payload._metrics.returned_chars).toBe(text.length);
      expect(payload._metrics.estimated_tokens).toBe(Math.ceil(text.length / 4));
    });

    test('returns tree with default depth', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 8 }
      );
    });

    test('respects custom depth limit', async () => {
      const handler = await getReadPageHandler();

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 5 },
        sampleAccessibilityTree
      );

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        depth: 5,
      });

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 5 }
      );
    });

    test('uses capped depth for interactive filter', async () => {
      const handler = await getReadPageHandler();

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 5 },
        sampleAccessibilityTree
      );

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'interactive',
      });

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 5 }
      );
    });

    test('uses custom depth when smaller than cap for interactive filter', async () => {
      const handler = await getReadPageHandler();

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 3 },
        sampleAccessibilityTree
      );

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'interactive',
        depth: 3,
      });

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 3 }
      );
    });

    test('caps custom depth above limit for interactive filter', async () => {
      const handler = await getReadPageHandler();

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'interactive',
        depth: 10,
      });

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 5 }
      );
    });

    test('generates ref IDs for elements', async () => {
      const handler = await getReadPageHandler();

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      });

      // Should have generated refs for elements with backendDOMNodeId
      expect(mockRefIdManager.generateRef).toHaveBeenCalled();
    });

    test('clears previous refs on new read', async () => {
      const handler = await getReadPageHandler();

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      });

      expect(mockRefIdManager.clearTargetRefs).toHaveBeenCalledWith(testSessionId, testTargetId);
    });

    test('handles empty page', async () => {
      const handler = await getReadPageHandler();

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        { nodes: [] }
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      // Should return without error
      expect(result.content[0].type).toBe('text');
    });
  });

  describe('Filtering', () => {
    test('filter=all returns all elements', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'all',
      }) as { content: Array<{ type: string; text: string }> };

      // Should include document role (non-interactive)
      expect(result.content[0].text).toContain('document');
    });

    test('filter=interactive returns only interactive elements', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'interactive',
      }) as { content: Array<{ type: string; text: string }> };

      // Should include button and textbox but not necessarily document
      const text = result.content[0].text;
      // Check that interactive elements are present
      // Note: exact behavior depends on implementation

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 5 }
      );
    });

    test('interactive elements include correct roles', async () => {
      const handler = await getReadPageHandler();

      // The sample tree has button, textbox, link which are all interactive
      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        filter: 'interactive',
      }) as { content: Array<{ type: string; text: string }> };

      // These roles should pass through the interactive filter
      const interactiveRoles = ['button', 'link', 'textbox'];
      // Implementation-specific check

      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 5 }
      );
    });
  });

  describe('Output Formatting', () => {
    test('includes role and name in output', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toContain('button');
      expect(text).toContain('Submit');
    });

    test('includes properties like focused', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      // The sample tree has a focused button
      expect(text).toContain('focused');
    });

    test('includes ref IDs in output', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toMatch(/\[ref_\d+ @e\d+\]/);
    });
  });

  describe('Output Limits', () => {
    test('handles large accessibility trees', async () => {
      const handler = await getReadPageHandler();

      // Create a large tree
      const largeTree = {
        nodes: Array.from({ length: 1000 }, (_, i) => ({
          nodeId: i,
          backendDOMNodeId: 100 + i,
          role: { value: 'generic' },
          name: { value: `Element ${i}` },
        })),
      };

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        largeTree
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      // Should handle without error
      expect(result.content[0].type).toBe('text');
    });

    function generateLargeAXTree(nodeCount: number) {
      const nodes: Array<{
        nodeId: number;
        backendDOMNodeId?: number;
        role: { value: string };
        name: { value: string };
        childIds: number[];
      }> = [{ nodeId: 1, role: { value: 'WebArea' }, name: { value: 'Test' }, childIds: [] }];
      for (let i = 2; i <= nodeCount; i++) {
        nodes[0].childIds.push(i);
        nodes.push({
          nodeId: i,
          backendDOMNodeId: i * 10,
          role: { value: 'button' },
          name: { value: 'Button ' + 'x'.repeat(100) },
          childIds: [],
        });
      }
      return { nodes };
    }

    test('does not fall back to DOM when explicit AX output exceeds limit', async () => {
      const mockSerializeDOM = jest.fn().mockResolvedValue({
        content: '[page_stats] url: https://example.com\n\n<body>\n  <button />\n</body>',
      });
      const handler = await getReadPageHandler(mockSerializeDOM);

      // 600 nodes × ~110 chars each ≈ 66K chars > MAX_OUTPUT (50K)
      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        generateLargeAXTree(600)
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(mockSerializeDOM).not.toHaveBeenCalled();
      expect(text).not.toContain('<body>');
      expect(text).toContain('[Output truncated');
      expect(text).toContain('fallback: "dom"');
    });

    test('explicit fallback=dom switches to DOM when AX tree exceeds output limit', async () => {
      const mockSerializeDOM = jest.fn().mockResolvedValue({
        content: '[page_stats] url: https://example.com\n\n<body></body>',
      });
      const handler = await getReadPageHandler(mockSerializeDOM);

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        generateLargeAXTree(600)
      );

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        fallback: 'dom',
        filter: 'all',
      });

      expect(mockSerializeDOM).toHaveBeenCalledWith(
        expect.anything(),
        expect.anything(),
        expect.objectContaining({
          maxDepth: 8,
          filter: 'all',
          interactiveOnly: false,
        })
      );
    });

    test('diagnostics.mode reflects DOM when AX-overflow triggers fallback=dom', async () => {
      const mockSerializeDOM = jest.fn().mockResolvedValue({
        content: '[page_stats] url: https://example.com\n\n<body></body>',
      });
      const handler = await getReadPageHandler(mockSerializeDOM);

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        generateLargeAXTree(600)
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        fallback: 'dom',
        diagnostics: true,
      }) as any;

      // effective mode must reflect the actual output format, not the requested one
      expect(result._diagnostics.mode).toBe('dom');
      expect(result._diagnostics.requestedMode).toBe('ax');
    });

    test('falls back to truncated AX output when DOM serialization fails', async () => {
      const mockSerializeDOM = jest.fn().mockRejectedValue(new Error('DOM serialization failed'));
      const handler = await getReadPageHandler(mockSerializeDOM);

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        generateLargeAXTree(600)
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        fallback: 'dom',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      // Should fall back to original truncation message
      expect(text).toContain('[Output truncated');
      expect(text).toContain('mode: "dom"');
      expect(text).toContain('smaller depth / ref_id');
      // Should NOT contain the auto-fallback notice
      expect(text).not.toContain('[AX tree exceeded output limit');
    });

    test('detects roots with linear child-id construction on a 5000-node AX tree', async () => {
      const handler = await getReadPageHandler();
      const nodes: any[] = [];
      let childIdsAccesses = 0;
      const rootChildren = Array.from({ length: 4999 }, (_, i) => i + 2);
      nodes.push({
        nodeId: 1,
        role: { value: 'WebArea' },
        name: { value: 'Root' },
        get childIds() {
          childIdsAccesses++;
          return rootChildren;
        },
      });
      for (let i = 2; i <= 5000; i++) {
        nodes.push({
          nodeId: i,
          backendDOMNodeId: i * 10,
          role: { value: 'button' },
          name: { value: `Button ${i}` },
          get childIds() {
            childIdsAccesses++;
            return [];
          },
        });
      }

      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 1 },
        { nodes }
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        depth: 1,
      }) as { content: Array<{ type: string; text: string }> };

      expect(result.content[0].type).toBe('text');
      // O(n) root detection/formatting reads childIds a small number of times;
      // the previous nodes.filter(...nodes.some(...includes)) path reads it
      // roughly n*n times for this tree.
      expect(childIdsAccesses).toBeLessThan(15000);
    });

    test('invalid mode returns clear error', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'html',
      }) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Invalid mode "html"');
      expect(result.content[0].text).toContain('Must be "ax", "dom", "css", "semantic", or "markdown"');
    });
  });

  describe('Markdown Mode', () => {
    test('returns clean markdown and pagination metadata by default', async () => {
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId)!;
      (page.content as jest.Mock).mockResolvedValue(
        '<html><body><nav>Main page</nav><main><h1>Article</h1><p>See <a href="https://example.com">link</a>.</p></main></body></html>'
      );
      (page.evaluate as jest.Mock).mockResolvedValueOnce({
        type: 'numbered',
        hasNext: true,
        hasPrev: false,
        currentPage: 1,
        totalPages: 3,
      });

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toContain('# Article');
      expect(text).toContain('[link](https://example.com)');
      expect(text).not.toContain('Main page');
      expect(text).toContain('[Pagination Detected]');
      expect(text).toContain('Type: numbered');
      expect(text).toContain('Pages: 1 / 3');
    });



    test('markdown contentFilter=prune returns raw and fit markdown with metrics', async () => {
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId);
      (page!.content as jest.Mock).mockResolvedValue(`
        <main>
          <h1>Article</h1>
          <p>Home Login Cookie settings Privacy Policy</p>
          <p>Enterprise pricing includes annual discounts and support.</p>
          <table><tr><th>Plan</th><th>Price</th></tr><tr><td>Enterprise</td><td>Contact us</td></tr></table>
        </main>
      `);

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        contentFilter: 'prune',
        returnRaw: true,
        returnFit: true,
        includePagination: false,
      }) as { content: Array<{ type: string; text: string }> };
      const jsonStart = result.content[0].text.indexOf('{');
      const body = JSON.parse(result.content[0].text.slice(jsonStart));

      expect(body.content).toContain('Enterprise pricing');
      expect(body.raw_markdown).toContain('Cookie settings');
      expect(body.fit_markdown).toContain('Enterprise pricing');
      expect(body.fit_markdown).not.toContain('Cookie settings');
      expect(body.filter.type).toBe('prune');
      expect(body.filter.raw_chars).toBeGreaterThan(body.filter.fit_chars);
    });

    test('markdown contentFilter=bm25 requires query', async () => {
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId);
      (page!.content as jest.Mock).mockResolvedValue('<main><h1>Article</h1></main>');

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        contentFilter: 'bm25',
        includePagination: false,
      }) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('requires a non-empty query');
    });

    test('can suppress markdown pagination metadata', async () => {
      const handler = await getReadPageHandler();
      const page = mockSessionManager.pages.get(testTargetId)!;
      (page.content as jest.Mock).mockResolvedValue('<main><h1>Article</h1></main>');

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'markdown',
        includePagination: false,
      }) as { content: Array<{ type: string; text: string }> };

      expect(result.content[0].text).toContain('# Article');
      expect(result.content[0].text).not.toContain('[Pagination Detected]');
      expect(page.evaluate).not.toHaveBeenCalled();
    });
  });

  describe('RefIdManager Integration', () => {
    test('generated refs are associated with correct session and target', async () => {
      const handler = await getReadPageHandler();

      await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      });

      // Check that refs were generated with correct session and target
      // generateRef is called with (sessionId, targetId, backendDOMNodeId, role, name, tagName)
      // tagName may be string or undefined depending on the AX role mapping
      const calls = mockRefIdManager.generateRef.mock.calls;
      const matchingCall = calls.find(
        (c: unknown[]) => c[0] === testSessionId && c[1] === testTargetId
      );
      expect(matchingCall).toBeDefined();
      expect(typeof matchingCall![2]).toBe('number');
      expect(typeof matchingCall![3]).toBe('string');
    });



    test('OPENCHROME_PROFILE=fast defaults AX reads to compact output and explicit compact=false disables it', async () => {
      process.env.OPENCHROME_PROFILE = 'fast';
      const handler = await getReadPageHandler();
      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        {
          nodes: [
            { nodeId: 1, backendDOMNodeId: 100, role: { value: 'document' }, name: { value: 'Fast Page' }, childIds: [2, 3] },
            { nodeId: 2, role: { value: 'StaticText' }, name: { value: 'Decorative copy' } },
            { nodeId: 3, backendDOMNodeId: 101, role: { value: 'button' }, name: { value: 'Submit' } },
          ],
        }
      );

      const fast = await handler(testSessionId, { tabId: testTargetId, mode: 'ax' }) as { content: Array<{ type: string; text: string }> };
      expect(fast.content[0].text).toContain('button: "Submit"');
      expect(fast.content[0].text).not.toContain('Decorative copy');

      const explicitFull = await handler(testSessionId, { tabId: testTargetId, mode: 'ax', compact: false }) as { content: Array<{ type: string; text: string }> };
      expect(explicitFull.content[0].text).toContain('Decorative copy');
    });

    test('compact AX mode omits non-actionable no-ref leaves while preserving actionable nodes', async () => {
      const handler = await getReadPageHandler();
      mockSessionManager.mockCDPClient.setCDPResponse(
        'Accessibility.getFullAXTree',
        { depth: 8 },
        {
          nodes: [
            { nodeId: 1, backendDOMNodeId: 100, role: { value: 'document' }, name: { value: 'Compact Page' }, childIds: [2, 3] },
            { nodeId: 2, role: { value: 'StaticText' }, name: { value: 'Decorative copy' } },
            { nodeId: 3, backendDOMNodeId: 101, role: { value: 'button' }, name: { value: 'Submit' } },
          ],
        }
      );

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        compact: true,
      }) as { content: Array<{ type: string; text: string }> };

      expect(result.content[0].text).toContain('button: "Submit"');
      expect(result.content[0].text).not.toContain('Decorative copy');
    });

    test('AX delta compression returns only changes after the first cached snapshot', async () => {
      const handler = await getReadPageHandler();
      const firstTree = {
        nodes: [
          { nodeId: 1, backendDOMNodeId: 100, role: { value: 'document' }, name: { value: 'Delta Page' }, childIds: [2] },
          { nodeId: 2, backendDOMNodeId: 101, role: { value: 'button' }, name: { value: 'Submit' } },
        ],
      };
      const secondTree = {
        nodes: [
          { nodeId: 1, backendDOMNodeId: 100, role: { value: 'document' }, name: { value: 'Delta Page' }, childIds: [2, 3] },
          { nodeId: 2, backendDOMNodeId: 101, role: { value: 'button' }, name: { value: 'Submit' } },
          { nodeId: 3, backendDOMNodeId: 102, role: { value: 'link' }, name: { value: 'Learn more' } },
        ],
      };
      mockSessionManager.mockCDPClient.setCDPResponse('Accessibility.getFullAXTree', { depth: 8 }, firstTree);

      const first = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        compression: 'delta',
      }) as { content: Array<{ type: string; text: string }> };
      expect(first.content[0].text).toContain('button: "Submit"');
      expect(first.content[0].text).not.toContain('[AX Delta');

      mockSessionManager.mockCDPClient.setCDPResponse('Accessibility.getFullAXTree', { depth: 8 }, secondTree);
      const second = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
        compression: 'delta',
      }) as { content: Array<{ type: string; text: string }> };

      expect(second.content[0].text).toContain('[AX Delta');
      expect(second.content[0].text).toContain('Learn more');
    });

  });

  describe('Error Handling', () => {
    test('returns error for missing tabId', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {}) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('tabId is required');
    });

    test('returns error when tab not found', async () => {
      const handler = await getReadPageHandler();
      mockSessionManager.getPage.mockResolvedValueOnce(null);

      const result = await handler(testSessionId, {
        tabId: 'non-existent-tab',
      }) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('not found');
    });

    test('handles CDP errors', async () => {
      const handler = await getReadPageHandler();
      mockSessionManager.mockCDPClient.send.mockRejectedValueOnce(new Error('CDP error'));

      // Use AX mode to exercise the top-level CDP error path. DOM mode now
      // fails closed with its own dedicated message ("Read page DOM
      // serialization error: …") covered by other tests, so steering this
      // case through AX keeps the original assertion meaningful.
      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Read page error');
    });
  });

  describe('AX Mode Page Stats', () => {
    test('AX mode output starts with [page_stats] line', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toMatch(/(?:^|\n)\[page_stats\]/);
    });

    test('AX mode page_stats includes url and title', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toContain('url: https://example.com');
      expect(text).toContain('title: Test Page');
    });

    test('AX mode page_stats includes docSize', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toContain('docSize: 1920x3000');
    });

    test('AX mode page_stats includes scroll and viewport', async () => {
      const handler = await getReadPageHandler();

      const result = await handler(testSessionId, {
        tabId: testTargetId,
        mode: 'ax',
      }) as { content: Array<{ type: string; text: string }> };

      const text = result.content[0].text;
      expect(text).toContain('scroll: 0,0');
      expect(text).toContain('viewport: 1920x1080');
    });
  });

  describe('Session Isolation', () => {
    test('rejects read from tab of another session', async () => {
      const handler = await getReadPageHandler();

      const session2Id = 'other-session';
      await mockSessionManager.createSession({ id: session2Id });
      const { targetId: session2TargetId } = await mockSessionManager.createTarget(session2Id);

      const result = await handler(testSessionId, {
        tabId: session2TargetId,
      }) as { content: Array<{ type: string; text: string }>; isError?: boolean };

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('does not belong to session');
    });
  });
});
