import { describe, expect, it, vi } from "vitest";
import { appendCdpPath, getHeadersWithAuth } from "./cdp.helpers.js";
import { __test } from "./client-fetch.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
import { shouldRejectBrowserMutation } from "./csrf.js";
import {
  ensureChromeExtensionRelayServer,
  stopChromeExtensionRelayServer,
} from "./extension-relay.js";
import { toBoolean } from "./routes/utils.js";
import type { BrowserServerState } from "./server-context.js";
import { listKnownProfileNames } from "./server-context.js";
import { resolveTargetIdFromTabs } from "./target-id.js";
import { getFreePort } from "./test-port.js";

describe("toBoolean", () => {
  it("parses yes/no and 1/0", () => {
    expect(toBoolean("yes")).toBe(true);
    expect(toBoolean("1")).toBe(true);
    expect(toBoolean("no")).toBe(false);
    expect(toBoolean("0")).toBe(false);
  });

  it("returns undefined for on/off strings", () => {
    expect(toBoolean("on")).toBeUndefined();
    expect(toBoolean("off")).toBeUndefined();
  });

  it("passes through boolean values", () => {
    expect(toBoolean(true)).toBe(true);
    expect(toBoolean(false)).toBe(false);
  });
});

describe("browser target id resolution", () => {
  it("resolves exact ids", () => {
    const res = resolveTargetIdFromTabs("FULL", [{ targetId: "AAA" }, { targetId: "FULL" }]);
    expect(res).toEqual({ ok: true, targetId: "FULL" });
  });

  it("resolves unique prefixes (case-insensitive)", () => {
    const res = resolveTargetIdFromTabs("57a01309", [
      { targetId: "57A01309E14B5DEE0FB41F908515A2FC" },
    ]);
    expect(res).toEqual({
      ok: true,
      targetId: "57A01309E14B5DEE0FB41F908515A2FC",
    });
  });

  it("fails on ambiguous prefixes", () => {
    const res = resolveTargetIdFromTabs("57A0", [
      { targetId: "57A01309E14B5DEE0FB41F908515A2FC" },
      { targetId: "57A0BEEF000000000000000000000000" },
    ]);
    expect(res.ok).toBe(false);
    if (!res.ok) {
      expect(res.reason).toBe("ambiguous");
      expect(res.matches?.length).toBe(2);
    }
  });

  it("fails when no tab matches", () => {
    const res = resolveTargetIdFromTabs("NOPE", [{ targetId: "AAA" }]);
    expect(res).toEqual({ ok: false, reason: "not_found" });
  });
});

describe("browser CSRF loopback mutation guard", () => {
  it("rejects mutating methods from non-loopback origin", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        origin: "https://evil.example",
      }),
    ).toBe(true);
  });

  it("allows mutating methods from loopback origin", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        origin: "http://127.0.0.1:18789",
      }),
    ).toBe(false);

    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        origin: "http://localhost:18789",
      }),
    ).toBe(false);
  });

  it("allows mutating methods without origin/referer (non-browser clients)", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
      }),
    ).toBe(false);
  });

  it("rejects mutating methods with origin=null", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        origin: "null",
      }),
    ).toBe(true);
  });

  it("rejects mutating methods from non-loopback referer", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        referer: "https://evil.example/attack",
      }),
    ).toBe(true);
  });

  it("rejects cross-site mutations via Sec-Fetch-Site when present", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "POST",
        secFetchSite: "cross-site",
      }),
    ).toBe(true);
  });

  it("does not reject non-mutating methods", () => {
    expect(
      shouldRejectBrowserMutation({
        method: "GET",
        origin: "https://evil.example",
      }),
    ).toBe(false);

    expect(
      shouldRejectBrowserMutation({
        method: "OPTIONS",
        origin: "https://evil.example",
      }),
    ).toBe(false);
  });
});

describe("cdp.helpers", () => {
  it("preserves query params when appending CDP paths", () => {
    const url = appendCdpPath("https://example.com?token=abc", "/json/version");
    expect(url).toBe("https://example.com/json/version?token=abc");
  });

  it("appends paths under a base prefix", () => {
    const url = appendCdpPath("https://example.com/chrome/?token=abc", "json/list");
    expect(url).toBe("https://example.com/chrome/json/list?token=abc");
  });

  it("adds basic auth headers when credentials are present", () => {
    const headers = getHeadersWithAuth("https://user:pass@example.com");
    expect(headers.Authorization).toBe(`Basic ${Buffer.from("user:pass").toString("base64")}`);
  });

  it("keeps preexisting authorization headers", () => {
    const headers = getHeadersWithAuth("https://user:pass@example.com", {
      Authorization: "Bearer token",
    });
    expect(headers.Authorization).toBe("Bearer token");
  });

  it("does not add relay header for unknown loopback ports", () => {
    const headers = getHeadersWithAuth("http://127.0.0.1:19444/json/version");
    expect(headers["x-openclaw-relay-token"]).toBeUndefined();
  });

  it("adds relay header for known relay ports", async () => {
    const port = await getFreePort();
    const cdpUrl = `http://127.0.0.1:${port}`;
    const prev = process.env.OPENCLAW_GATEWAY_TOKEN;
    process.env.OPENCLAW_GATEWAY_TOKEN = "test-gateway-token";
    try {
      await ensureChromeExtensionRelayServer({ cdpUrl });
      const headers = getHeadersWithAuth(`${cdpUrl}/json/version`);
      expect(headers["x-openclaw-relay-token"]).toBeTruthy();
      expect(headers["x-openclaw-relay-token"]).not.toBe("test-gateway-token");
    } finally {
      await stopChromeExtensionRelayServer({ cdpUrl }).catch(() => {});
      if (prev === undefined) {
        delete process.env.OPENCLAW_GATEWAY_TOKEN;
      } else {
        process.env.OPENCLAW_GATEWAY_TOKEN = prev;
      }
    }
  });
});

describe("fetchBrowserJson loopback auth (bridge auth registry)", () => {
  it("falls back to per-port bridge auth when config auth is not available", async () => {
    const port = 18765;
    const getBridgeAuthForPort = vi.fn((candidate: number) =>
      candidate === port ? { token: "registry-token" } : undefined,
    );
    const init = __test.withLoopbackBrowserAuth(`http://127.0.0.1:${port}/`, undefined, {
      loadConfig: () => ({}),
      resolveBrowserControlAuth: () => ({}),
      getBridgeAuthForPort,
    });
    const headers = new Headers(init.headers ?? {});
    expect(headers.get("authorization")).toBe("Bearer registry-token");
    expect(getBridgeAuthForPort).toHaveBeenCalledWith(port);
  });
});

describe("browser server-context listKnownProfileNames", () => {
  it("includes configured and runtime-only profile names", () => {
    const resolved = resolveBrowserConfig({
      defaultProfile: "openclaw",
      profiles: {
        openclaw: { cdpPort: 18800, color: "#FF4500" },
      },
    });
    const openclaw = resolveProfile(resolved, "openclaw");
    if (!openclaw) {
      throw new Error("expected openclaw profile");
    }

    const state: BrowserServerState = {
      server: null as unknown as BrowserServerState["server"],
      port: 18791,
      resolved,
      profiles: new Map([
        [
          "stale-removed",
          {
            profile: { ...openclaw, name: "stale-removed" },
            running: null,
          },
        ],
      ]),
    };

    expect(listKnownProfileNames(state).toSorted()).toEqual([
      "chrome",
      "openclaw",
      "stale-removed",
    ]);
  });
});
