// A `go test -exec` wrapper for GOOS=js GOARCH=wasm that installs a minimal DOM
// before starting the Go runtime, so the reconciler can be driven headlessly
// under node instead of only in a browser. From kjol/go:
//
//	GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime
//
// The subtlety that matters: in a real browser, setting .innerHTML *parses* the
// markup into real child nodes, so a later appendChild lands after it. The shim
// models that (one opaque "#raw" child) rather than keeping innerHTML as a
// detached string — treating it as a string would hide the very class of bug
// these tests exist to catch (stale raw markup surviving a diff).
"use strict";

const { execSync } = require("child_process");

class CSSStyle {
	constructor() { this.props = {}; }
	setProperty(k, v) { this.props[k] = String(v); }
	removeProperty(k) { delete this.props[k]; }
	getPropertyValue(k) { return this.props[k] ?? ""; }
	get cssText() {
		return Object.keys(this.props).sort().map((k) => `${k}: ${this.props[k]}`).join("; ");
	}
}

const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SVG_NS = "http://www.w3.org/2000/svg";

class DNode {
	constructor(tag, ns = XHTML_NS) {
		this.tag = tag;
		// An element's namespace is fixed at creation. createElement() always yields
		// HTML — which is why an <svg> built that way is inert; see the reconciler.
		this.namespaceURI = ns;
		this.childNodes = [];
		this.attrs = {};
		this.parentNode = null;
		this.listeners = {};
		this.nodeValue = null;
		this.rawHTML = null;
		this.style = new CSSStyle();
		// Tests set .rect to control what getBoundingClientRect reports; there is no
		// layout engine here, so geometry is whatever the test declares.
		this.rect = { left: 0, top: 0, width: 0, height: 0 };
		// Likewise for overflow. clientWidth is the visible content box, scrollWidth the
		// full content — content overflows exactly when the second exceeds the first, and
		// a test declares both rather than a layout engine deriving them.
		this.clientWidth = 0;
		this.scrollWidth = 0;
	}
	get nodeType() { return this.tag === "#text" ? 3 : 1; }
	get firstChild() { return this.childNodes[0] ?? null; }
	get parentElement() { return this.parentNode; }
	getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
	hasAttribute(k) { return k in this.attrs; }
	getBoundingClientRect() {
		const r = this.rect;
		return { left: r.left, top: r.top, width: r.width, height: r.height, right: r.left + r.width, bottom: r.top + r.height };
	}
	contains(other) {
		for (let n = other; n; n = n.parentNode) if (n === this) return true;
		return false;
	}
	// Only the attribute-presence selectors the runtime actually uses, e.g.
	// "[data-floating-id]".
	closest(selector) {
		const m = /^\[([a-zA-Z0-9-]+)\]$/.exec(selector);
		if (!m) throw new Error(`domexec shim: unsupported selector ${selector}`);
		for (let n = this; n; n = n.parentNode) if (n.nodeType === 1 && n.hasAttribute(m[1])) return n;
		return null;
	}
	appendChild(c) { c.parentNode = this; this.childNodes.push(c); return c; }
	removeChild(c) {
		const i = this.childNodes.indexOf(c);
		if (i < 0) throw new Error("removeChild: node is not a child");
		this.childNodes.splice(i, 1);
		c.parentNode = null;
		return c;
	}
	replaceChild(next, old) {
		const i = this.childNodes.indexOf(old);
		if (i < 0) throw new Error("replaceChild: node is not a child");
		this.childNodes[i] = next;
		next.parentNode = this;
		old.parentNode = null;
		return old;
	}
	setAttribute(k, v) { this.attrs[k] = String(v); }
	removeAttribute(k) { delete this.attrs[k]; }
	addEventListener(name, fn) { (this.listeners[name] ??= []).push(fn); }
	removeEventListener(name, fn) {
		const l = this.listeners[name] ?? [];
		const i = l.indexOf(fn);
		if (i >= 0) l.splice(i, 1);
	}
	set innerHTML(html) {
		for (const c of this.childNodes) c.parentNode = null;
		this.childNodes = [];
		if (html !== "") {
			const raw = new DNode("#raw");
			raw.rawHTML = html;
			raw.parentNode = this;
			this.childNodes.push(raw);
		}
	}
	get innerHTML() { return this.childNodes.map(serialize).join(""); }
	get outerHTML() { return serialize(this); }
}

function serialize(n) {
	if (n.tag === "#text") return n.nodeValue ?? "";
	if (n.tag === "#raw") return n.rawHTML ?? "";
	const attrs = Object.keys(n.attrs).sort().map((k) => ` ${k}="${n.attrs[k]}"`).join("");
	const style = n.style.cssText ? ` style="${n.style.cssText}"` : "";
	return `<${n.tag}${attrs}${style}>${n.childNodes.map(serialize).join("")}</${n.tag}>`;
}

const body = new DNode("body");
const documentElement = new DNode("html");

globalThis.document = {
	body,
	documentElement,
	createElement: (tag) => new DNode(tag, XHTML_NS),
	createElementNS: (ns, tag) => new DNode(tag, ns),
	createTextNode: (text) => { const n = new DNode("#text"); n.nodeValue = text; return n; },
	getElementById: () => null,
	querySelector: () => null,
	addEventListener: () => {},
	removeEventListener: () => {},
};

// Viewport size the floating engine collides against. Tests override these.
globalThis.innerWidth = 1024;
globalThis.innerHeight = 768;
globalThis.addEventListener ??= () => {};
globalThis.removeEventListener ??= () => {};
globalThis.requestAnimationFrame = (fn) => setTimeout(() => fn(0), 0);
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
globalThis.getComputedStyle = (el) => ({
	getPropertyValue: (k) => el.style.getPropertyValue(k),
	fontSize: "16px",
});

// ---- go_js_wasm_exec boilerplate ----
globalThis.require = require;
globalThis.fs = require("fs");
globalThis.TextEncoder = require("util").TextEncoder;
globalThis.TextDecoder = require("util").TextDecoder;
globalThis.performance ??= require("performance");
globalThis.crypto ??= require("crypto");

// wasm_exec.js moved from misc/wasm to lib/wasm in Go 1.24.
const goroot = execSync("go env GOROOT").toString().trim();
try {
	require(goroot + "/lib/wasm/wasm_exec.js");
} catch {
	require(goroot + "/misc/wasm/wasm_exec.js");
}

const go = new Go();
go.argv = process.argv.slice(2);
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
	process.on("exit", (code) => {
		if (code === 0 && !go.exited) {
			go._pendingEvent = { id: 0 };
			go._resume();
		}
	});
	return go.run(result.instance);
}).catch((err) => {
	console.error(err);
	process.exit(1);
});
