// @vitest-environment happy-dom

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

import {
    findWorkflowNode,
    getNodeDisplayName,
    getNodeInputSlotNames,
    getNodeParamEntries,
    getNodeTypeLabel,
    getNodeWidgetValueEntries,
    getWorkflowNodes,
    resolveAssetWorkflow,
} from "../features/viewer/workflowGraphMap/workflowGraphMapData.js";
import {
    expandSubgraphsForMinimap,
    synthesizeWorkflowFromPromptGraph,
} from "../components/sidebar/utils/minimap.js";

describe("workflow graph map data", () => {
    it("maps widgets_values to Comfy widget inputs without connected socket offset", () => {
        const node = {
            id: 104,
            type: "WanMoekSamplerAdvanced",
            inputs: [
                { name: "model_high_noise", link: 1 },
                { name: "model_low_noise", link: 2 },
                { name: "positive", link: 3 },
                { name: "negative", link: 4 },
                { name: "latent_image", link: 5 },
                { name: "boundary", widget: true },
                { name: "add_noise", widget: true },
                { name: "noise_seed", widget: true },
                { name: "control_after_generate", widget: true },
                { name: "steps", widget: true },
            ],
            widgets_values: [0.9, "enable", 131289120546304, "randomize", 20],
        };

        expect(getNodeInputSlotNames(node)).toEqual([
            "boundary",
            "add_noise",
            "noise_seed",
            "control_after_generate",
            "steps",
        ]);
        expect(getNodeParamEntries(node).slice(0, 5)).toEqual([
            ["boundary", 0.9],
            ["add_noise", "enable"],
            ["noise_seed", 131289120546304],
            ["control_after_generate", "randomize"],
            ["steps", 20],
        ]);
    });

    it("uses Comfy widget object metadata and skips linked non-widget inputs", () => {
        const node = {
            id: 7,
            type: "CustomSampler",
            inputs: [
                { name: "model", type: "MODEL", link: 1 },
                { name: "latent", type: "LATENT", link: 2 },
                { label: "scheduler mode", name: "scheduler", type: "COMBO", widget: { name: "scheduler" } },
                { label: "custom strength", name: "strength", type: "FLOAT", widget: { name: "strength" } },
            ],
            widgets_values: ["karras", 0.75],
        };

        expect(getNodeInputSlotNames(node)).toEqual(["scheduler mode", "custom strength"]);
        expect(getNodeWidgetValueEntries(node)).toEqual([
            { label: "scheduler mode", value: "karras", index: 0 },
            { label: "custom strength", value: 0.75, index: 1 },
        ]);
    });

    it("keeps linked widget labels and trailing unlinked widget inputs in widgets_values order", () => {
        const node = {
            id: 120,
            type: "WanFirstLastFrameToVideo",
            inputs: [
                { name: "positive", type: "CONDITIONING", link: 20 },
                { name: "negative", type: "CONDITIONING", link: 21 },
                { name: "vae", type: "VAE", link: 22 },
                { name: "width", type: "INT", link: 23, widget: { name: "width" } },
                { name: "height", type: "INT", link: 24, widget: { name: "height" } },
                { name: "length", type: "INT", link: 25, widget: { name: "length" } },
                { name: "batch_size", type: "INT", widget: { name: "batch_size" } },
            ],
            widgets_values: [832, 480, 81, 1],
        };

        expect(getNodeInputSlotNames(node)).toEqual(["width", "height", "length", "batch_size"]);
        expect(getNodeParamEntries(node).slice(0, 4)).toEqual([
            ["width", 832],
            ["height", 480],
            ["length", 81],
            ["batch_size", 1],
        ]);
    });

    it("does not infer unmarked socket inputs as widgets for core sampler nodes", () => {
        const node = {
            id: 3,
            type: "KSampler",
            inputs: [
                { name: "model", type: "MODEL", link: 1 },
                { name: "positive", type: "CONDITIONING", link: 2 },
                { name: "negative", type: "CONDITIONING", link: 3 },
                { name: "latent_image", type: "LATENT", link: 4 },
                { name: "seed", type: "INT" },
                { name: "steps", type: "INT" },
                { name: "cfg", type: "FLOAT" },
                { name: "sampler_name", type: "COMBO" },
                { name: "scheduler", type: "COMBO" },
                { name: "denoise", type: "FLOAT" },
            ],
            widgets_values: [1088132929177955, "randomize", 20, 8, "euler", "simple", 1],
        };

        expect(getNodeInputSlotNames(node)).toEqual([]);
        expect(getNodeWidgetValueEntries(node)).toEqual([
            { label: "seed", value: 1088132929177955, index: 0 },
            { label: "control_after_generate", value: "randomize", index: 1 },
            { label: "steps", value: 20, index: 2 },
            { label: "cfg", value: 8, index: 3 },
            { label: "sampler_name", value: "euler", index: 4 },
            { label: "scheduler", value: "simple", index: 5 },
            { label: "denoise", value: 1, index: 6 },
        ]);
        expect(getNodeParamEntries(node)).toEqual([
            ["seed", 1088132929177955],
            ["control_after_generate", "randomize"],
            ["steps", 20],
            ["cfg", 8],
            ["sampler_name", "euler"],
            ["scheduler", "simple"],
            ["denoise", 1],
        ]);
    });

    it("uses prompt inputs for subgraph sampler parameters when widgets_values are ambiguous", () => {
        const workflow = resolveAssetWorkflow({
            workflow: {
                nodes: [
                    {
                        id: 95,
                        type: "c20beb1e-2a45-4872-913e-21018c09c578",
                        properties: {
                            proxyWidgets: [
                                ["3", "seed"],
                                ["3", "steps"],
                            ],
                        },
                    },
                ],
                definitions: {
                    subgraphs: [
                        {
                            id: "c20beb1e-2a45-4872-913e-21018c09c578",
                            nodes: [
                                {
                                    id: 3,
                                    type: "KSampler",
                                    inputs: [
                                        { name: "model", type: "MODEL", link: 13 },
                                        { name: "positive", type: "CONDITIONING", link: 30 },
                                        { name: "negative", type: "CONDITIONING", link: 33 },
                                        { name: "latent_image", type: "LATENT", link: 427 },
                                        { name: "seed", type: "INT", link: 71, widget: { name: "seed" } },
                                        { name: "steps", type: "INT", link: 72, widget: { name: "steps" } },
                                    ],
                                    widgets_values: [
                                        1088132929177955,
                                        "randomize",
                                        8,
                                        1,
                                        "res_multistep",
                                        "simple",
                                        1,
                                    ],
                                },
                            ],
                        },
                    ],
                },
            },
            prompt: {
                "95:3": {
                    class_type: "KSampler",
                    inputs: {
                        seed: 1088132929177955,
                        steps: 8,
                        cfg: 1,
                        sampler_name: "res_multistep",
                        scheduler: "simple",
                        denoise: 1,
                        model: ["95:11", 0],
                        positive: ["95:27", 0],
                        negative: ["95:33", 0],
                        latent_image: ["95:242", 0],
                    },
                },
            },
        });
        const sampler = findWorkflowNode(workflow, "95::3");

        expect(getNodeParamEntries(sampler)).toEqual([
            ["seed", 1088132929177955],
            ["steps", 8],
            ["cfg", 1],
            ["sampler_name", "res_multistep"],
            ["scheduler", "simple"],
            ["denoise", 1],
        ]);
    });

    it("supports custom nodes that save widgets_values as a dictionary", () => {
        expect(
            getNodeWidgetValueEntries({
                type: "CustomNode",
                widgets_values: {
                    mode: "fast",
                    amount: 12,
                },
            }),
        ).toEqual([
            { label: "mode", value: "fast", index: 0 },
            { label: "amount", value: 12, index: 1 },
        ]);
    });

    it("extracts primitive and object params from synthesized prompt graphs", () => {
        const workflow = resolveAssetWorkflow({
            prompt: {
                85: {
                    class_type: "Power Lora Loader (rgthree)",
                    _meta: { title: "Power Lora Loader" },
                    inputs: {
                        model: ["140", 0],
                        clip: ["83", 0],
                        text: "cinematic portrait",
                        lora_2: {
                            on: true,
                            lora: "detailer.safetensors",
                            strength: 0.5,
                        },
                    },
                },
            },
        });

        const node = workflow.nodes.find((item) => String(item.id) === "85");
        expect(node.title).toBe("Power Lora Loader");
        expect(getNodeParamEntries(node)).toEqual([
            ["text", "cinematic portrait"],
            [
                "lora_2",
                {
                    on: true,
                    lora: "detailer.safetensors",
                    strength: 0.5,
                },
            ],
        ]);
    });

    it("treats colon-prefixed subgraph prompt references as links", () => {
        const prompt = {
            75: {
                inputs: {
                    filename_prefix: "video/LTX_2.3_i2v",
                    video: ["320:310", 0],
                },
                class_type: "SaveVideo",
                _meta: { title: "Save Video" },
            },
            "320:310": {
                inputs: {
                    fps: ["320:298", 0],
                    images: ["320:315", 0],
                    audio: ["320:297", 0],
                },
                class_type: "CreateVideo",
                _meta: { title: "Create Video" },
            },
            "320:298": {
                inputs: { expression: "a", "values.a": ["320:300", 0] },
                class_type: "ComfyMathExpression",
            },
            "320:300": {
                inputs: { value: 25 },
                class_type: "PrimitiveInt",
            },
        };

        const workflow = synthesizeWorkflowFromPromptGraph(prompt);
        const saveNode = workflow.nodes.find((node) => String(node.id) === "75");
        const createNode = workflow.nodes.find((node) => String(node.id) === "320:310");

        expect(workflow.links).toEqual(
            expect.arrayContaining([
                expect.arrayContaining(["320:310", 75]),
                expect.arrayContaining(["320:298", "320:310"]),
            ]),
        );
        expect(getNodeParamEntries(saveNode)).toEqual([["filename_prefix", "video/LTX_2.3_i2v"]]);
        expect(getNodeParamEntries(createNode)).toEqual([]);
    });

    it("uses readable names for opaque subgraph node types", () => {
        const workflow = resolveAssetWorkflow({
            workflow: {
                nodes: [
                    {
                        id: 20,
                        type: "12345678-1234-1234-1234-123456789abc",
                        properties: {},
                    },
                ],
                definitions: {
                    subgraphs: [
                        {
                            id: "12345678-1234-1234-1234-123456789abc",
                            name: "Face Detailer",
                            nodes: [],
                        },
                    ],
                },
            },
        });

        const node = workflow.nodes[0];
        expect(node.properties.subgraph_name).toBe("Face Detailer");
        expect(getNodeDisplayName(node)).toBe("Face Detailer");
        expect(getNodeTypeLabel(node)).toBe("Subgraph");
    });

    it("resolves official workflow template wrappers", () => {
        const workflow = resolveAssetWorkflow({
            template: {
                nodes: [
                    {
                        id: 11,
                        type: "CLIPTextEncode",
                        title: "Positive Prompt",
                        inputs: [{ name: "text", type: "STRING", widget: { name: "text" } }],
                        widgets_values: ["cinematic template prompt"],
                    },
                ],
                links: [],
            },
        });

        expect(workflow.nodes).toHaveLength(1);
        expect(getNodeParamEntries(workflow.nodes[0])).toEqual([["text", "cinematic template prompt"]]);
    });

    it("uses rootGraph subgraph definitions for saved Comfy workflows", () => {
        const workflow = resolveAssetWorkflow({
            workflow: {
                nodes: [{ id: 30, type: "root-sg", properties: {} }],
                rootGraph: {
                    subgraphs: [
                        {
                            id: "root-sg",
                            name: "Root Graph Detailer",
                            nodes: [{ id: 1, type: "InnerDetailer", title: "Inner Detailer" }],
                            links: [],
                        },
                    ],
                },
            },
        });

        expect(workflow.nodes[0].properties.subgraph_name).toBe("Root Graph Detailer");
        expect(getWorkflowNodes(workflow).map((node: any) => String(node.id))).toEqual(["30", "1"]);
        expect(getNodeDisplayName(workflow.nodes[0])).toBe("Root Graph Detailer");
    });

    it("exposes subgraph proxy widget values on the parent node", () => {
        const workflow = resolveAssetWorkflow({
            workflow: {
                nodes: [
                    {
                        id: 320,
                        type: "2454ad83-157c-40dd-9f19-5daaf4041ce0",
                        properties: {
                            proxyWidgets: [
                                ["312", "value"],
                                ["299", "value"],
                                ["317", "text_encoder"],
                            ],
                        },
                    },
                ],
                definitions: {
                    subgraphs: [
                        {
                            id: "2454ad83-157c-40dd-9f19-5daaf4041ce0",
                            name: "Image to Video (LTX-2.3)",
                            inputs: [
                                { name: "value_2", label: "width" },
                                { name: "value_3", label: "height" },
                                { name: "text_encoder" },
                            ],
                            nodes: [
                                {
                                    id: 312,
                                    type: "PrimitiveInt",
                                    title: "Width",
                                    inputs: [],
                                    widgets_values: [1024],
                                },
                                {
                                    id: 299,
                                    type: "PrimitiveInt",
                                    title: "Height",
                                    inputs: [],
                                    widgets_values: [768],
                                },
                                {
                                    id: 317,
                                    type: "LTXAVTextEncoderLoader",
                                    title: "Text Encoder Loader",
                                    inputs: [{ name: "text_encoder", type: "COMBO", widget: { name: "text_encoder" } }],
                                    widgets_values: ["gemma_3_12B_it_fp4_mixed.safetensors"],
                                },
                            ],
                            links: [
                                [597, -10, 0, 312, 0, "INT"],
                                [598, -10, 1, 299, 0, "INT"],
                                [606, -10, 2, 317, 0, "COMBO"],
                            ],
                        },
                    ],
                },
            },
        });

        expect(getNodeParamEntries(workflow.nodes[0])).toEqual([
            ["width", 1024],
            ["height", 768],
            ["text_encoder", "gemma_3_12B_it_fp4_mixed.safetensors"],
        ]);
    });

    it("falls back to node title when the type is an opaque hash", () => {
        const node = {
            id: 7,
            type: "abcdefabcdefabcdefabcdefabcdefabcdef",
            title: "Regional Prompt",
        };

        expect(getNodeDisplayName(node)).toBe("Regional Prompt");
        expect(getNodeTypeLabel(node)).toBe("Subgraph");
    });

    it("expands runtime and serialized subgraphs with graph-qualified node ids", () => {
        const workflow = {
            nodes: [
                { id: 1, type: "ImageInput", pos: [0, 0], size: [140, 60] },
                {
                    id: 2,
                    type: "sg-runtime",
                    pos: [220, 0],
                    size: [220, 140],
                    subgraph: {
                        id: "runtime-subgraph",
                        name: "Runtime Detailer",
                        nodes: [
                            { id: 1, type: "InnerRuntime", pos: [0, 0], size: [120, 50] },
                        ],
                        links: [],
                    },
                },
                { id: 3, type: "sg-serialized", pos: [500, 0], size: [220, 140] },
            ],
            links: [[1, 1, 0, 2, 0, "IMAGE"]],
            rootGraph: {
                subgraphs: [
                    {
                        id: "sg-serialized",
                        name: "Serialized Detailer",
                        nodes: [
                            { id: 1, type: "InnerSerialized", pos: [0, 0], size: [120, 50] },
                        ],
                        links: [[10, 1, 0, 1, 0, "IMAGE"]],
                    },
                ],
            },
        };

        const expanded = expandSubgraphsForMinimap(workflow);
        const ids = expanded.nodes.map((node) => String(node.id));

        expect(ids).toContain("1");
        expect(ids).toContain("2::1");
        expect(ids).toContain("3::1");
        expect(expanded.links).toEqual(
            expect.arrayContaining([
                expect.arrayContaining([1, 1, 0, 2, 0, "IMAGE"]),
                expect.arrayContaining([10, "3::1", 0, "3::1", 0, "IMAGE"]),
            ]),
        );
    });

    it("expands nested subgraphs that reference root-level definitions", () => {
        const workflow = {
            nodes: [
                { id: 10, type: "outer-sg", pos: [0, 0], size: [300, 220] },
            ],
            links: [],
            definitions: {
                subgraphs: [
                    {
                        id: "outer-sg",
                        name: "Outer",
                        nodes: [
                            { id: 20, type: "inner-sg", pos: [0, 0], size: [240, 160] },
                        ],
                        links: [],
                    },
                    {
                        id: "inner-sg",
                        name: "Inner",
                        nodes: [
                            { id: 30, type: "KSampler", pos: [10, 20], size: [160, 80] },
                        ],
                        links: [],
                    },
                ],
            },
        };

        const expanded = expandSubgraphsForMinimap(workflow);
        const ids = expanded.nodes.map((node) => String(node.id));

        expect(ids).toContain("10");
        expect(ids).toContain("10::20");
        expect(ids).toContain("10::20::30");
    });

    it("supports root-only mode for node listing and lookup", () => {
        const workflow = {
            nodes: [
                {
                    id: 2,
                    type: "sg-runtime",
                    subgraph: {
                        id: "runtime-subgraph",
                        nodes: [{ id: 1, type: "InnerRuntime" }],
                        links: [],
                    },
                },
            ],
            links: [],
        };

        expect(getWorkflowNodes(workflow, { includeSubgraphs: false }).map((node: any) => String(node.id))).toEqual(["2"]);
        expect(getWorkflowNodes(workflow, { includeSubgraphs: true }).map((node: any) => String(node.id))).toEqual(["2", "1"]);
        expect(findWorkflowNode(workflow, "2::1", { includeSubgraphs: false })).toBeNull();
        expect(findWorkflowNode(workflow, "2::1", { includeSubgraphs: true })?.type).toBe("InnerRuntime");
    });

    it("treats array-like widgets_values safely and prefers input slot labels", () => {
        const node = {
            id: 12,
            type: "Sampler",
            inputs: [
                { name: "seed", type: "INT", widget: { name: "seed" } },
                { name: "steps", type: "INT", widget: { name: "steps" } },
            ],
            widgets: [{ name: "swapped_steps" }, { name: "swapped_seed" }],
            widgets_values: {
                0: 123,
                1: 28,
                length: 2,
            },
        };

        expect(getNodeWidgetValueEntries(node)).toEqual([
            { label: "seed", value: 123, index: 0 },
            { label: "steps", value: 28, index: 1 },
        ]);
    });
});
