const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
const patchToString = (fn, name) => {
	if (typeof fn !== "function") return;
	Object_defineProperty(fn, "toString", {
		value: function toString() {
			return makeNativeString(name);
		},
		writable: false,
		configurable: true,
		enumerable: false,
	});
};

// Ensure navigator.webdriver behaves like real Chrome
if (navigator.webdriver !== false && navigator.webdriver !== undefined) {
	const proto = Object_getPrototypeOf(navigator);
	if (proto && "webdriver" in proto) {
		delete proto.webdriver;
	}
}

// Ensure window.chrome exists and is populated with standard Chrome APIs
if (!window.chrome) {
	Object_defineProperty(window, "chrome", {
		writable: true,
		enumerable: true,
		configurable: false,
		value: {},
	});
}

if (window.chrome && !("app" in window.chrome)) {
	const STATIC_DATA = {
		isInstalled: false,
		InstallState: {
			DISABLED: "disabled",
			INSTALLED: "installed",
			NOT_INSTALLED: "not_installed",
		},
		RunningState: {
			CANNOT_RUN: "cannot_run",
			READY_TO_RUN: "ready_to_run",
			RUNNING: "running",
		},
	};

	const makeError = (fn) => new TypeError(`Error in invocation of app.${fn}()`);

	window.chrome.app = {
		...STATIC_DATA,
		get isInstalled() {
			return false;
		},
		getDetails: function getDetails() {
			if (arguments.length) throw makeError("getDetails");
			return null;
		},
		getIsInstalled: function getIsInstalled() {
			if (arguments.length) throw makeError("getIsInstalled");
			return false;
		},
		runningState: function runningState() {
			if (arguments.length) throw makeError("runningState");
			return "cannot_run";
		},
	};

	patchToString(window.chrome.app.getDetails, "getDetails");
	patchToString(window.chrome.app.getIsInstalled, "getIsInstalled");
	patchToString(window.chrome.app.runningState, "runningState");
}

if (window.chrome && !("csi" in window.chrome) && window.performance?.timing) {
	window.chrome.csi = function csi() {
		const { timing } = window.performance;
		return {
			onloadT: timing.domContentLoadedEventEnd,
			startE: timing.navigationStart,
			pageT: Date.now() - timing.navigationStart,
			tran: 15,
		};
	};
	patchToString(window.chrome.csi, "csi");
}

if (
	window.chrome &&
	!("loadTimes" in window.chrome) &&
	window.performance?.timing &&
	window.PerformancePaintTiming
) {
	const { performance } = window;
	const ntEntryFallback = {
		nextHopProtocol: "h2",
		type: "other",
	};

	const protocolInfo = {
		get connectionInfo() {
			const ntEntry = performance.getEntriesByType("navigation")[0] || ntEntryFallback;
			return ntEntry.nextHopProtocol;
		},
		get npnNegotiatedProtocol() {
			const ntEntry = performance.getEntriesByType("navigation")[0] || ntEntryFallback;
			return ["h2", "hq"].includes(ntEntry.nextHopProtocol) ? ntEntry.nextHopProtocol : "unknown";
		},
		get navigationType() {
			const ntEntry = performance.getEntriesByType("navigation")[0] || ntEntryFallback;
			return ntEntry.type;
		},
		get wasAlternateProtocolAvailable() {
			return false;
		},
		get wasFetchedViaSpdy() {
			const ntEntry = performance.getEntriesByType("navigation")[0] || ntEntryFallback;
			return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
		},
		get wasNpnNegotiated() {
			const ntEntry = performance.getEntriesByType("navigation")[0] || ntEntryFallback;
			return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
		},
	};

	const { timing } = window.performance;

	const toFixed = (num, fixed) => {
		const re = new RegExp("^-?\\d+(?:.\\d{0," + (fixed || -1) + "})?");
		return num.toString().match(re)[0];
	};

	const timingInfo = {
		get firstPaintAfterLoadTime() {
			return 0;
		},
		get requestTime() {
			return timing.navigationStart / 1000;
		},
		get startLoadTime() {
			return timing.navigationStart / 1000;
		},
		get commitLoadTime() {
			return timing.responseStart / 1000;
		},
		get finishDocumentLoadTime() {
			return timing.domContentLoadedEventEnd / 1000;
		},
		get finishLoadTime() {
			return timing.loadEventEnd / 1000;
		},
		get firstPaintTime() {
			const fpEntry = performance.getEntriesByType("paint")[0] || {
				startTime: timing.loadEventEnd / 1000,
			};
			return toFixed((fpEntry.startTime + performance.timeOrigin) / 1000, 3);
		},
	};

	window.chrome.loadTimes = function loadTimes() {
		return { ...protocolInfo, ...timingInfo };
	};
	patchToString(window.chrome.loadTimes, "loadTimes");
}

const isSecureOrigin = document.location.protocol.startsWith("https");
if (window.chrome && !("runtime" in window.chrome) && isSecureOrigin) {
	const STATIC_DATA = {
		OnInstalledReason: {
			CHROME_UPDATE: "chrome_update",
			INSTALL: "install",
			SHARED_MODULE_UPDATE: "shared_module_update",
			UPDATE: "update",
		},
		OnRestartRequiredReason: {
			APP_UPDATE: "app_update",
			OS_UPDATE: "os_update",
			PERIODIC: "periodic",
		},
		PlatformArch: {
			ARM: "arm",
			ARM64: "arm64",
			MIPS: "mips",
			MIPS64: "mips64",
			X86_32: "x86-32",
			X86_64: "x86-64",
		},
		PlatformNaclArch: {
			ARM: "arm",
			MIPS: "mips",
			MIPS64: "mips64",
			X86_32: "x86-32",
			X86_64: "x86-64",
		},
		PlatformOs: {
			ANDROID: "android",
			CROS: "cros",
			LINUX: "linux",
			MAC: "mac",
			OPENBSD: "openbsd",
			WIN: "win",
		},
		RequestUpdateCheckStatus: {
			NO_UPDATE: "no_update",
			THROTTLED: "throttled",
			UPDATE_AVAILABLE: "update_available",
		},
	};

	const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
		NoMatchingSignature: new TypeError(preamble + "No matching signature."),
		MustSpecifyExtensionID: new TypeError(
			preamble +
				`${method} called from a webpage must specify an Extension ID (string) for its first argument.`,
		),
		InvalidExtensionID: new TypeError(preamble + `Invalid extension id: '${extensionId}'`),
	});

	const isValidExtensionId = (value) => value.length === 32 && value.toLowerCase().match(/^[a-p]+$/);

	const sendMessageHandler = {
		apply(target, _ctx, args) {
			const [extensionId, options, responseCallback] = args || [];
			const errorPreamble =
				"Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ";
			const Errors = makeCustomRuntimeErrors(errorPreamble, "chrome.runtime.sendMessage()", extensionId);

			const noArguments = args.length === 0;
			const tooManyArguments = args.length > 4;
			const incorrectOptions = options && typeof options !== "object";
			const incorrectResponseCallback = responseCallback && typeof responseCallback !== "function";

			if (noArguments || tooManyArguments || incorrectOptions || incorrectResponseCallback) {
				throw Errors.NoMatchingSignature;
			}

			if (args.length < 2) {
				throw Errors.MustSpecifyExtensionID;
			}

			if (typeof extensionId !== "string") {
				throw Errors.NoMatchingSignature;
			}

			if (!isValidExtensionId(extensionId)) {
				throw Errors.InvalidExtensionID;
			}

			return undefined;
		},
	};

	const connectHandler = {
		apply(target, _ctx, args) {
			const [extensionId, connectInfo] = args || [];
			const errorPreamble =
				"Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ";
			const Errors = makeCustomRuntimeErrors(errorPreamble, "chrome.runtime.connect()", extensionId);

			const noArguments = args.length === 0;
			const emptyStringArgument = args.length === 1 && extensionId === "";
			if (noArguments || emptyStringArgument) {
				throw Errors.MustSpecifyExtensionID;
			}

			const tooManyArguments = args.length > 2;
			const incorrectConnectInfoType = connectInfo && typeof connectInfo !== "object";
			if (tooManyArguments || incorrectConnectInfoType) {
				throw Errors.NoMatchingSignature;
			}

			const extensionIdIsString = typeof extensionId === "string";
			if (extensionIdIsString && extensionId === "") {
				throw Errors.MustSpecifyExtensionID;
			}
			if (extensionIdIsString && !isValidExtensionId(extensionId)) {
				throw Errors.InvalidExtensionID;
			}

			const validateConnectInfo = (info) => {
				if (args.length > 1) {
					throw Errors.NoMatchingSignature;
				}
				if (Object_keys(info).length === 0) {
					throw Errors.MustSpecifyExtensionID;
				}
				Object_entries(info).forEach(([key, value]) => {
					const isExpected = ["name", "includeTlsChannelId"].includes(key);
					if (!isExpected) {
						throw new TypeError(errorPreamble + `Unexpected property: '${key}'.`);
					}
					const mismatch = (propName, expected, found) =>
						TypeError(
							errorPreamble +
								`Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`,
						);
					if (key === "name" && typeof value !== "string") {
						throw mismatch(key, "string", typeof value);
					}
					if (key === "includeTlsChannelId" && typeof value !== "boolean") {
						throw mismatch(key, "boolean", typeof value);
					}
				});
			};

			if (typeof extensionId === "object") {
				validateConnectInfo(extensionId);
				throw Errors.MustSpecifyExtensionID;
			}

			const makeConnectResponse = () => {
				const onSomething = () => ({
					addListener: function addListener() {},
					dispatch: function dispatch() {},
					hasListener: function hasListener() {},
					hasListeners: function hasListeners() {
						return false;
					},
					removeListener: function removeListener() {},
				});

				return {
					name: "",
					sender: undefined,
					disconnect: function disconnect() {},
					onDisconnect: onSomething(),
					onMessage: onSomething(),
					postMessage: function postMessage() {
						if (!arguments.length) {
							throw new TypeError("Insufficient number of arguments.");
						}
						throw new Error("Attempting to use a disconnected port object");
					},
				};
			};

			return makeConnectResponse();
		},
	};

	window.chrome.runtime = {
		...STATIC_DATA,
		get id() {
			return undefined;
		},
		connect: null,
		sendMessage: null,
	};

	const sendMessageProxy = new Window_Proxy(function sendMessage() {}, sendMessageHandler);
	const connectProxy = new Window_Proxy(function connect() {}, connectHandler);
	patchToString(sendMessageProxy, "sendMessage");
	patchToString(connectProxy, "connect");
	window.chrome.runtime.sendMessage = sendMessageProxy;
	window.chrome.runtime.connect = connectProxy;
}

// Suppress Permission.query for automation-controlled
if (navigator.permissions?.query) {
	if (isSecureOrigin && "Notification" in window) {
		Object_defineProperty(Notification, "permission", {
			get: () => "default",
			configurable: true,
		});
	} else if (!isSecureOrigin) {
		const originalQuery = navigator.permissions.query;
		navigator.permissions.query = function (parameters) {
			if (parameters?.name === "notifications") {
			const status = { state: "denied", onchange: null };
			if (typeof PermissionStatus !== "undefined") {
				Object_setPrototypeOf(status, PermissionStatus.prototype);
			}
			return Promise_resolve(status);
			}
			return originalQuery.call(this, parameters);
		};
	}
}

// Remove CDC_ markers from document
const documentProps = Object_getOwnPropertyNames(document);
for (const prop of documentProps) {
	if (prop.startsWith("cdc_")) {
		delete document[prop];
	}
}
