/**
 * Unit tests for stealth / anti-detection fixes — PUPPETEER EDITION.
 *
 * Covers:
 *   - StealthEval — CDP isolated-world lifecycle (evaluate, invalidate, retry)
 *   - isInputElement / isSelectorFocused — stealth DOM queries with fallback
 *   - typeShiftSymbol — CDP Input.dispatchKeyEvent path vs evaluate fallback
 *   - humanType integration — shift symbols routed via CDP
 *   - Navigation invalidation (goto → stealth.invalidate)
 *   - patchPage stealth infrastructure wiring
 *   - SHIFT_SYMBOL_CODES / SHIFT_SYMBOL_KEYCODES completeness
 *   - focus() — human-like click instead of programmatic CDP focus
 *   - uncheck() — fallback behavior matches Playwright (assume checked on error)
 *   - mouse.wheel() — smooth scroll via smoothWheel
 *   - mouse.dragAndDrop() — Bézier drag between coordinates
 *   - ElementHandle patching — click, hover, type, press, tap, focus, select
 *   - Frame patching — delegates to page-level humanized methods
 *   - Browser-level patching — newPage, createBrowserContext, targetcreated
 *
 * All tests are fast, mock-based, and do NOT require a browser.
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { resolveConfig, rand, randRange, sleep } from "../src/human/config.js";
import { humanType } from "../src/human-puppeteer/keyboard.js";
import { humanMove, humanClick, clickTarget, humanIdle } from "../src/human/mouse.js";

// =========================================================================
// Helper: build mock page / raw objects (Puppeteer-style)
// =========================================================================

function buildMockPage(overrides: Record<string, any> = {}): any {
  const mainFrameObj = overrides.mainFrameReturn ?? {
    childFrames: vi.fn(() => []),
    click: vi.fn(async () => {}),
    hover: vi.fn(async () => {}),
    type: vi.fn(async () => {}),
    fill: vi.fn(async () => {}),
    check: vi.fn(async () => {}),
    uncheck: vi.fn(async () => {}),
    select: vi.fn(async () => []),
    press: vi.fn(async () => {}),
    clear: vi.fn(async () => {}),
    focus: vi.fn(async () => {}),
    tap: vi.fn(async () => {}),
    dragAndDrop: vi.fn(async () => {}),
    $: vi.fn(async () => null),

    $$: vi.fn(async () => []),
    waitForSelector: vi.fn(async () => null),
  };

  const page: any = {
    evaluate: overrides.evaluate ?? vi.fn(async () => false),
    mouse: {
      move: vi.fn(async () => {}),
      down: vi.fn(async () => {}),
      up: vi.fn(async () => {}),
      click: vi.fn(async () => {}),
      wheel: vi.fn(async () => {}),
      dragAndDrop: overrides.mouseDragAndDrop ?? vi.fn(async () => {}),
    },
    keyboard: {
      press: overrides.keyboardPress
        ? vi.fn(overrides.keyboardPress)
        : vi.fn(async () => {}),
      type: vi.fn(async () => {}),
      down: vi.fn(async () => {}),
      up: vi.fn(async () => {}),
      sendCharacter: vi.fn(async () => {}),
    },
    click: vi.fn(async () => {}),
    hover: vi.fn(async () => {}),
    type: vi.fn(async () => {}),
    fill: vi.fn(async () => {}),
    check: vi.fn(async () => {}),
    uncheck: vi.fn(async () => {}),
    select: vi.fn(async () => []),
    press: vi.fn(async () => {}),
    focus: vi.fn(async () => {}),
    goto: vi.fn(async () => ({})),
    tap: vi.fn(async () => {}),
    clear: vi.fn(async () => {}),
    $: overrides.$ ?? vi.fn(async () => null),

    $$: overrides.$$ ?? vi.fn(async () => []),
    waitForSelector: overrides.waitForSelector ?? vi.fn(async () => null),
    // Puppeteer-specific: viewport() returns object (not viewportSize())
    viewport: vi.fn(() => ({ width: 1280, height: 720 })),
    mainFrame: vi.fn(() => mainFrameObj),
    frames: vi.fn(() => []),
    // Puppeteer-specific: createCDPSession on page, not context
    createCDPSession: vi.fn(async () => buildMockCDP()),
    url: vi.fn(() => "about:blank"),
  };
  return page;
}

function buildMockCDP(overrides: Record<string, any> = {}): any {
  return {
    send: overrides.send ?? vi.fn(async (method: string, params?: any) => {
      if (method === "Page.getFrameTree") {
        return { frameTree: { frame: { id: "F1" } } };
      }
      if (method === "Page.createIsolatedWorld") {
        return { executionContextId: 42 };
      }
      if (method === "Runtime.evaluate") {
        return { result: { value: false } };
      }
      return {};
    }),
  };
}

function buildRawKeyboard() {
  const downKeys: string[] = [];
  const upKeys: string[] = [];
  const insertedChars: string[] = [];
  const raw = {
    down: vi.fn(async (k: string) => { downKeys.push(k); }),
    up: vi.fn(async (k: string) => { upKeys.push(k); }),
    type: vi.fn(async () => {}),
    insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
  };
  return { raw, downKeys, upKeys, insertedChars };
}

function buildMockElementHandle(overrides: Record<string, any> = {}): any {
  return {
    click: vi.fn(async () => {}),
    hover: vi.fn(async () => {}),
    type: vi.fn(async () => {}),
    press: vi.fn(async () => {}),
    tap: vi.fn(async () => {}),
    focus: vi.fn(async () => {}),
    select: vi.fn(async () => []),
    drop: vi.fn(async () => {}),
    dragAndDrop: vi.fn(async () => {}),
    boundingBox: overrides.boundingBox ?? vi.fn(async () => ({ x: 100, y: 200, width: 120, height: 30 })),
    evaluate: overrides.evaluate ?? vi.fn(async () => false),
    $: vi.fn(async () => null),

    $$: vi.fn(async () => []),
    waitForSelector: vi.fn(async () => null),
  };
}

function buildMockBrowser(pages: any[] = []): any {
  const browser: any = {
    pages: vi.fn(async () => pages),
    newPage: vi.fn(async () => buildMockPage()),
    createBrowserContext: vi.fn(async () => ({
      newPage: vi.fn(async () => buildMockPage()),
    })),
    on: vi.fn(),
    close: vi.fn(async () => {}),
  };
  return browser;
}


// =========================================================================
// SHIFT_SYMBOL_CODES / SHIFT_SYMBOL_KEYCODES completeness
// =========================================================================
describe("Puppeteer: SHIFT_SYMBOL maps completeness", () => {
  it("every shift symbol has a code and keycode entry", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const SHIFT_SYMBOLS = ['@', '#', '!', '$', '%', '^', '&', '*', '(', ')',
      '_', '+', '{', '}', '|', ':', '"', '<', '>', '?', '~'];

    for (const sym of SHIFT_SYMBOLS) {
      const { raw } = buildRawKeyboard();
      const page = buildMockPage();
      const mockCdp = {
        send: vi.fn(async () => ({})),
      };

      await humanType(page, raw, sym, cfg, mockCdp as any);

      const cdpCalls = mockCdp.send.mock.calls;
      const keyEvents = cdpCalls.filter(
        (c: any[]) => c[0] === "Input.dispatchKeyEvent"
      );
      expect(keyEvents.length).toBe(2);
      expect(page.evaluate).not.toHaveBeenCalled();
    }
  });

  it("all shift symbol keyDown events have correct structure", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "!", cfg, mockCdp as any);

    const keyDown = cdpCalls.find(
      ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyDown"
    );
    expect(keyDown).toBeDefined();
    const params = keyDown![1];

    expect(params.key).toBe("!");
    expect(params.modifiers).toBe(8);
    expect(typeof params.code).toBe("string");
    expect(params.code.length).toBeGreaterThan(0);
    expect(typeof params.windowsVirtualKeyCode).toBe("number");
    expect(params.windowsVirtualKeyCode).toBeGreaterThan(0);
    expect(params.text).toBe("!");
    expect(params.unmodifiedText).toBe("!");
  });

  it("keyUp event has no text/unmodifiedText fields", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "!", cfg, mockCdp as any);

    const keyUp = cdpCalls.find(
      ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyUp"
    );
    expect(keyUp).toBeDefined();
    const params = keyUp![1];

    expect(params.text).toBeUndefined();
    expect(params.unmodifiedText).toBeUndefined();
  });

  it("digit shift symbols have correct keycodes (49-57, 48)", async () => {
    const digitSymbols = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'];
    const expectedKeycodes = [49, 50, 51, 52, 53, 54, 55, 56, 57, 48];
    const cfg = resolveConfig("default", { mistype_chance: 0 });

    for (let i = 0; i < digitSymbols.length; i++) {
      const { raw } = buildRawKeyboard();
      const page = buildMockPage();
      const cdpCalls: Array<[string, any]> = [];
      const mockCdp = {
        send: vi.fn(async (method: string, params: any) => {
          cdpCalls.push([method, params]);
          return {};
        }),
      };

      await humanType(page, raw, digitSymbols[i], cfg, mockCdp as any);

      const keyDown = cdpCalls.find(
        ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyDown"
      );
      expect(keyDown).toBeDefined();
      expect(keyDown![1].windowsVirtualKeyCode).toBe(expectedKeycodes[i]);
    }
  });
});


// =========================================================================
// typeShiftSymbol — CDP path vs fallback (Puppeteer keyboard.ts)
// =========================================================================
describe("Puppeteer: typeShiftSymbol CDP vs fallback", () => {
  it("uses CDP path when cdpSession is provided (no page.evaluate)", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const mockCdp = { send: vi.fn(async () => ({})) };

    await humanType(page, raw, "@", cfg, mockCdp as any);

    expect(page.evaluate).not.toHaveBeenCalled();
    expect(mockCdp.send).toHaveBeenCalled();
  });

  it("CDP path does NOT call raw.insertText for shift symbols", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw, insertedChars } = buildRawKeyboard();
    const page = buildMockPage();
    const mockCdp = { send: vi.fn(async () => ({})) };

    await humanType(page, raw, "#", cfg, mockCdp as any);

    expect(insertedChars.length).toBe(0);
  });

  it("falls back to page.evaluate when no cdpSession", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw, insertedChars } = buildRawKeyboard();
    const page = buildMockPage();

    await humanType(page, raw, "$", cfg, null);

    expect(page.evaluate).toHaveBeenCalled();
    expect(insertedChars).toContain("$");
  });

  it("fallback path calls raw.insertText before page.evaluate", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const callOrder: string[] = [];
    const raw = {
      down: vi.fn(async () => { callOrder.push("raw.down"); }),
      up: vi.fn(async () => { callOrder.push("raw.up"); }),
      type: vi.fn(async () => {}),
      insertText: vi.fn(async () => { callOrder.push("raw.insertText"); }),
    };
    const page = buildMockPage({
      evaluate: vi.fn(async () => { callOrder.push("page.evaluate"); }),
    });

    await humanType(page, raw, "%", cfg, null);

    const insertIdx = callOrder.indexOf("raw.insertText");
    const evalIdx = callOrder.indexOf("page.evaluate");
    expect(insertIdx).toBeLessThan(evalIdx);
  });

  it("Shift is held during CDP key events", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const callOrder: string[] = [];

    const raw = {
      down: vi.fn(async (k: string) => { callOrder.push(`raw.down(${k})`); }),
      up: vi.fn(async (k: string) => { callOrder.push(`raw.up(${k})`); }),
      type: vi.fn(async () => {}),
      insertText: vi.fn(async () => {}),
    };
    const page = buildMockPage();
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        callOrder.push(`cdp.${params.type || method}`);
        return {};
      }),
    };

    await humanType(page, raw, "!", cfg, mockCdp as any);

    const shiftDownIdx = callOrder.indexOf("raw.down(Shift)");
    const keyDownIdx = callOrder.indexOf("cdp.keyDown");
    const keyUpIdx = callOrder.indexOf("cdp.keyUp");
    const shiftUpIdx = callOrder.indexOf("raw.up(Shift)");

    expect(shiftDownIdx).toBeLessThan(keyDownIdx);
    expect(keyDownIdx).toBeLessThan(keyUpIdx);
    expect(keyUpIdx).toBeLessThan(shiftUpIdx);
  });
});


// =========================================================================
// humanType integration — mixed text with CDP (Puppeteer keyboard.ts)
// =========================================================================
describe("Puppeteer: humanType mixed text with CDP", () => {
  it("normal chars use raw.down/up, shift symbols use CDP", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw, downKeys } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "a!", cfg, mockCdp as any);

    expect(downKeys).toContain("a");

    const keyEvents = cdpCalls.filter(
      ([m]) => m === "Input.dispatchKeyEvent"
    );
    expect(keyEvents.length).toBe(2);
    expect(page.evaluate).not.toHaveBeenCalled();
  });

  it("text without shift symbols does not call CDP", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const mockCdp = { send: vi.fn(async () => ({})) };

    await humanType(page, raw, "hello", cfg, mockCdp as any);

    expect(mockCdp.send).not.toHaveBeenCalled();
    expect(page.evaluate).not.toHaveBeenCalled();
  });

  it("multiple shift symbols all go through CDP", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "!@#", cfg, mockCdp as any);

    expect(page.evaluate).not.toHaveBeenCalled();
    const keyEvents = cdpCalls.filter(
      ([m]) => m === "Input.dispatchKeyEvent"
    );
    expect(keyEvents.length).toBe(6);
  });

  it("'Hello World!' — no page.evaluate leak", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const mockCdp = { send: vi.fn(async () => ({})) };

    await humanType(page, raw, "Hello World!", cfg, mockCdp as any);

    expect(page.evaluate).not.toHaveBeenCalled();
  });

  it("password-like text 'SecurePass!123' uses CDP for '!'", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "SecurePass!123", cfg, mockCdp as any);

    const keyEvents = cdpCalls.filter(
      ([m]) => m === "Input.dispatchKeyEvent"
    );
    expect(keyEvents.length).toBe(2);
    expect(keyEvents[0][1].key).toBe("!");
    expect(page.evaluate).not.toHaveBeenCalled();
  });

  it("CDP modifier flag is always 8 (Shift)", async () => {
    const cfg = resolveConfig("default", {
      mistype_chance: 0,
      typing_delay: 0,
      shift_down_delay: [0, 0],
      shift_up_delay: [0, 0],
      key_hold: [0, 0],
    });

    const allSymbols = '@#!$%^&*()_+{}|:"<>?~';
    const { raw } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, allSymbols, cfg, mockCdp as any);

    for (const [method, params] of cdpCalls) {
      if (method === "Input.dispatchKeyEvent") {
        expect(params.modifiers).toBe(8);
      }
    }
  }, 30000);
});


// =========================================================================
// Non-ASCII text does NOT go through CDP shift path
// =========================================================================
describe("Puppeteer: non-ASCII text avoids CDP shift path", () => {
  it("Cyrillic text uses insertText, not CDP", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw, insertedChars } = buildRawKeyboard();
    const page = buildMockPage();
    const mockCdp = { send: vi.fn(async () => ({})) };

    await humanType(page, raw, "Привет", cfg, mockCdp as any);

    expect(insertedChars.join("")).toBe("Привет");
    expect(mockCdp.send).not.toHaveBeenCalled();
    expect(page.evaluate).not.toHaveBeenCalled();
  });

  it("mixed text: ASCII + Cyrillic + shift symbol", async () => {
    const cfg = resolveConfig("default", { mistype_chance: 0 });
    const { raw, downKeys, insertedChars } = buildRawKeyboard();
    const page = buildMockPage();
    const cdpCalls: Array<[string, any]> = [];
    const mockCdp = {
      send: vi.fn(async (method: string, params: any) => {
        cdpCalls.push([method, params]);
        return {};
      }),
    };

    await humanType(page, raw, "Hi! Мир", cfg, mockCdp as any);

    expect(downKeys).toContain("Shift");
    expect(downKeys).toContain("H");
    expect(downKeys).toContain("i");

    const keyEvents = cdpCalls.filter(([m]) => m === "Input.dispatchKeyEvent");
    expect(keyEvents.length).toBe(2);

    expect(downKeys).toContain(" ");

    expect(insertedChars).toContain("М");
    expect(insertedChars).toContain("и");
    expect(insertedChars).toContain("р");

    expect(page.evaluate).not.toHaveBeenCalled();
  });
});


// =========================================================================
// Per-call human config override (Puppeteer page-level)
// =========================================================================
describe("Puppeteer: page.type accepts per-call human config override", () => {
  it("page.type forwards merged config to humanType", async () => {
    const keyboardMod = await import("../src/human-puppeteer/keyboard.js");
    const scrollMod = await import("../src/human-puppeteer/scroll.js");

    const cfg = resolveConfig("default", {
      idle_between_actions: false,
      field_switch_delay: [0, 1],
    });
    expect(cfg.typing_delay).toBe(70);

    let captured: any = null;
    const typeSpy = vi.spyOn(keyboardMod, "humanType").mockImplementation(
      async (_page, _raw, _text, callCfg) => { captured = callCfg; },
    );
    const scrollSpy = vi.spyOn(scrollMod, "scrollToElement").mockImplementation(
      async (_page, _raw, _sel, cx, cy) => ({
        box: { x: 100, y: 100, width: 50, height: 30 },
        cursorX: cx,
        cursorY: cy,
      }),
    );

    const { patchPage } = await import("../src/human-puppeteer/index.js");
    const page = buildMockPage();
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await (page as any).type("#email", "hi", {
      typing_delay: 30,
      mistype_chance: 0,
    });

    expect(captured.typing_delay).toBe(30);
    expect(captured.mistype_chance).toBe(0);
    expect(cfg.typing_delay).toBe(70);

    typeSpy.mockRestore();
    scrollSpy.mockRestore();
  });
});


// =========================================================================
// patchPage stealth infrastructure (Puppeteer)
// =========================================================================
describe("Puppeteer: patchPage stealth infrastructure", () => {
  it("page._stealth is a StealthEval instance after patching", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect((page as any)._stealth).toBeDefined();
    expect(typeof (page as any)._stealth.evaluate).toBe("function");
    expect(typeof (page as any)._stealth.invalidate).toBe("function");
    expect(typeof (page as any)._stealth.getCdpSession).toBe("function");
  });

  it("page._original and page._humanCfg are set", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect((page as any)._original).toBeDefined();
    expect((page as any)._humanCfg).toBe(cfg);
  });

  it("goto invalidates stealth context", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    const invalidateSpy = vi.spyOn(stealth, "invalidate");

    await page.goto("https://example.com");

    expect(invalidateSpy).toHaveBeenCalled();
  });

  it("internal helpers are stored on page for element/frame patching", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    // Only the helpers actually used by ElementHandle/Frame patching
    expect(typeof (page as any)._ensureCursorInit).toBe("function");
    expect((page as any)._humanCursor).toBeDefined();
    expect((page as any)._humanRaw).toBeDefined();
    expect((page as any)._humanRawKb).toBeDefined();
  });

  it("page.createCDPSession is used (not context.newCDPSession)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    await stealth.getCdpSession();

    expect(page.createCDPSession).toHaveBeenCalled();
  });
});


// =========================================================================
// StealthEval lifecycle (Puppeteer — via page.createCDPSession)
// =========================================================================
describe("Puppeteer: StealthEval lifecycle", () => {
  it("stealth.invalidate() is callable without error", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    expect(() => stealth.invalidate()).not.toThrow();
  });

  it("stealth.getCdpSession() returns a CDP session", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    const session = await stealth.getCdpSession();
    expect(session).toBeDefined();
    expect(typeof session.send).toBe("function");
  });

  it("stealth.evaluate() creates world and returns value", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string, params?: any) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 42 };
        }
        if (method === "Runtime.evaluate") {
          return { result: { value: true } };
        }
        return {};
      }),
    });

    const page = buildMockPage();
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    const result = await stealth.evaluate("1 + 1");
    expect(result).toBe(true);
  });

  it("stealth.evaluate() retries on exceptionDetails", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");
    let attempt = 0;

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string, params?: any) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 50 + attempt };
        }
        if (method === "Runtime.evaluate") {
          attempt++;
          if (attempt === 1) {
            return { exceptionDetails: { text: "stale" } };
          }
          return { result: { value: "recovered" } };
        }
        return {};
      }),
    });

    const page = buildMockPage();
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    const result = await stealth.evaluate("test");
    expect(result).toBe("recovered");
  });

  it("stealth.evaluate() returns undefined after double failure", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 70 };
        }
        if (method === "Runtime.evaluate") {
          return { exceptionDetails: { text: "always broken" } };
        }
        return {};
      }),
    });

    const page = buildMockPage();
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const stealth = (page as any)._stealth;
    const result = await stealth.evaluate("broken");
    expect(result).toBeUndefined();
  });
});


// =========================================================================
// focus() — human-like click instead of programmatic focus
// =========================================================================
describe("Puppeteer: focus() humanization", () => {
  it("page.focus is replaced with humanized version", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalFocus = page.focus;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.focus).not.toBe(originalFocus);
    expect(typeof page.focus).toBe("function");
  });

  it("focus() calls click when element is not focused", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string, params?: any) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 42 };
        }
        if (method === "Runtime.evaluate") {
          // Return false = element is NOT focused → should click
          return { result: { value: false } };
        }
        return {};
      }),
    });

    const page = buildMockPage();
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default");
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    // page.focus is patched — it delegates to click internally
    expect(typeof page.focus).toBe("function");
    // Verify it's not the original
    expect(page.focus).not.toBe((page as any)._original.focus);
  });

  it("focus is patched on page for frame delegation", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    const originalFocus = page.focus;
    patchPage(page as any, cfg, cursor as any);

    // focus is patched directly on page — frames delegate to page.focus
    expect(page.focus).not.toBe(originalFocus);
    expect(typeof page.focus).toBe("function");
  });
});


// =========================================================================
// uncheck() — fallback behavior (assume checked on error)
// =========================================================================
describe("Puppeteer: select() humanization", () => {
  it("page.select is replaced with humanized version", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalSelect = page.select;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.select).not.toBe(originalSelect);
    expect(typeof page.select).toBe("function");
  });

  it("select() hovers before delegating to original", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    // Verify original select is stored
    const originals = (page as any)._original;
    expect(typeof originals.select).toBe("function");
  });
});


// =========================================================================
// mouse.wheel() — smooth scroll
// =========================================================================
describe("Puppeteer: mouse.wheel() smooth scroll", () => {
  it("mouse.wheel is patched after patchPage", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalWheel = page.mouse.wheel;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.mouse.wheel).not.toBe(originalWheel);
  });

  it("mouse.wheel({deltaY: 300}) calls original wheel multiple times (smooth)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const wheelCalls: any[] = [];
    const page = buildMockPage();
    const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); });
    page.mouse.wheel = origWheel;

    const cfg = resolveConfig("default");
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.wheel({ deltaY: 300 });

    // smoothWheel breaks 300px into multiple small chunks (20-40px each)
    // So original wheel should be called many times, not just once
    expect(wheelCalls.length).toBeGreaterThan(1);
  });

  it("mouse.wheel({deltaX: 200}) smooths horizontal scroll too", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const wheelCalls: any[] = [];
    const page = buildMockPage();
    const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); });
    page.mouse.wheel = origWheel;

    const cfg = resolveConfig("default");
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.wheel({ deltaX: 200 });

    expect(wheelCalls.length).toBeGreaterThan(1);
  });

  it("mouse.wheel with no args does nothing", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const wheelCalls: any[] = [];
    const page = buildMockPage();
    const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); });
    page.mouse.wheel = origWheel;

    const cfg = resolveConfig("default");
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.wheel({});

    expect(wheelCalls.length).toBe(0);
  });
});


// =========================================================================
// mouse.dragAndDrop() — Bézier drag between coordinates
// =========================================================================
describe("Puppeteer: mouse.dragAndDrop() humanization", () => {
  it("mouse.dragAndDrop is patched after patchPage", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalDnD = page.mouse.dragAndDrop;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.mouse.dragAndDrop).not.toBe(originalDnD);
  });

  it("mouse.dragAndDrop calls mouseMove multiple times (Bézier), then mouseDown/mouseUp", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const downCalls: any[] = [];
    const upCalls: any[] = [];

    const page = buildMockPage();
    const origMove = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });
    const origDown = vi.fn(async () => { downCalls.push(true); });
    const origUp = vi.fn(async () => { upCalls.push(true); });
    page.mouse.move = origMove;
    page.mouse.down = origDown;
    page.mouse.up = origUp;

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.dragAndDrop(
      { x: 100, y: 100 },
      { x: 400, y: 400 },
    );

    // Bézier movement generates many intermediate points
    expect(moveCalls.length).toBeGreaterThan(10);
    // mouseDown and mouseUp should each be called once
    expect(downCalls.length).toBe(1);
    expect(upCalls.length).toBe(1);
  });
});


// =========================================================================
// Keyboard patches — type, press, down, up
// =========================================================================
describe("Puppeteer: keyboard patches", () => {
  it("keyboard.type is patched to use humanType", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalType = page.keyboard.type;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.keyboard.type).not.toBe(originalType);
  });

  it("keyboard.press is patched with delay", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originalPress = page.keyboard.press;
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.keyboard.press).not.toBe(originalPress);
  });

  it("keyboard.down is patched with small delay", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const downCalls: string[] = [];
    const page = buildMockPage();
    const origDown = page.keyboard.down;
    page.keyboard.down = vi.fn(async (k: string) => { downCalls.push(k); });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.keyboard.down).not.toBe(origDown);

    await page.keyboard.down("a");
    // Original should have been called via the patch
    // (the patched version calls originals.keyboardDown which is the stored original)
  });

  it("keyboard.up is patched with small delay", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const origUp = page.keyboard.up;

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.keyboard.up).not.toBe(origUp);
  });
});


// =========================================================================
// Mouse patches — move, click with clickCount
// =========================================================================
describe("Puppeteer: mouse patches", () => {
  it("mouse.move is patched with Bézier movement", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const page = buildMockPage();
    page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.move(500, 500);

    // Bézier generates many intermediate points
    expect(moveCalls.length).toBeGreaterThan(10);
  });

  it("mouse.click is patched with Bézier + humanClick", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const downCalls: any[] = [];
    const upCalls: any[] = [];

    const page = buildMockPage();
    page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); });
    page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts); });

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.click(300, 300);

    expect(moveCalls.length).toBeGreaterThan(5);
    expect(downCalls.length).toBe(1);
    expect(upCalls.length).toBe(1);
  });

  it("mouse.click with clickCount:2 triggers double-click sequence", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const downCalls: any[] = [];
    const upCalls: any[] = [];

    const page = buildMockPage();
    page.mouse.move = vi.fn(async () => {});
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts ?? {}); });
    page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts ?? {}); });

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    await page.mouse.click(300, 300, { clickCount: 2 });

    // First click: down() + up() (no clickCount), Second click: down({clickCount:2}) + up({clickCount:2})
    expect(downCalls.length).toBe(2);
    expect(upCalls.length).toBe(2);

    // Second down/up should have clickCount:2
    const secondDown = downCalls[1];
    const secondUp = upCalls[1];
    expect(secondDown.clickCount).toBe(2);
    expect(secondUp.clickCount).toBe(2);
  });
});


// =========================================================================
// select-all: Puppeteer uses down(modifier) → press('a') → up(modifier)
// =========================================================================
describe("Puppeteer: select-all via modifier keys (not combo string)", () => {
  it("fill() calls keyboardDown(Control/Meta) → keyboardPress('a') → keyboardUp(Control/Meta)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const keyDowns: string[] = [];
    const keyUps: string[] = [];
    const keyPresses: string[] = [];

    const page = buildMockPage();
    page.keyboard.down = vi.fn(async (k: string) => { keyDowns.push(k); });
    page.keyboard.up = vi.fn(async (k: string) => { keyUps.push(k); });
    page.keyboard.press = vi.fn(async (k: string) => { keyPresses.push(k); });

    const cfg = resolveConfig("default", {
      field_switch_delay: [0, 0],
      idle_between_actions: false,
    });
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    // fill → click → selectAll → Backspace → type
    // We can't easily call fill without scrollToElement working,
    // but we can test pressSelectAll indirectly by checking originals are stored
    const originals = (page as any)._original;
    expect(typeof originals.keyboardDown).toBe("function");
    expect(typeof originals.keyboardPress).toBe("function");
    expect(typeof originals.keyboardUp).toBe("function");

    // Verify the modifier is correct for the platform
    const expectedModifier = process.platform === 'darwin' ? 'Meta' : 'Control';

    // Test by importing and calling pressSelectAll-equivalent logic
    await originals.keyboardDown(expectedModifier);
    await originals.keyboardPress('a');
    await originals.keyboardUp(expectedModifier);

    expect(keyDowns).toContain(expectedModifier);
    expect(keyPresses).toContain('a');
    expect(keyUps).toContain(expectedModifier);
  });
});


// =========================================================================
// ElementHandle patching (Puppeteer-specific)
// =========================================================================
describe("Puppeteer: ElementHandle patching", () => {
  it("page.$() returns patched ElementHandle", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockEl = buildMockElementHandle();
    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#test');
    expect(el).toBeDefined();
    expect((el as any)._humanPatched).toBe(true);
  });

  it("page.$$() returns patched ElementHandles", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockEl1 = buildMockElementHandle();
    const mockEl2 = buildMockElementHandle();
    const page = buildMockPage({

      $$: vi.fn(async () => [mockEl1, mockEl2]),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const els = await page.$$('.items');
    expect(els.length).toBe(2);
    expect((els[0] as any)._humanPatched).toBe(true);
    expect((els[1] as any)._humanPatched).toBe(true);
  });

  it("page.waitForSelector() returns patched ElementHandle", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockEl = buildMockElementHandle();
    const page = buildMockPage({
      waitForSelector: vi.fn(async () => mockEl),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.waitForSelector('#loading');
    expect(el).toBeDefined();
    expect((el as any)._humanPatched).toBe(true);
  });

  it("patched el.click() uses humanMove + humanClick", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const downCalls: any[] = [];
    const upCalls: any[] = [];

    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => ({ x: 200, y: 300, width: 100, height: 30 })),
      evaluate: vi.fn(async () => false), // not an input
    });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });
    page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); });
    page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts); });

    const cfg = resolveConfig("default", { idle_between_actions: false });
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#btn');
    await el.click();

    // Bézier movement → multiple move calls
    expect(moveCalls.length).toBeGreaterThan(5);
    // Click → down + up
    expect(downCalls.length).toBe(1);
    expect(upCalls.length).toBe(1);
  });

  it("patched el.click({clickCount:2}) triggers double-click", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const downCalls: any[] = [];
    const upCalls: any[] = [];

    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => ({ x: 200, y: 300, width: 100, height: 30 })),
      evaluate: vi.fn(async () => false),
    });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });
    page.mouse.move = vi.fn(async () => {});
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts ?? {}); });
    page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts ?? {}); });

    const cfg = resolveConfig("default", { idle_between_actions: false });
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#text');
    await el.click({ clickCount: 2 });

    // First click + second click with clickCount:2
    expect(downCalls.length).toBe(2);
    expect(upCalls.length).toBe(2);
    expect(downCalls[1].clickCount).toBe(2);
    expect(upCalls[1].clickCount).toBe(2);
  });

  it("patched el.hover() uses humanMove without clicking", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const downCalls: any[] = [];

    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => ({ x: 150, y: 250, width: 80, height: 25 })),
      evaluate: vi.fn(async () => false),
    });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });
    page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); });

    const cfg = resolveConfig("default", { idle_between_actions: false });
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#link');
    await el.hover();

    expect(moveCalls.length).toBeGreaterThan(5);
    // Hover should NOT click
    expect(downCalls.length).toBe(0);
  });

  it("patched el.type() moves, clicks, then types with humanType", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const downCalls: any[] = [];
    const charCalls: string[] = [];

    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => ({ x: 100, y: 200, width: 200, height: 30 })),
      evaluate: vi.fn(async () => true), // is an input
    });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });
    page.mouse.move = vi.fn(async () => {});
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); });
    page.mouse.up = vi.fn(async () => {});
    page.keyboard.down = vi.fn(async (k: string) => { charCalls.push(`down:${k}`); });
    page.keyboard.up = vi.fn(async (k: string) => { charCalls.push(`up:${k}`); });
    page.keyboard.sendCharacter = vi.fn(async () => {});

    const cfg = resolveConfig("default", {
      mistype_chance: 0,
      idle_between_actions: false,
      typing_delay: 0,
      typing_delay_spread: 0,
      key_hold: [0, 0],
    });
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#email');
    await el.type('ab');

    // Should have clicked first (mouseDown)
    expect(downCalls.length).toBe(1);
    // Should have typed 'a' and 'b' via keyboard.down/up
    expect(charCalls).toContain('down:a');
    expect(charCalls).toContain('up:a');
    expect(charCalls).toContain('down:b');
    expect(charCalls).toContain('up:b');
  });

  it("patched el.focus() clicks to focus instead of programmatic focus", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const downCalls: any[] = [];

    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => ({ x: 100, y: 200, width: 200, height: 30 })),
      evaluate: vi.fn(async () => true),
    });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });
    page.mouse.move = vi.fn(async () => {});
    page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); });
    page.mouse.up = vi.fn(async () => {});

    const cfg = resolveConfig("default", { idle_between_actions: false });
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#input');
    await el.focus();

    // focus should trigger a click (mouseDown + mouseUp)
    expect(downCalls.length).toBe(1);
  });

  it("el with null boundingBox falls back to original", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const originalClickCalled = { value: false };
    const mockEl = buildMockElementHandle({
      boundingBox: vi.fn(async () => null), // not visible
    });
    // Override the original click to track it
    mockEl.click = vi.fn(async () => { originalClickCalled.value = true; });

    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    const el = await page.$('#hidden');
    await el.click();

    // Should have fallen back to original click
    expect(originalClickCalled.value).toBe(true);
  });

  it("nested el.$() returns patched child", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const childEl = buildMockElementHandle();
    const parentEl = buildMockElementHandle();
    parentEl.$ = vi.fn(async () => childEl);

    const page = buildMockPage({
      $: vi.fn(async () => parentEl),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const parent = await page.$('#parent');
    const child = await parent.$('.child');

    expect(child).toBeDefined();
    expect((child as any)._humanPatched).toBe(true);
  });

  it("double-patching is prevented (_humanPatched guard)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockEl = buildMockElementHandle();
    const page = buildMockPage({
      $: vi.fn(async () => mockEl),
    });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    // First call
    const el1 = await page.$('#btn');
    const clickFn1 = el1.click;

    // Second call — same element, should not re-patch
    const el2 = await page.$('#btn');
    const clickFn2 = el2.click;

    // Functions should be identical (not re-wrapped)
    expect(clickFn1).toBe(clickFn2);
  });
});


// =========================================================================
// Frame-level patching
// =========================================================================
describe("Puppeteer: frame-level patching", () => {
  it("child frames are marked _humanPatched", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const childFrame: any = {
      click: vi.fn(async () => {}),
      hover: vi.fn(async () => {}),
      type: vi.fn(async () => {}),
      fill: vi.fn(async () => {}),
      check: vi.fn(async () => {}),
      uncheck: vi.fn(async () => {}),
      select: vi.fn(async () => []),
      press: vi.fn(async () => {}),
      clear: vi.fn(async () => {}),
      focus: vi.fn(async () => {}),
      tap: vi.fn(async () => {}),
      pressSequentially: vi.fn(async () => {}),
      dragAndDrop: vi.fn(async () => {}),
      $: vi.fn(async () => null),

      $$: vi.fn(async () => []),
      waitForSelector: vi.fn(async () => null),
      childFrames: vi.fn(() => []),
    };

    const mainFrame = {
      ...childFrame,
      childFrames: vi.fn(() => [childFrame]),
    };

    const page = buildMockPage({ mainFrameReturn: mainFrame });
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect((childFrame as any)._humanPatched).toBe(true);
  });

  it("frame.focus is patched to delegate to page.focus", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const childFrame: any = {
      click: vi.fn(async () => {}),
      hover: vi.fn(async () => {}),
      type: vi.fn(async () => {}),
      fill: vi.fn(async () => {}),
      check: vi.fn(async () => {}),
      uncheck: vi.fn(async () => {}),
      select: vi.fn(async () => []),
      press: vi.fn(async () => {}),
      clear: vi.fn(async () => {}),
      focus: vi.fn(async () => {}),
      tap: vi.fn(async () => {}),
      pressSequentially: vi.fn(async () => {}),
      dragAndDrop: vi.fn(async () => {}),
      $: vi.fn(async () => null),

      $$: vi.fn(async () => []),
      waitForSelector: vi.fn(async () => null),
      childFrames: vi.fn(() => []),
    };

    const mainFrame = {
      ...childFrame,
      childFrames: vi.fn(() => [childFrame]),
    };

    const page = buildMockPage({ mainFrameReturn: mainFrame });
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    // frame.focus should now be patched (not the original)
    const originalFrameFocus = vi.fn(async () => {});
    expect(childFrame.focus).not.toBe(originalFrameFocus);
    expect(typeof childFrame.focus).toBe("function");
  });

  it("frame.$() returns patched ElementHandles", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const mockEl = buildMockElementHandle();
    const childFrame: any = {
      click: vi.fn(async () => {}),
      hover: vi.fn(async () => {}),
      type: vi.fn(async () => {}),
      fill: vi.fn(async () => {}),
      check: vi.fn(async () => {}),
      uncheck: vi.fn(async () => {}),
      select: vi.fn(async () => []),
      press: vi.fn(async () => {}),
      clear: vi.fn(async () => {}),
      focus: vi.fn(async () => {}),
      tap: vi.fn(async () => {}),
      pressSequentially: vi.fn(async () => {}),
      dragAndDrop: vi.fn(async () => {}),
      $: vi.fn(async () => mockEl),

      $$: vi.fn(async () => []),
      waitForSelector: vi.fn(async () => null),
      childFrames: vi.fn(() => []),
    };

    const mainFrame = {
      ...childFrame,
      childFrames: vi.fn(() => [childFrame]),
    };

    const page = buildMockPage({ mainFrameReturn: mainFrame });
    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const el = await childFrame.$('#in-frame');
    expect(el).toBeDefined();
    expect((el as any)._humanPatched).toBe(true);
  });
});


// =========================================================================
// Browser-level patching
// =========================================================================
describe("Puppeteer: browser-level patching", () => {
  it("patchBrowser patches newPage()", async () => {
    const { patchBrowser } = await import("../src/human-puppeteer/index.js");

    const browser = buildMockBrowser();
    const origNewPage = browser.newPage; 
    const cfg = resolveConfig("default");
    patchBrowser(browser as any, cfg);

    expect(browser.newPage).not.toBe(origNewPage); 
    expect(typeof browser.newPage).toBe("function");
  });

  it("patchBrowser patches createIncognitoBrowserContext()", async () => {
    const { patchBrowser } = await import("../src/human-puppeteer/index.js");

    const browser: any = {
      pages: vi.fn(async () => []),
      newPage: vi.fn(async () => buildMockPage()),
      createIncognitoBrowserContext: vi.fn(async () => ({
        newPage: vi.fn(async () => buildMockPage()),
      })),
      on: vi.fn(),
      close: vi.fn(async () => {}),
    };

    const origCreateCtx = browser.createIncognitoBrowserContext;
    const cfg = resolveConfig("default");
    patchBrowser(browser as any, cfg);

    expect(browser.createIncognitoBrowserContext).not.toBe(origCreateCtx);
  });

  it("patchBrowser listens for targetcreated event", async () => {
    const { patchBrowser } = await import("../src/human-puppeteer/index.js");

    const browser = buildMockBrowser();
    const cfg = resolveConfig("default");
    patchBrowser(browser as any, cfg);

    expect(browser.on).toHaveBeenCalledWith("targetcreated", expect.any(Function));
  });

  it("newPage from patched browser returns page with _original", async () => {
    const { patchBrowser } = await import("../src/human-puppeteer/index.js");

    const mockPage = buildMockPage();
    const browser = buildMockBrowser();
    const origNewPage = vi.fn(async () => mockPage);
    browser.newPage = origNewPage;

    const cfg = resolveConfig("default");
    patchBrowser(browser as any, cfg);

    const page = await browser.newPage();
    expect((page as any)._original).toBeDefined();
    expect((page as any)._humanCfg).toBe(cfg);
    expect((page as any)._stealth).toBeDefined();
  });

  it("pages from patched createIncognitoBrowserContext also get humanized", async () => {
    const { patchBrowser } = await import("../src/human-puppeteer/index.js");

    const mockPage = buildMockPage();
    const mockCtx = {
      newPage: vi.fn(async () => mockPage),
    };
    const browser: any = {
      pages: vi.fn(async () => []),
      newPage: vi.fn(async () => buildMockPage()),
      createIncognitoBrowserContext: vi.fn(async () => mockCtx),
      on: vi.fn(),
      close: vi.fn(async () => {}),
    };

    const cfg = resolveConfig("default");
    patchBrowser(browser as any, cfg);

    const ctx = await browser.createIncognitoBrowserContext();
    const page = await ctx.newPage();

    expect((page as any)._original).toBeDefined();
    expect((page as any)._humanCfg).toBe(cfg);
  });
});



// =========================================================================
// isInputElement / isSelectorFocused — through patchPage click flow
// =========================================================================
describe("Puppeteer: isInputElement stealth integration via patchPage", () => {
  it("click() uses stealth.evaluate for isInputElement (no page.evaluate)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const evaluateCalls: any[] = [];
    const stealthEvaluateCalls: string[] = [];

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string, params?: any) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 100 };
        }
        if (method === "Runtime.evaluate") {
          stealthEvaluateCalls.push(params.expression);
          return { result: { value: false } };
        }
        return {};
      }),
    });

    const mockEl = buildMockElementHandle();
    const page = buildMockPage({
      evaluate: vi.fn(async (...args: any[]) => {
        evaluateCalls.push(args);
        return false;
      }),
      $: vi.fn(async () => mockEl),
    });
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default", { idle_between_actions: false });
    const cursor = { x: 100, y: 100, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    try {
      await page.click("#btn");
    } catch (e) {
      // scrollToElement might throw with mocks
    }

    const isInputCalls = stealthEvaluateCalls.filter(
      expr => expr.includes("tagName") || expr.includes("querySelector")
    );

    const qsCalls = evaluateCalls.filter(
      args => typeof args[0] === "string" && args[0].includes("querySelector")
    );

    if (isInputCalls.length > 0) {
      expect(qsCalls.length).toBe(0);
    }
  });
});


// =========================================================================
// isSelectorFocused stealth integration via patchPage press flow
// =========================================================================
describe("Puppeteer: isSelectorFocused stealth integration via patchPage", () => {
  it("focus() uses stealth.evaluate for focus check", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const stealthEvaluateCalls: string[] = [];

    const mockCdp = buildMockCDP({
      send: vi.fn(async (method: string, params?: any) => {
        if (method === "Page.getFrameTree") {
          return { frameTree: { frame: { id: "F1" } } };
        }
        if (method === "Page.createIsolatedWorld") {
          return { executionContextId: 200 };
        }
        if (method === "Runtime.evaluate") {
          stealthEvaluateCalls.push(params.expression);
          // Return true = element IS focused → focus() should skip clicking
          return { result: { value: true } };
        }
        return {};
      }),
    });

    const page = buildMockPage({
      evaluate: vi.fn(async () => true),
    });
    page.createCDPSession = vi.fn(async () => mockCdp);

    const cfg = resolveConfig("default");
    const cursor = { x: 50, y: 50, initialized: true };
    patchPage(page as any, cfg, cursor as any);

    try {
      await page.focus("input#field");
    } catch (e) {
      // May throw with mocks — that's fine, we just need the stealth call
    }

    const focusCalls = stealthEvaluateCalls.filter(
      expr => expr.includes("activeElement")
    );
    expect(focusCalls.length).toBeGreaterThan(0);
  });
});


// =========================================================================
// Puppeteer-specific: sendCharacter mapped to insertText
// =========================================================================
describe("Puppeteer: sendCharacter → insertText mapping", () => {
  it("rawKb.insertText is mapped from keyboard.sendCharacter", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const sentChars: string[] = [];
    const page = buildMockPage();
    page.keyboard.sendCharacter = vi.fn(async (ch: string) => { sentChars.push(ch); });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    const originals = (page as any)._original;
    expect(typeof originals.keyboardSendCharacter).toBe("function");

    // rawKb.insertText should use the original sendCharacter
    await originals.keyboardSendCharacter("€");
    expect(sentChars).toContain("€");
  });
});


// =========================================================================
// Puppeteer-specific: viewport() vs viewportSize()
// =========================================================================
describe("Puppeteer: viewport() API (not viewportSize)", () => {
  it("patchPage works with page.viewport() (Puppeteer API)", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    // Puppeteer uses viewport(), not viewportSize()
    page.viewport = vi.fn(() => ({ width: 1920, height: 1080 }));
    expect(page.viewportSize).toBeUndefined; // should NOT exist

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };

    // Should not throw
    expect(() => patchPage(page as any, cfg, cursor as any)).not.toThrow();
  });
});


// =========================================================================
// Cursor initialization
// =========================================================================
describe("Puppeteer: cursor initialization", () => {
  it("cursor is initialized on patchPage with random position", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const moveCalls: Array<[number, number]> = [];
    const page = buildMockPage();
    page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); });

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    // Wait for the async init
    await sleep(50);

    // Cursor should be initialized within the config range
    expect(cursor.x).toBeGreaterThanOrEqual(cfg.initial_cursor_x[0]);
    expect(cursor.x).toBeLessThanOrEqual(cfg.initial_cursor_x[1]);
    expect(cursor.y).toBeGreaterThanOrEqual(cfg.initial_cursor_y[0]);
    expect(cursor.y).toBeLessThanOrEqual(cfg.initial_cursor_y[1]);
    expect(cursor.initialized).toBe(true);
  });
});


// =========================================================================
// Page-level method replacement verification
// =========================================================================
describe("Puppeteer: all page methods are replaced", () => {
  it("all interaction methods are replaced after patchPage", async () => {
    const { patchPage } = await import("../src/human-puppeteer/index.js");

    const page = buildMockPage();
    const originals = {
      goto: page.goto,
      click: page.click,
      hover: page.hover,
      type: page.type,
      select: page.select,
      focus: page.focus,
      tap: page.tap,
      mouseMove: page.mouse.move,
      mouseClick: page.mouse.click,
      mouseWheel: page.mouse.wheel,
      keyboardType: page.keyboard.type,
      keyboardPress: page.keyboard.press,
      keyboardDown: page.keyboard.down,
      keyboardUp: page.keyboard.up,
    };

    const cfg = resolveConfig("default");
    const cursor = { x: 0, y: 0, initialized: false };
    patchPage(page as any, cfg, cursor as any);

    expect(page.goto).not.toBe(originals.goto);
    expect(page.click).not.toBe(originals.click);
    expect(page.hover).not.toBe(originals.hover);
    expect(page.type).not.toBe(originals.type);
    expect(page.select).not.toBe(originals.select);
    expect(page.focus).not.toBe(originals.focus);
    expect(page.mouse.move).not.toBe(originals.mouseMove);
    expect(page.mouse.click).not.toBe(originals.mouseClick);
    expect(page.mouse.wheel).not.toBe(originals.mouseWheel);
    expect(page.keyboard.type).not.toBe(originals.keyboardType);
    expect(page.keyboard.press).not.toBe(originals.keyboardPress);
    expect(page.keyboard.down).not.toBe(originals.keyboardDown);
    expect(page.keyboard.up).not.toBe(originals.keyboardUp);
  });
});


// =========================================================================
// SLOW TESTS — require real browser (run with: vitest run --testTimeout=60000)
// Only run when SLOW=1 env var is set
// =========================================================================

const SLOW = process.env.SLOW === '1';
const describeIfSlow = SLOW ? describe : describe.skip;

describeIfSlow("Puppeteer stealth browser: no evaluate leak on click", () => {
  it("click() does not trigger querySelector from evaluate context", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
    await sleep(1000);

    // Inject detection script
    await page.evaluate(() => {
      (window as any).__evalLeaks = [];
      const origQS = document.querySelector.bind(document);
      document.querySelector = function (sel: string) {
        try { throw new Error(); } catch (e: any) {
          if (e.stack && e.stack.includes(':302:')) {
            (window as any).__evalLeaks.push(sel);
          }
        }
        return origQS(sel);
      } as any;
    });

    await page.click('#searchInput');
    await sleep(500);

    const leaks = await page.evaluate(() => (window as any).__evalLeaks || []);
    expect(leaks.length).toBe(0);

    await browser.close();
  }, 30000);
});

describeIfSlow("Puppeteer stealth browser: shift symbols isTrusted=true", () => {
  it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
    await sleep(1000);

    await page.evaluate(() => {
      (window as any).__untrustedKeys = [];
      (window as any).__trustedKeys = [];
      const input = document.querySelector('#searchInput');
      if (input) {
        input.addEventListener('keydown', (e) => {
          if (!e.isTrusted) {
            (window as any).__untrustedKeys.push((e as KeyboardEvent).key);
          } else {
            (window as any).__trustedKeys.push((e as KeyboardEvent).key);
          }
        }, true);
      }
    });

    await page.click('#searchInput');
    await sleep(300);
    await page.keyboard.type('test!');
    await sleep(500);

    const untrusted = await page.evaluate(() => (window as any).__untrustedKeys || []);
    const trusted = await page.evaluate(() => (window as any).__trustedKeys || []);

    expect(untrusted).not.toContain('!');
    expect(trusted).toContain('!');

    await browser.close();
  }, 30000);
});

describeIfSlow("Puppeteer stealth browser: navigation invalidation", () => {
  it("click works after navigation (isolated world re-created)", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    expect((page as any)._stealth).toBeDefined();

    await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
    await sleep(1000);
    await page.click('#searchInput');
    await sleep(300);

    // Second navigation
    await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
    await sleep(1000);

    // Should still work
    await page.click('#searchInput');
    await sleep(300);
    await page.keyboard.type('after navigation');
    await sleep(500);

    // Puppeteer doesn't have locator().inputValue(), use evaluate instead
    const val = await page.evaluate(() => {
      const el = document.querySelector('#searchInput') as HTMLInputElement;
      return el ? el.value : '';
    });
    expect(val).toContain('after navigation');

    await browser.close();
  }, 60000);
});

describeIfSlow("Puppeteer stealth browser: ElementHandle humanization", () => {
  it("el.click() uses Bézier movement, not instant CDP click", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    await sleep(1000);

    // Track mousemove events at document level
    await page.evaluate(() => {
      (window as any).__moveCounts = 0;
      document.addEventListener('mousemove', () => {
        (window as any).__moveCounts++;
      }, true);
    });

    // <h1> on example.com — always present, clickable, no navigation
    const el = await page.$('h1');
    expect(el).not.toBeNull();
    expect((el as any)._humanPatched).toBe(true);

    // Reset counter right before the click
    await page.evaluate(() => { (window as any).__moveCounts = 0; });

    // Click via ElementHandle — should use Bézier curve
    await el!.click();
    await sleep(500);

    const moveCount = await page.evaluate(() => (window as any).__moveCounts || 0);

    // Bézier movement generates many intermediate mousemove events (>10)
    // An instant CDP dispatchMouseEvent would generate 0 or 1
    expect(moveCount).toBeGreaterThan(5);

    await browser.close();
  }, 30000);
});

describeIfSlow("Puppeteer stealth browser: focus() uses click", () => {
  it("page.focus() triggers mouse events (not programmatic focus)", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
    await sleep(2000);

    // Click somewhere else first to ensure #searchInput is NOT focused
    await page.mouse.click(50, 50);
    await sleep(500);

    // Inject event tracking on #searchInput AFTER ensuring it's not focused
    await page.evaluate(() => {
      (window as any).__mouseEvents = [];
      const input = document.querySelector('#searchInput');
      if (input) {
        for (const evt of ['mousedown', 'mouseup', 'click', 'mousemove']) {
          input.addEventListener(evt, (e) => {
            (window as any).__mouseEvents.push(evt);
          }, true);
        }
      }
    });

    // Now focus — should trigger humanized click with mouse events
    await page.focus('#searchInput');
    await sleep(1000);

    const events = await page.evaluate(() => (window as any).__mouseEvents || []);
    expect(events.length).toBeGreaterThan(0);
    expect(events).toContain('mousedown');

    await browser.close();
  }, 30000);
});

describeIfSlow("Puppeteer stealth browser: mouse.wheel smooth scroll", () => {
  it("mouse.wheel generates multiple small scroll events", async () => {
    const { launch } = await import("../src/puppeteer.js");

    const browser = await launch({ humanize: true, headless: true });
    const page = await browser.newPage();

    await page.goto('https://en.wikipedia.org/wiki/Main_Page', { waitUntil: 'domcontentloaded' });
    await sleep(1000);

    // Track scroll events
    await page.evaluate(() => {
      (window as any).__wheelEvents = 0;
      window.addEventListener('wheel', () => {
        (window as any).__wheelEvents++;
      }, { passive: true });
    });

    await page.mouse.wheel({ deltaY: 500 });
    await sleep(1000);

    const wheelCount = await page.evaluate(() => (window as any).__wheelEvents || 0);
    // Smooth scroll should generate multiple wheel events, not just 1
    expect(wheelCount).toBeGreaterThan(3);

    await browser.close();
  }, 30000);
});
