import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { binaryInfo } from "../src/download.js";
import { DEFAULT_VIEWPORT, getBinaryPath, getChromiumVersion, getPlatformTag } from "../src/config.js";

describe("binaryInfo", () => {
  it("returns correct structure", () => {
    const orig = process.env.CLOAKBROWSER_CACHE_DIR;
    process.env.CLOAKBROWSER_CACHE_DIR = `/tmp/cloakbrowser-test-${Date.now()}`;
    try {
      const info = binaryInfo();

      expect(info.version).toBe(getChromiumVersion());
      expect(info.bundledVersion).toBeTruthy();
      expect(info.platform).toMatch(/^(linux|darwin|windows)-(x64|arm64)$/);
      expect(info.binaryPath).toBeTruthy();
      expect(typeof info.installed).toBe("boolean");
      expect(info.cacheDir).toContain("cloakbrowser");
    } finally {
      if (orig) process.env.CLOAKBROWSER_CACHE_DIR = orig;
      else delete process.env.CLOAKBROWSER_CACHE_DIR;
    }
  });

  it("reports tier from the installed binary, not a cached license", () => {
    // A valid, fresh license is cached but NO Pro binary is on disk → free.
    const orig = process.env.CLOAKBROWSER_CACHE_DIR;
    const dir = `/tmp/cloakbrowser-test-${Date.now()}-tier`;
    fs.mkdirSync(dir, { recursive: true });
    process.env.CLOAKBROWSER_CACHE_DIR = dir;
    try {
      fs.writeFileSync(
        path.join(dir, ".license_cache"),
        JSON.stringify({
          key_sha256: "abc",
          valid: true,
          plan: "solo",
          expires: null,
          validated_at: Date.now() / 1000,
        })
      );
      expect(binaryInfo().tier).toBe("free");

      // Now drop a Pro binary on disk → pro.
      fs.writeFileSync(path.join(dir, `latest_pro_version_${getPlatformTag()}`), "147.0.5555.1");
      const bp = getBinaryPath("147.0.5555.1", true);
      fs.mkdirSync(path.dirname(bp), { recursive: true });
      fs.writeFileSync(bp, "fake");
      fs.chmodSync(bp, 0o755);
      const info = binaryInfo();
      expect(info.tier).toBe("pro");
      expect(info.version).toBe("147.0.5555.1");
    } finally {
      fs.rmSync(dir, { recursive: true, force: true });
      if (orig) process.env.CLOAKBROWSER_CACHE_DIR = orig;
      else delete process.env.CLOAKBROWSER_CACHE_DIR;
    }
  });

  it("threads the channel into the Pro download URL", () => {
    const orig = process.env.CLOAKBROWSER_CACHE_DIR;
    const dir = `/tmp/cloakbrowser-test-${Date.now()}-chan`;
    fs.mkdirSync(dir, { recursive: true });
    process.env.CLOAKBROWSER_CACHE_DIR = dir;
    try {
      // Same cached Pro binary satisfies both channels (version-keyed cache dir).
      fs.writeFileSync(path.join(dir, `latest_pro_version_preview_${getPlatformTag()}`), "151.0.7900.10.1");
      fs.writeFileSync(path.join(dir, `latest_pro_version_${getPlatformTag()}`), "151.0.7900.10.1");
      const bp = getBinaryPath("151.0.7900.10.1", true);
      fs.mkdirSync(path.dirname(bp), { recursive: true });
      fs.writeFileSync(bp, "fake");
      fs.chmodSync(bp, 0o755);

      expect(binaryInfo(undefined, "preview").downloadUrl).toMatch(/\/api\/download\/latest\?channel=preview$/);
      const stableUrl = binaryInfo(undefined, "stable").downloadUrl;
      expect(stableUrl).toMatch(/\/api\/download\/latest$/);
      expect(stableUrl).not.toContain("channel=preview");
    } finally {
      fs.rmSync(dir, { recursive: true, force: true });
      if (orig) process.env.CLOAKBROWSER_CACHE_DIR = orig;
      else delete process.env.CLOAKBROWSER_CACHE_DIR;
    }
  });
});

describe("composable Playwright launch helpers", () => {
  const origBinaryPath = process.env.CLOAKBROWSER_BINARY_PATH;

  beforeEach(() => {
    process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
    vi.resetModules();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.resetModules();
    if (origBinaryPath) {
      process.env.CLOAKBROWSER_BINARY_PATH = origBinaryPath;
    } else {
      delete process.env.CLOAKBROWSER_BINARY_PATH;
    }
  });

  it("forwards releaseChannel to Playwright binary resolution", async () => {
    delete process.env.CLOAKBROWSER_BINARY_PATH;
    const ensureBinary = vi.fn().mockResolvedValue("/fake/chrome");
    vi.doMock("../src/download.js", () => ({ ensureBinary }));
    try {
      const { buildLaunchOptions } = await import("../src/playwright.js");

      await buildLaunchOptions({ releaseChannel: "preview" });

      expect(ensureBinary).toHaveBeenCalledWith(undefined, undefined, "preview");
    } finally {
      vi.doUnmock("../src/download.js");
    }
  });

  it("exports composable helpers from the package entrypoint", async () => {
    const entry = await import("../src/index.js");

    expect(entry.buildLaunchOptions).toBeTypeOf("function");
    expect(entry.buildContextOptions).toBeTypeOf("function");
    expect(entry.humanizeBrowser).toBeTypeOf("function");
  });

  it("buildContextOptions returns Playwright context options without launching a browser", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const { buildContextOptions } = await import("../src/index.js");

    const options = buildContextOptions({
      userAgent: "Explicit/1.0",
      viewport: { width: 1280, height: 720 },
      colorScheme: "dark",
      contextOptions: {
        userAgent: "Context/9.9",
        viewport: { width: 9999, height: 9999 },
        colorScheme: "light",
        storageState: "state.json",
        locale: "de-DE",
        timezoneId: "Europe/Berlin",
      },
    });

    expect(options).toMatchObject({
      userAgent: "Explicit/1.0",
      viewport: { width: 1280, height: 720 },
      colorScheme: "dark",
      storageState: "state.json",
    });
    expect(options.locale).toBeUndefined();
    expect(options.timezoneId).toBeUndefined();
    expect(warnSpy).toHaveBeenCalledTimes(2);
  });

  it("buildContextOptions applies DEFAULT_VIEWPORT by default and allows null viewport", async () => {
    const { buildContextOptions } = await import("../src/index.js");

    expect(buildContextOptions().viewport).toEqual(DEFAULT_VIEWPORT);
    expect(buildContextOptions({ viewport: null }).viewport).toBeNull();
  });

  it("buildContextOptions uses no viewport (null) when headed, so the page tracks the real window", async () => {
    const { buildContextOptions } = await import("../src/index.js");

    // Headed: no emulated viewport (CDP emulation would force outerWidth < innerWidth).
    expect(buildContextOptions({ headless: false }).viewport).toBeNull();
    // Headless keeps the deterministic default.
    expect(buildContextOptions({ headless: true }).viewport).toEqual(DEFAULT_VIEWPORT);
    // Explicit viewport always honored, even headed.
    const custom = { width: 800, height: 600 };
    expect(buildContextOptions({ headless: false, viewport: custom }).viewport).toEqual(custom);
  });

  it("buildContextOptions reads effective headless from launchOptions.headless", async () => {
    const { buildContextOptions } = await import("../src/index.js");

    // buildLaunchOptions spreads launchOptions LAST, so launchOptions.headless wins
    // at the actual launch. Viewport must follow it — a raw headless:false (browser
    // actually headed) must NOT get a fixed viewport (would reintroduce outer<inner).
    expect(buildContextOptions({ launchOptions: { headless: false } }).viewport).toBeNull();
    // And launchOptions.headless:true forces the deterministic viewport even if the
    // top-level field said headed.
    expect(
      buildContextOptions({ headless: false, launchOptions: { headless: true } }).viewport,
    ).toEqual(DEFAULT_VIEWPORT);
  });

  it("buildLaunchOptions returns Playwright options without launching a browser", async () => {
    // Free macOS lacks inline proxy auth → credentialed HTTP proxy goes through
    // Playwright's proxy dict. Drive the platform via process.platform/arch since
    // getPlatformTag reads them at call time (a config spy can't reach the
    // intra-config gate call).
    const origPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
    const origArch = Object.getOwnPropertyDescriptor(process, "arch")!;
    Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
    Object.defineProperty(process, "arch", { value: "arm64", configurable: true });
    try {
      const { buildLaunchOptions } = await import("../src/index.js");

      const options = await buildLaunchOptions({
        headless: false,
        proxy: "http://user:pass@proxy.example:8080",
        args: ["--custom-flag"],
        launchOptions: { timeout: 1234 },
      });

      expect(options.executablePath).toBe("/fake/chrome");
      expect(options.headless).toBe(false);
      expect(options.args).toContain("--custom-flag");
      expect(options.ignoreDefaultArgs).toContain("--enable-automation");
      expect(options.proxy).toEqual({
        server: "http://proxy.example:8080",
        username: "user",
        password: "pass",
      });
      expect(options.args).not.toContain(
        "--proxy-server=http://user:pass@proxy.example:8080",
      );
      expect(options.timeout).toBe(1234);
      // No license key -> env is not injected
      expect(options.env).toBeUndefined();
    } finally {
      Object.defineProperty(process, "platform", origPlatform);
      Object.defineProperty(process, "arch", origArch);
      vi.restoreAllMocks();
    }
  });

  it("buildLaunchOptions injects env with license key", async () => {
    const { buildLaunchOptions } = await import("../src/index.js");

    const options = await buildLaunchOptions({
      licenseKey: "cb_test_key",
      launchOptions: { timeout: 1234 },
    });

    expect(options.env).toBeDefined();
    expect(options.env!.CLOAKBROWSER_LICENSE_KEY).toBe("cb_test_key");
    // launchOptions timeout still forwarded
    expect(options.timeout).toBe(1234);
  });

  it("buildLaunchOptions preserves custom env via launchOptions", async () => {
    const { buildLaunchOptions } = await import("../src/index.js");

    const options = await buildLaunchOptions({
      licenseKey: "cb_key",
      launchOptions: {
        timeout: 1234,
        env: { MY_VAR: "custom" },
      },
    });

    expect(options.env).toBeDefined();
    // Custom env var preserved
    expect(options.env!.MY_VAR).toBe("custom");
    // License key injected
    expect(options.env!.CLOAKBROWSER_LICENSE_KEY).toBe("cb_key");
  });

  it("humanizeBrowser patches an existing browser only when requested", async () => {
    const { humanizeBrowser } = await import("../src/index.js");
    const browser = {
      contexts: () => [],
      newContext: vi.fn(async () => ({})),
      newPage: vi.fn(async () => ({ context: () => ({}) })),
    };
    const originalNewContext = browser.newContext;

    await humanizeBrowser(browser as any, { humanize: false });
    expect(browser.newContext).toBe(originalNewContext);

    await humanizeBrowser(browser as any, { humanize: true });
    expect(browser.newContext).not.toBe(originalNewContext);
  });
});

// Integration tests require the binary — run with:
//   CLOAKBROWSER_BINARY_PATH=/path/to/chrome npm test
describe.skipIf(!process.env.CLOAKBROWSER_BINARY_PATH)(
  "launch (integration)",
  () => {
    it("launches browser and checks stealth", async () => {
      const { launch } = await import("../src/playwright.js");

      const browser = await launch({ headless: true });
      const page = await browser.newPage();
      await page.goto("about:blank");

      const webdriver = await page.evaluate(() => navigator.webdriver);
      expect(webdriver).toBeFalsy();

      const plugins = await page.evaluate(() => Array.from(navigator.plugins).length);
      expect(plugins).toBeGreaterThan(0);

      await browser.close();
    }, 30_000);
  }
);

// ---------------------------------------------------------------------------
// launchContext / launchPersistentContext unit tests (mock playwright-core)
// ---------------------------------------------------------------------------

describe("launchContext (unit)", () => {
  let mockContext: any;
  let mockBrowser: any;
  let mockChromium: any;
  const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;

  beforeEach(() => {
    process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
    const origClose = vi.fn();
    mockContext = { close: origClose, _origClose: origClose };
    mockBrowser = {
      newContext: vi.fn().mockResolvedValue(mockContext),
      close: vi.fn(),
    };
    mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };

    vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.resetModules();
    if (origEnv) {
      process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
    } else {
      delete process.env.CLOAKBROWSER_BINARY_PATH;
    }
  });

  it("applies DEFAULT_VIEWPORT when no viewport given", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext();

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT);
  });

  it("uses custom viewport when provided", async () => {
    const { launchContext } = await import("../src/playwright.js");
    const custom = { width: 1280, height: 720 };
    await launchContext({ viewport: custom });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.viewport).toEqual(custom);
  });

  it("forwards userAgent to newContext", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({ userAgent: "Custom/1.0" });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.userAgent).toBe("Custom/1.0");
  });

  it("passes timezone via binary flag, not CDP context", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({ timezone: "America/New_York" });

    // launch() called with --fingerprint-timezone binary flag
    const launchArgs = mockChromium.launch.mock.calls[0][0];
    const hasTimezoneFlag = launchArgs.args.some((a: string) =>
      a.startsWith("--fingerprint-timezone=America/New_York")
    );
    expect(hasTimezoneFlag).toBe(true);

    // NOT in newContext() — no CDP emulation
    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.timezoneId).toBeUndefined();
  });

  it("forwards colorScheme to newContext", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({ colorScheme: "dark" });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.colorScheme).toBe("dark");
  });

  it("close() also closes browser", async () => {
    const { launchContext } = await import("../src/playwright.js");
    const ctx = await launchContext();

    await ctx.close();
    // Original context close called
    expect(mockContext._origClose).toHaveBeenCalledOnce();
    // Browser also closed
    expect(mockBrowser.close).toHaveBeenCalledOnce();
  });

  it("forwards contextOptions to newContext (storageState, etc.)", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({
      contextOptions: {
        storageState: "state.json",
        permissions: ["geolocation"],
      },
    });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.storageState).toBe("state.json");
    expect(ctxArgs.permissions).toEqual(["geolocation"]);
  });

  it("explicit top-level fields win over contextOptions on collision", async () => {
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({
      userAgent: "Explicit/1.0",
      viewport: { width: 1280, height: 720 },
      colorScheme: "dark",
      contextOptions: {
        userAgent: "ShouldBeOverridden/9.9",
        viewport: { width: 9999, height: 9999 },
        colorScheme: "light",
      },
    });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    expect(ctxArgs.userAgent).toBe("Explicit/1.0");
    expect(ctxArgs.viewport).toEqual({ width: 1280, height: 720 });
    expect(ctxArgs.colorScheme).toBe("dark");
  });

  it("strips locale and timezoneId from contextOptions (stealth-sensitive)", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const { launchContext } = await import("../src/playwright.js");
    await launchContext({
      contextOptions: {
        storageState: "state.json",
        locale: "de-DE",
        timezoneId: "Europe/Berlin",
      },
    });

    const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
    // Stealth-sensitive keys stripped — they would reintroduce detectable CDP emulation.
    expect(ctxArgs.locale).toBeUndefined();
    expect(ctxArgs.timezoneId).toBeUndefined();
    // Benign keys preserved
    expect(ctxArgs.storageState).toBe("state.json");
    // Warning was logged for both stripped keys
    expect(warnSpy).toHaveBeenCalledTimes(2);
  });
});

describe("launchPersistentContext (unit)", () => {
  let mockContext: any;
  let mockChromium: any;
  const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;

  beforeEach(() => {
    process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
    mockContext = { close: vi.fn(), pages: vi.fn().mockReturnValue([]) };
    mockChromium = {
      launchPersistentContext: vi.fn().mockResolvedValue(mockContext),
    };

    vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.resetModules();
    if (origEnv) {
      process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
    } else {
      delete process.env.CLOAKBROWSER_BINARY_PATH;
    }
  });

  it("applies DEFAULT_VIEWPORT", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({ userDataDir: "/tmp/profile" });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.viewport).toEqual(DEFAULT_VIEWPORT);
  });

  it("passes timezone and locale via binary args, not CDP context", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      timezone: "Asia/Tokyo",
      locale: "ja-JP",
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    // Binary args (native, undetectable)
    expect(args.args).toContain("--fingerprint-timezone=Asia/Tokyo");
    expect(args.args).toContain("--lang=ja-JP");
    // NOT in context kwargs (would trigger detectable CDP emulation)
    expect(args.timezoneId).toBeUndefined();
    expect(args.locale).toBeUndefined();
  });

  it("forwards proxy string", async () => {
    // Free macOS → credentialed HTTP proxy via Playwright's proxy dict, not
    // inline --proxy-server. Drive platform via process.platform/arch.
    const origPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
    const origArch = Object.getOwnPropertyDescriptor(process, "arch")!;
    Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
    Object.defineProperty(process, "arch", { value: "arm64", configurable: true });
    try {
      const { launchPersistentContext } = await import("../src/playwright.js");
      await launchPersistentContext({
        userDataDir: "/tmp/profile",
        proxy: "http://user:pass@proxy:8080",
      });

      const args = mockChromium.launchPersistentContext.mock.calls[0][1];
      expect(args.proxy).toEqual({
        server: "http://proxy:8080",
        username: "user",
        password: "pass",
      });
      expect(args.args).not.toContain("--proxy-server=http://user:pass@proxy:8080");
    } finally {
      Object.defineProperty(process, "platform", origPlatform);
      Object.defineProperty(process, "arch", origArch);
      vi.restoreAllMocks();
    }
  });

  it("forwards userAgent and colorScheme", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      userAgent: "Custom/1.0",
      colorScheme: "dark",
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.userAgent).toBe("Custom/1.0");
    expect(args.colorScheme).toBe("dark");
  });

  it("forwards contextOptions to launchPersistentContext", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      contextOptions: {
        permissions: ["geolocation"],
        extraHTTPHeaders: { "X-Custom": "1" },
      },
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.permissions).toEqual(["geolocation"]);
    expect(args.extraHTTPHeaders).toEqual({ "X-Custom": "1" });
  });

  it("explicit top-level fields win over contextOptions in persistent context", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      userAgent: "Explicit/1.0",
      viewport: { width: 1280, height: 720 },
      contextOptions: {
        userAgent: "ShouldBeOverridden/9.9",
        viewport: { width: 9999, height: 9999 },
      },
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.userAgent).toBe("Explicit/1.0");
    expect(args.viewport).toEqual({ width: 1280, height: 720 });
  });

  it("strips locale and timezoneId from contextOptions (persistent context)", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      contextOptions: {
        locale: "de-DE",
        timezoneId: "Europe/Berlin",
      },
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.locale).toBeUndefined();
    expect(args.timezoneId).toBeUndefined();
    expect(warnSpy).toHaveBeenCalledTimes(2);
  });

  it("injects env when licenseKey param provided", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      licenseKey: "cb_persistent",
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.env).toBeDefined();
    expect(args.env!.CLOAKBROWSER_LICENSE_KEY).toBe("cb_persistent");
  });

  it("does not inject env when no license key", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({ userDataDir: "/tmp/profile" });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.env).toBeUndefined();
  });

  it("preserves custom launchOptions env merged with license key", async () => {
    const { launchPersistentContext } = await import("../src/playwright.js");
    await launchPersistentContext({
      userDataDir: "/tmp/profile",
      licenseKey: "cb_merge",
      launchOptions: {
        env: { MY_VAR: "keep" },
      },
    });

    const args = mockChromium.launchPersistentContext.mock.calls[0][1];
    expect(args.env).toBeDefined();
    expect(args.env!.CLOAKBROWSER_LICENSE_KEY).toBe("cb_merge");
    expect(args.env!.MY_VAR).toBe("keep");
  });
});

// ---------------------------------------------------------------------------
// License exit-code surfacing at launch() (mock playwright-core to reject)
// ---------------------------------------------------------------------------

describe("launch license error surfacing (unit)", () => {
  const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;

  beforeEach(() => {
    process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.resetModules();
    if (origEnv) {
      process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
    } else {
      delete process.env.CLOAKBROWSER_BINARY_PATH;
    }
  });

  it("maps a license exit code to CloakBrowserLicenseError", async () => {
    const licenseErr = new Error(
      "browserType.launch: Target closed\nBrowser logs:\n- [pid=1] <process did exit: exitCode=77, signal=null>"
    );
    vi.doMock("playwright-core", () => ({
      chromium: { launch: vi.fn().mockRejectedValue(licenseErr) },
    }));
    const { launch } = await import("../src/playwright.js");
    const { CloakBrowserLicenseError } = await import("../src/license.js");
    await expect(launch()).rejects.toBeInstanceOf(CloakBrowserLicenseError);
  });

  it("re-throws a non-license launch error unchanged (exact object)", async () => {
    const other = new Error("some unrelated launch failure");
    vi.doMock("playwright-core", () => ({
      chromium: { launch: vi.fn().mockRejectedValue(other) },
    }));
    const { launch } = await import("../src/playwright.js");
    await expect(launch()).rejects.toBe(other);
  });
});
