import fs from "node:fs";
import fsp from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import { afterEach, describe, expect, it, vi } from "vitest";

import {
  decorateClawdProfile,
  ensureProfileCleanExit,
  findChromeExecutableMac,
  findChromeExecutableWindows,
  isChromeReachable,
  resolveBrowserExecutableForPlatform,
  stopClawdChrome,
} from "./chrome.js";
import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME } from "./constants.js";

async function readJson(filePath: string): Promise<Record<string, unknown>> {
  const raw = await fsp.readFile(filePath, "utf-8");
  return JSON.parse(raw) as Record<string, unknown>;
}

describe("browser chrome profile decoration", () => {
  afterEach(() => {
    vi.unstubAllGlobals();
    vi.restoreAllMocks();
  });

  it("writes expected name + signed ARGB seed to Chrome prefs", async () => {
    const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-"));
    try {
      decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR });

      const expectedSignedArgb = ((0xff << 24) | 0xff4500) >> 0;

      const localState = await readJson(path.join(userDataDir, "Local State"));
      const profile = localState.profile as Record<string, unknown>;
      const infoCache = profile.info_cache as Record<string, unknown>;
      const def = infoCache.Default as Record<string, unknown>;

      expect(def.name).toBe(DEFAULT_CLAWD_BROWSER_PROFILE_NAME);
      expect(def.shortcut_name).toBe(DEFAULT_CLAWD_BROWSER_PROFILE_NAME);
      expect(def.profile_color_seed).toBe(expectedSignedArgb);
      expect(def.profile_highlight_color).toBe(expectedSignedArgb);
      expect(def.default_avatar_fill_color).toBe(expectedSignedArgb);
      expect(def.default_avatar_stroke_color).toBe(expectedSignedArgb);

      const prefs = await readJson(path.join(userDataDir, "Default", "Preferences"));
      const browser = prefs.browser as Record<string, unknown>;
      const theme = browser.theme as Record<string, unknown>;
      const autogenerated = prefs.autogenerated as Record<string, unknown>;
      const autogeneratedTheme = autogenerated.theme as Record<string, unknown>;

      expect(theme.user_color2).toBe(expectedSignedArgb);
      expect(autogeneratedTheme.color).toBe(expectedSignedArgb);

      const marker = await fsp.readFile(
        path.join(userDataDir, ".clawd-profile-decorated"),
        "utf-8",
      );
      expect(marker.trim()).toMatch(/^\d+$/);
    } finally {
      await fsp.rm(userDataDir, { recursive: true, force: true });
    }
  });

  it("best-effort writes name when color is invalid", async () => {
    const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-"));
    try {
      decorateClawdProfile(userDataDir, { color: "lobster-orange" });
      const localState = await readJson(path.join(userDataDir, "Local State"));
      const profile = localState.profile as Record<string, unknown>;
      const infoCache = profile.info_cache as Record<string, unknown>;
      const def = infoCache.Default as Record<string, unknown>;

      expect(def.name).toBe(DEFAULT_CLAWD_BROWSER_PROFILE_NAME);
      expect(def.profile_color_seed).toBeUndefined();
    } finally {
      await fsp.rm(userDataDir, { recursive: true, force: true });
    }
  });

  it("recovers from missing/invalid preference files", async () => {
    const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-"));
    try {
      await fsp.mkdir(path.join(userDataDir, "Default"), { recursive: true });
      await fsp.writeFile(path.join(userDataDir, "Local State"), "{", "utf-8"); // invalid JSON
      await fsp.writeFile(
        path.join(userDataDir, "Default", "Preferences"),
        "[]", // valid JSON but wrong shape
        "utf-8",
      );

      decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR });

      const localState = await readJson(path.join(userDataDir, "Local State"));
      expect(typeof localState.profile).toBe("object");

      const prefs = await readJson(path.join(userDataDir, "Default", "Preferences"));
      expect(typeof prefs.profile).toBe("object");
    } finally {
      await fsp.rm(userDataDir, { recursive: true, force: true });
    }
  });

  it("writes clean exit prefs to avoid restore prompts", async () => {
    const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-"));
    try {
      ensureProfileCleanExit(userDataDir);
      const prefs = await readJson(path.join(userDataDir, "Default", "Preferences"));
      expect(prefs.exit_type).toBe("Normal");
      expect(prefs.exited_cleanly).toBe(true);
    } finally {
      await fsp.rm(userDataDir, { recursive: true, force: true });
    }
  });

  it("is idempotent when rerun on an existing profile", async () => {
    const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-"));
    try {
      decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR });
      decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR });

      const prefs = await readJson(path.join(userDataDir, "Default", "Preferences"));
      const profile = prefs.profile as Record<string, unknown>;
      expect(profile.name).toBe(DEFAULT_CLAWD_BROWSER_PROFILE_NAME);
    } finally {
      await fsp.rm(userDataDir, { recursive: true, force: true });
    }
  });
});

describe("browser chrome helpers", () => {
  afterEach(() => {
    vi.unstubAllEnvs();
    vi.unstubAllGlobals();
    vi.restoreAllMocks();
  });

  it("picks the first existing Chrome candidate on macOS", () => {
    const exists = vi
      .spyOn(fs, "existsSync")
      .mockImplementation((p) =>
        String(p).includes("Google Chrome.app/Contents/MacOS/Google Chrome"),
      );
    const exe = findChromeExecutableMac();
    expect(exe?.kind).toBe("chrome");
    expect(exe?.path).toMatch(/Google Chrome\.app/);
    exists.mockRestore();
  });

  it("returns null when no Chrome candidate exists", () => {
    const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false);
    expect(findChromeExecutableMac()).toBeNull();
    exists.mockRestore();
  });

  it("picks the first existing Chrome candidate on Windows", () => {
    vi.stubEnv("LOCALAPPDATA", "C:\\Users\\Test\\AppData\\Local");
    const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => {
      const pathStr = String(p);
      return (
        pathStr.includes("Google\\Chrome\\Application\\chrome.exe") ||
        pathStr.includes("BraveSoftware\\Brave-Browser\\Application\\brave.exe") ||
        pathStr.includes("Microsoft\\Edge\\Application\\msedge.exe")
      );
    });
    const exe = findChromeExecutableWindows();
    expect(exe?.kind).toBe("chrome");
    expect(exe?.path).toMatch(/chrome\.exe$/);
    exists.mockRestore();
  });

  it("finds Chrome in Program Files on Windows", () => {
    const marker = path.win32.join("Program Files", "Google", "Chrome");
    const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker));
    const exe = findChromeExecutableWindows();
    expect(exe?.kind).toBe("chrome");
    expect(exe?.path).toMatch(/chrome\.exe$/);
    exists.mockRestore();
  });

  it("returns null when no Chrome candidate exists on Windows", () => {
    const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false);
    expect(findChromeExecutableWindows()).toBeNull();
    exists.mockRestore();
  });

  it("resolves Windows executables without LOCALAPPDATA", () => {
    vi.stubEnv("LOCALAPPDATA", "");
    vi.stubEnv("ProgramFiles", "C:\\Program Files");
    vi.stubEnv("ProgramFiles(x86)", "C:\\Program Files (x86)");
    const marker = path.win32.join(
      "Program Files",
      "Google",
      "Chrome",
      "Application",
      "chrome.exe",
    );
    const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker));
    const exe = resolveBrowserExecutableForPlatform(
      {} as Parameters<typeof resolveBrowserExecutableForPlatform>[0],
      "win32",
    );
    expect(exe?.kind).toBe("chrome");
    expect(exe?.path).toMatch(/chrome\.exe$/);
    exists.mockRestore();
  });

  it("reports reachability based on /json/version", async () => {
    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({ webSocketDebuggerUrl: "ws://127.0.0.1/devtools" }),
      } as unknown as Response),
    );
    await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(true);

    vi.stubGlobal(
      "fetch",
      vi.fn().mockResolvedValue({
        ok: false,
        json: async () => ({}),
      } as unknown as Response),
    );
    await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false);

    vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("boom")));
    await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false);
  });

  it("stopClawdChrome no-ops when process is already killed", async () => {
    const proc = { killed: true, exitCode: null, kill: vi.fn() };
    await stopClawdChrome(
      {
        proc,
        cdpPort: 12345,
      } as unknown as Parameters<typeof stopClawdChrome>[0],
      10,
    );
    expect(proc.kill).not.toHaveBeenCalled();
  });

  it("stopClawdChrome sends SIGTERM and returns once CDP is down", async () => {
    vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("down")));
    const proc = { killed: false, exitCode: null, kill: vi.fn() };
    await stopClawdChrome(
      {
        proc,
        cdpPort: 12345,
      } as unknown as Parameters<typeof stopClawdChrome>[0],
      10,
    );
    expect(proc.kill).toHaveBeenCalledWith("SIGTERM");
  });
});
