// @ts-check
/// <reference path="../node_modules/@types/jest/index.d.ts" />

const { start, makeNodeDef, checkBeforeAndAfterReload, assertNotNullOrUndefined } = require("../utils");
const lg = require("../utils/litegraph");

/**
 * @typedef { import("../utils/ezgraph") } Ez
 * @typedef { ReturnType<Ez["Ez"]["graph"]>["ez"] } EzNodeFactory
 */

/**
 * @param { EzNodeFactory } ez
 * @param { InstanceType<Ez["EzGraph"]> } graph
 * @param { InstanceType<Ez["EzInput"]> } input
 * @param { string } widgetType
 * @param { number } controlWidgetCount
 * @returns
 */
async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) {
	// Connect to primitive and ensure its still connected after
	let primitive = ez.PrimitiveNode();
	primitive.outputs[0].connectTo(input);

	await checkBeforeAndAfterReload(graph, async () => {
		primitive = graph.find(primitive);
		let { connections } = primitive.outputs[0];
		expect(connections).toHaveLength(1);
		expect(connections[0].targetNode.id).toBe(input.node.node.id);

		// Ensure widget is correct type
		const valueWidget = primitive.widgets.value;
		expect(valueWidget.widget.type).toBe(widgetType);

		// Check if control_after_generate should be added
		if (controlWidgetCount) {
			const controlWidget = primitive.widgets.control_after_generate;
			expect(controlWidget.widget.type).toBe("combo");
			if(widgetType === "combo") {
				const filterWidget = primitive.widgets.control_filter_list;
				expect(filterWidget.widget.type).toBe("string");
			}
		}

		// Ensure we dont have other widgets
		expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount);
	});

	return primitive;
}

describe("widget inputs", () => {
	beforeEach(() => {
		lg.setup(global);
	});

	afterEach(() => {
		lg.teardown(global);
	});

	[
		{ name: "int", type: "INT", widget: "number", control: 1 },
		{ name: "float", type: "FLOAT", widget: "number", control: 1 },
		{ name: "text", type: "STRING" },
		{
			name: "customtext",
			type: "STRING",
			opt: { multiline: true },
		},
		{ name: "toggle", type: "BOOLEAN" },
		{ name: "combo", type: ["a", "b", "c"], control: 2 },
	].forEach((c) => {
		test(`widget conversion + primitive works on ${c.name}`, async () => {
			const { ez, graph } = await start({
				mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }),
			});

			// Create test node and convert to input
			const n = ez.TestNode();
			const w = n.widgets[c.name];
			w.convertToInput();
			expect(w.isConvertedToInput).toBeTruthy();
			const input = w.getConvertedInput();
			expect(input).toBeTruthy();

			// @ts-ignore : input is valid here
			await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
		});
	});

	test("converted widget works after reload", async () => {
		const { ez, graph } = await start();
		let n = ez.CheckpointLoaderSimple();

		const inputCount = n.inputs.length;

		// Convert ckpt name to an input
		n.widgets.ckpt_name.convertToInput();
		expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
		expect(n.inputs.ckpt_name).toBeTruthy();
		expect(n.inputs.length).toEqual(inputCount + 1);

		// Convert back to widget and ensure input is removed
		n.widgets.ckpt_name.convertToWidget();
		expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
		expect(n.inputs.ckpt_name).toBeFalsy();
		expect(n.inputs.length).toEqual(inputCount);

		// Convert again and reload the graph to ensure it maintains state
		n.widgets.ckpt_name.convertToInput();
		expect(n.inputs.length).toEqual(inputCount + 1);

		const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2);

		// Disconnect & reconnect
		primitive.outputs[0].connections[0].disconnect();
		let { connections } = primitive.outputs[0];
		expect(connections).toHaveLength(0);

		primitive.outputs[0].connectTo(n.inputs.ckpt_name);
		({ connections } = primitive.outputs[0]);
		expect(connections).toHaveLength(1);
		expect(connections[0].targetNode.id).toBe(n.node.id);

		// Convert back to widget and ensure input is removed
		n.widgets.ckpt_name.convertToWidget();
		expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
		expect(n.inputs.ckpt_name).toBeFalsy();
		expect(n.inputs.length).toEqual(inputCount);
	});

	test("converted widget works on clone", async () => {
		const { graph, ez } = await start();
		let n = ez.CheckpointLoaderSimple();

		// Convert the widget to an input
		n.widgets.ckpt_name.convertToInput();
		expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();

		// Clone the node
		n.menu["Clone"].call();
		expect(graph.nodes).toHaveLength(2);
		const clone = graph.nodes[1];
		expect(clone.id).not.toEqual(n.id);

		// Ensure the clone has an input
		expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
		expect(clone.inputs.ckpt_name).toBeTruthy();

		// Ensure primitive connects to both nodes
		let primitive = ez.PrimitiveNode();
		primitive.outputs[0].connectTo(n.inputs.ckpt_name);
		primitive.outputs[0].connectTo(clone.inputs.ckpt_name);
		expect(primitive.outputs[0].connections).toHaveLength(2);

		// Convert back to widget and ensure input is removed
		clone.widgets.ckpt_name.convertToWidget();
		expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
		expect(clone.inputs.ckpt_name).toBeFalsy();
	});

	test("shows missing node error on custom node with converted input", async () => {
		const { graph } = await start();

		const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");

		await graph.app.loadGraphData({
			last_node_id: 3,
			last_link_id: 4,
			nodes: [
				{
					id: 1,
					type: "TestNode",
					pos: [41.87329101561909, 389.7381480823742],
					size: { 0: 220, 1: 374 },
					flags: {},
					order: 1,
					mode: 0,
					inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }],
					outputs: [],
					properties: { "Node name for S&R": "TestNode" },
					widgets_values: [1],
				},
				{
					id: 3,
					type: "PrimitiveNode",
					pos: [-312, 433],
					size: { 0: 210, 1: 82 },
					flags: {},
					order: 0,
					mode: 0,
					outputs: [{ links: [4], widget: { name: "test" } }],
					title: "test",
					properties: {},
				},
			],
			links: [[4, 3, 0, 1, 6, "FLOAT"]],
			groups: [],
			config: {},
			extra: {},
			version: 0.4,
		});

		expect(dialogShow).toBeCalledTimes(1);
		expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
		expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
	});

	test("defaultInput widgets can be converted back to inputs", async () => {
		const { graph, ez } = await start({
			mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }),
		});

		// Create test node and ensure it starts as an input
		let n = ez.TestNode();
		let w = n.widgets.example;
		expect(w.isConvertedToInput).toBeTruthy();
		let input = w.getConvertedInput();
		expect(input).toBeTruthy();

		// Ensure it can be converted to
		w.convertToWidget();
		expect(w.isConvertedToInput).toBeFalsy();
		expect(n.inputs.length).toEqual(0);
		// and from
		w.convertToInput();
		expect(w.isConvertedToInput).toBeTruthy();
		input = w.getConvertedInput();

		// Reload and ensure it still only has 1 converted widget
		if (!assertNotNullOrUndefined(input)) return;

		await connectPrimitiveAndReload(ez, graph, input, "number", 1);
		n = graph.find(n);
		expect(n.widgets).toHaveLength(1);
		w = n.widgets.example;
		expect(w.isConvertedToInput).toBeTruthy();

		// Convert back to widget and ensure it is still a widget after reload
		w.convertToWidget();
		await graph.reload();
		n = graph.find(n);
		expect(n.widgets).toHaveLength(1);
		expect(n.widgets[0].isConvertedToInput).toBeFalsy();
		expect(n.inputs.length).toEqual(0);
	});

	test("forceInput widgets can not be converted back to inputs", async () => {
		const { graph, ez } = await start({
			mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }),
		});

		// Create test node and ensure it starts as an input
		let n = ez.TestNode();
		let w = n.widgets.example;
		expect(w.isConvertedToInput).toBeTruthy();
		const input = w.getConvertedInput();
		expect(input).toBeTruthy();

		// Convert to widget should error
		expect(() => w.convertToWidget()).toThrow();

		// Reload and ensure it still only has 1 converted widget
		if (assertNotNullOrUndefined(input)) {
			await connectPrimitiveAndReload(ez, graph, input, "number", 1);
			n = graph.find(n);
			expect(n.widgets).toHaveLength(1);
			expect(n.widgets.example.isConvertedToInput).toBeTruthy();
		}
	});

	test("primitive can connect to matching combos on converted widgets", async () => {
		const { ez } = await start({
			mockNodeDefs: {
				...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
				...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }),
			},
		});

		const n1 = ez.TestNode1();
		const n2 = ez.TestNode2();
		const p = ez.PrimitiveNode();
		p.outputs[0].connectTo(n1.inputs[0]);
		p.outputs[0].connectTo(n2.inputs[0]);
		expect(p.outputs[0].connections).toHaveLength(2);
		const valueWidget = p.widgets.value;
		expect(valueWidget.widget.type).toBe("combo");
		expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]);
	});

	test("primitive can not connect to non matching combos on converted widgets", async () => {
		const { ez } = await start({
			mockNodeDefs: {
				...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
				...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
			},
		});

		const n1 = ez.TestNode1();
		const n2 = ez.TestNode2();
		const p = ez.PrimitiveNode();
		p.outputs[0].connectTo(n1.inputs[0]);
		expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow();
		expect(p.outputs[0].connections).toHaveLength(1);
	});

	test("combo output can not connect to non matching combos list input", async () => {
		const { ez } = await start({
			mockNodeDefs: {
				...makeNodeDef("TestNode1", {}, [["A", "B"]]),
				...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true}] }),
				...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true}] }),
			},
		});

		const n1 = ez.TestNode1();
		const n2 = ez.TestNode2();
		const n3 = ez.TestNode3();

		n1.outputs[0].connectTo(n2.inputs[0]);
		expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow();
	});

	test("combo primitive can filter list when control_after_generate called", async () => {
		const { ez } = await start({
			mockNodeDefs: {
				...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }),
			},
		});

		const n1 = ez.TestNode1();
		n1.widgets.example.convertToInput();
		const p = ez.PrimitiveNode()
		p.outputs[0].connectTo(n1.inputs[0]);

		const value = p.widgets.value;
		const control = p.widgets.control_after_generate.widget;
		const filter = p.widgets.control_filter_list;

		expect(p.widgets.length).toBe(3);
		control.value = "increment";
		expect(value.value).toBe("A");

		// Manually trigger after queue when set to increment
		control["afterQueued"]();
		expect(value.value).toBe("B");

		// Filter to items containing D
		filter.value = "D";
		control["afterQueued"]();
		expect(value.value).toBe("D");
		control["afterQueued"]();
		expect(value.value).toBe("DD");

		// Check decrement
		value.value = "BBB";
		control.value = "decrement";
		filter.value = "B";
		control["afterQueued"]();
		expect(value.value).toBe("BB");
		control["afterQueued"]();
		expect(value.value).toBe("B");

		// Check regex works
		value.value = "BBB";
		filter.value = "/[AB]|^C$/";
		control["afterQueued"]();
		expect(value.value).toBe("AAA");
		control["afterQueued"]();
		expect(value.value).toBe("BB");
		control["afterQueued"]();
		expect(value.value).toBe("AA");
		control["afterQueued"]();
		expect(value.value).toBe("C");
		control["afterQueued"]();
		expect(value.value).toBe("B");
		control["afterQueued"]();
		expect(value.value).toBe("A");

		// Check random
		control.value = "randomize";
		filter.value = "/D/";
		for(let i = 0; i < 100; i++) {
			control["afterQueued"]();
			expect(value.value === "D" || value.value === "DD").toBeTruthy();
		}

		// Ensure it doesnt apply when fixed
		control.value = "fixed";
		value.value = "B";
		filter.value = "C";
		control["afterQueued"]();
		expect(value.value).toBe("B");
	});
});
