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

import { createMockSessionManager, createMockRefIdManager } from '../utils/mock-session';
import { 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 - DOM Mode', () => {
  let mockSessionManager: ReturnType<typeof createMockSessionManager>;
  let mockRefIdManager: ReturnType<typeof createMockRefIdManager>;
  let testSessionId: string;
  let testTargetId: string;

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

    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;
  };

  // Sample DOM tree matching the structure described in the task
  const sampleDOMTree = {
    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: 'HEAD', localName: 'head',
        attributes: [],
        children: [],
      }, {
        nodeId: 4, backendNodeId: 4, nodeType: 1, nodeName: 'BODY', localName: 'body',
        attributes: [],
        children: [
          {
            nodeId: 5, backendNodeId: 100, nodeType: 1, nodeName: 'H1', localName: 'h1',
            attributes: ['id', 'main-title'],
            children: [{ nodeId: 6, backendNodeId: 6, nodeType: 3, nodeName: '#text', localName: '', nodeValue: 'Hello World' }],
          },
          {
            nodeId: 7, backendNodeId: 101, nodeType: 1, nodeName: 'BUTTON', localName: 'button',
            attributes: ['type', 'submit'],
            children: [{ nodeId: 8, backendNodeId: 8, nodeType: 3, nodeName: '#text', localName: '', nodeValue: 'Click Me' }],
          },
          {
            nodeId: 9, backendNodeId: 102, nodeType: 1, nodeName: 'INPUT', localName: 'input',
            attributes: ['type', 'text', 'placeholder', 'Enter name'],
          },
          {
            nodeId: 10, backendNodeId: 103, nodeType: 1, nodeName: 'A', localName: 'a',
            attributes: ['href', '/about'],
            children: [{ nodeId: 11, backendNodeId: 11, nodeType: 3, nodeName: '#text', localName: '', nodeValue: 'About' }],
          },
        ],
      }],
    }],
  };

  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 responses for AX tree (for backward compat tests)
    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
    mockSessionManager.mockCDPClient.setCDPResponse(
      'DOM.getDocument',
      { depth: -1, pierce: true },
      { root: sampleDOMTree }
    );

    // Set up page.evaluate for page stats (DOM mode)
    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(() => {
    jest.clearAllMocks();
  });

  describe('DOM Mode', () => {
    test('mode=dom returns compact DOM output with backendNodeId identifiers', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;

      expect(result.isError).toBeUndefined();
      const text = result.content[0].text;

      // Should contain page_stats header
      expect(text).toContain('[page_stats]');
      expect(text).toContain('url: https://example.com');

      // Should contain backendNodeId identifiers (NOT ref_N)
      expect(text).toContain('[100]');
      expect(text).toContain('[101]');
      expect(text).toContain('[102]');
      expect(text).not.toContain('ref_');

      // Should contain tag names
      expect(text).toContain('<h1');
      expect(text).toContain('<button');
      expect(text).toContain('<input');

      // Should contain text content
      expect(text).toContain('Hello World');
      expect(text).toContain('Click Me');
    });

    test('diagnostics are omitted by default and exposed only when requested', async () => {
      const handler = await getReadPageHandler();

      const normal = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      expect(normal._diagnostics).toBeUndefined();

      const diagnostic = await handler(testSessionId, { tabId: testTargetId, mode: 'dom', diagnostics: true }) as any;
      expect(diagnostic._diagnostics).toEqual(expect.objectContaining({
        mode: 'dom',
        domGetDocumentMs: expect.any(Number),
        formatMs: expect.any(Number),
        paginationMs: expect.any(Number),
        sanitizeMs: expect.any(Number),
      }));
      expect(diagnostic.content[0].text).toContain('[page_stats]');
    });

    test('mode=dom does NOT clear ref IDs', async () => {
      const handler = await getReadPageHandler();
      await handler(testSessionId, { tabId: testTargetId, mode: 'dom' });

      // clearTargetRefs should NOT be called in DOM mode
      expect(mockRefIdManager.clearTargetRefs).not.toHaveBeenCalled();
    });

    test('mode=dom does NOT generate ref IDs', async () => {
      const handler = await getReadPageHandler();
      await handler(testSessionId, { tabId: testTargetId, mode: 'dom' });

      // generateRef should NOT be called in DOM mode
      expect(mockRefIdManager.generateRef).not.toHaveBeenCalled();
    });

    test('mode=dom filters HEAD section', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      // HEAD should be filtered out by the serializer
      expect(text).not.toContain('<head');
    });

    test('mode=dom passes depth parameter', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom', depth: 2 }) as any;

      expect(result.isError).toBeUndefined();
      // CDP is fetched with unbounded depth even when a maxDepth is set:
      // pierceIframes defaults to true in DOM mode, and the contentDocument
      // descent gap means a bounded fetch can silently drop iframe body
      // content within maxDepth. The serializer enforces maxDepth on the
      // output side instead.
      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'DOM.getDocument',
        { depth: -1, pierce: true }
      );
      // Output should still be valid
      expect(result.content[0].text).toContain('[page_stats]');
    });

    test('mode=dom with filter=interactive shows only interactive elements', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom', filter: 'interactive' }) as any;
      const text = result.content[0].text;

      // Should contain interactive elements
      expect(text).toContain('<button');
      expect(text).toContain('<input');
      expect(text).toContain('<a');

      // Should NOT contain non-interactive elements
      expect(text).not.toContain('<h1');
    });

    test('interactive elements have ★ marker', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      // Interactive elements (button, input, a) should have ★ marker
      expect(text).toContain('★');
      // button line should have ★
      expect(text).toMatch(/<button[^>]*\/>.*★/);
      // input line should have ★
      expect(text).toMatch(/<input[^>]*\/>.*★/);
      // a (link) line should have ★
      expect(text).toMatch(/<a[^>]*\/>.*★/);
    });

    test('non-interactive elements do not have ★ marker', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      // h1 is not interactive — its line should not have ★
      const lines = text.split('\n');
      const h1Line = lines.find((l: string) => l.includes('<h1'));
      expect(h1Line).toBeDefined();
      expect(h1Line).not.toContain('★');
    });

    test('new data-* attributes appear in DOM output', async () => {
      // Set up a DOM tree with data-cy and data-qa attributes
      const domWithDataAttrs = {
        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: 200, nodeType: 1, nodeName: 'BUTTON', localName: 'button',
              attributes: ['data-cy', 'submit-btn', 'data-qa', 'qa-submit', 'data-state', 'active', 'tabindex', '0'],
              children: [],
            }],
          }],
        }],
      };

      mockSessionManager.mockCDPClient.setCDPResponse(
        'DOM.getDocument',
        { depth: -1, pierce: true },
        { root: domWithDataAttrs }
      );

      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      expect(text).toContain('data-cy="submit-btn"');
      expect(text).toContain('data-qa="qa-submit"');
      expect(text).toContain('data-state="active"');
      expect(text).toContain('tabindex="0"');
    });

    test('docSize appears in page_stats', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      // Should contain docSize in page_stats header
      expect(text).toContain('docSize: 1920x3000');
    });

    test('explicit mode=dom returns an error when DOM serialization fails instead of falling back to AX', async () => {
      mockSessionManager.mockCDPClient.send.mockImplementation(async (_page: unknown, method: string) => {
        if (method === 'DOM.getDocument') {
          throw new Error('DOM unavailable');
        }
        if (method === 'Accessibility.getFullAXTree') {
          return sampleAccessibilityTree;
        }
        return {};
      });

      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'dom' }) as any;
      const text = result.content[0].text;

      expect(result.isError).toBe(true);
      expect(text).toContain('Read page DOM serialization error: DOM unavailable');
      expect(text).not.toContain('ref_');
      expect(mockRefIdManager.generateRef).not.toHaveBeenCalled();
      expect(mockSessionManager.mockCDPClient.send).not.toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        expect.anything()
      );
    });
  });

  describe('Backward Compatibility', () => {
    test('default mode (no mode param) returns compact DOM output', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId }) as any;
      const text = result.content[0].text;

      // Default should now be DOM mode
      expect(text).toContain('[page_stats]');
      // DOM mode uses backendNodeId identifiers, not ref_N
      expect(text).not.toContain('ref_');

      // clearTargetRefs should NOT be called in DOM mode
      expect(mockRefIdManager.clearTargetRefs).not.toHaveBeenCalled();
    });

    test('default mode falls back to AX when DOM serialization fails', async () => {
      mockSessionManager.mockCDPClient.send.mockImplementation(async (_page: unknown, method: string) => {
        if (method === 'DOM.getDocument') {
          throw new Error('DOM unavailable');
        }
        if (method === 'Accessibility.getFullAXTree') {
          return sampleAccessibilityTree;
        }
        return {};
      });

      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId }) as any;
      const text = result.content[0].text;

      expect(result.isError).toBeUndefined();
      expect(text).toContain('ref_');
      expect(text).not.toContain('Read page DOM serialization error');
      expect(mockSessionManager.mockCDPClient.send).toHaveBeenCalledWith(
        expect.anything(),
        'Accessibility.getFullAXTree',
        { depth: 8 }
      );
      expect(mockRefIdManager.generateRef).toHaveBeenCalled();
    });

    test('mode=ax explicitly returns AX tree', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: testTargetId, mode: 'ax' }) as any;
      const text = result.content[0].text;

      // Explicit ax mode should work same as default
      expect(text).toContain('ref_');
    });

  });

  describe('Error Handling', () => {
    test('mode=dom handles missing tabId', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { mode: 'dom' }) as any;

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

    test('mode=dom handles invalid tab', async () => {
      const handler = await getReadPageHandler();
      const result = await handler(testSessionId, { tabId: 'invalid-tab', mode: 'dom' }) as any;

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