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

import toolModel from "./test-data/tool";
import { matchCase, matchInputs, validateInputs, visitAllInputs, visitInputs } from "./utilities";

function visitInputsString(inputs) {
    let results = "";
    visitInputs(inputs, (input, identifier) => {
        results += `${identifier}=${input.value};`;
    });
    return results;
}

describe("form component utilities", () => {
    it("tool model test", () => {
        const visits = [];
        visitInputs(toolModel.inputs, function (node, name) {
            visits.push({ name: name, node: node });
        });
        const result =
            '[{"name":"a","node":{"name":"a","type":"text"}},{"name":"b|c","node":{"name":"c","type":"select","value":"h","options":[["d","d",false],["h","h",false]]}},{"name":"b|i","node":{"name":"i","type":"text","value":"i"}},{"name":"b|j","node":{"name":"j","type":"text","value":"j"}},{"name":"k_0|l","node":{"name":"l","type":"text","value":"l"}},{"name":"k_0|m|n","node":{"name":"n","type":"select","value":"r","options":[["o","o",false],["r","r",false]]}},{"name":"k_0|m|s","node":{"name":"s","type":"text","value":"s"}},{"name":"k_0|m|t","node":{"name":"t","type":"text","value":"t"}}]';
        expect(JSON.stringify(visits)).toEqual(result);
    });
    it("conditional case matching", () => {
        const input = {
            name: "a",
            type: "conditional",
            test_param: {
                name: "b",
                type: "boolean",
                value: "true",
                truevalue: undefined,
                falsevalue: undefined,
            },
            cases: [
                {
                    value: "true",
                    inputs: [
                        {
                            name: "c",
                            type: "text",
                            value: "cvalue",
                        },
                    ],
                },
                {
                    value: "false",
                    inputs: [
                        {
                            name: "d",
                            type: "text",
                            value: "dvalue",
                        },
                    ],
                },
            ],
        };

        // test simple case matching
        expect(matchCase(input, "true")).toEqual(0);
        expect(matchCase(input, true)).toEqual(0);
        expect(matchCase(input, "false")).toEqual(1);
        expect(matchCase(input, false)).toEqual(1);

        // test truevalue
        input.test_param.truevalue = "truevalue";
        expect(matchCase(input, "true")).toEqual(-1);
        input.cases[0].value = "truevalue";
        expect(matchCase(input, "true")).toEqual(0);

        // test falsevalue
        input.test_param.falsevalue = "falsevalue";
        expect(matchCase(input, "true")).toEqual(0);
        expect(matchCase(input, "false")).toEqual(-1);
        input.cases[1].value = "falsevalue";
        expect(matchCase(input, "false")).toEqual(1);

        // test (empty) truevalue
        input.test_param.truevalue = undefined;
        input.cases[0].value = "true";
        expect(matchCase(input, "true")).toEqual(0);
        input.test_param.truevalue = "";
        expect(matchCase(input, "true")).toEqual(-1);
        input.cases[0].value = "";
        expect(matchCase(input, "true")).toEqual(0);

        // test visit inputs
        expect(visitInputsString([input])).toEqual("a|b=true;a|c=cvalue;");
        input.test_param.value = "false";
        expect(visitInputsString([input])).toEqual("a|b=false;a|d=dvalue;");

        // switch test parameter to other type than boolean e.g. select
        input.test_param.type = "select";
        expect(matchCase(input, "")).toEqual(0);
        expect(matchCase(input, "unavailable")).toEqual(-1);
        expect(matchCase(input, "falsevalue")).toEqual(1);
    });

    it("test basic value validation", () => {
        const index = {
            input_a: {},
            input_b: {},
            input_c: {},
        };
        const values = {
            input_a: "1",
            input_b: "2",
            input_c: { values: [{ id: 0 }] },
        };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);
        values.input_a = undefined;
        result = validateInputs(index, values);
        expect(JSON.stringify(result)).toEqual('["input_a","Please provide a value for this option."]');
        index.input_a.optional = true;
        result = validateInputs(index, values);
        expect(result).toEqual(null);
        values.input_c.values = [];
        result = validateInputs(index, values);
        expect(JSON.stringify(result)).toEqual('["input_c","Please provide data for this input."]');
    });

    it("test error matching", () => {
        const index = {
            input_a: {},
            input_b_0: {},
            "input_c|input_d": {},
        };
        const values = {
            input_a: "error_a",
            input_b: ["error_b"],
            input_c: { input_d: "error_d" },
        };
        const result = matchInputs(index, values);
        expect(result["input_a"]).toEqual("error_a");
        expect(result["input_b_0"]).toEqual("error_b");
        expect(result["input_c|input_d"]).toEqual("error_d");
    });

    it("test multiple validators", () => {
        const index = {
            path: {
                validators: [
                    {
                        type: "regex",
                        expression: "^.*[^/]$",
                        message: "Value cannot end with a trailing slash",
                        negate: false,
                    },
                    {
                        type: "regex",
                        expression: "^(?!\\s)(?!.*\\s$).*$",
                        message: "Value cannot have leading or trailing whitespace",
                        negate: false,
                    },
                ],
            },
        };

        // Valid path
        let values = { path: "clean/path" };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        // Invalid: trailing slash
        values = { path: "path/" };
        result = validateInputs(index, values);
        expect(result).toEqual(["path", "Value cannot end with a trailing slash"]);

        // Invalid: trailing whitespace
        values = { path: "path " };
        result = validateInputs(index, values);
        expect(result).toEqual(["path", "Value cannot have leading or trailing whitespace"]);
    });

    it("test validators skip empty optional values", () => {
        const index = {
            optional_field: {
                optional: true,
                validators: [
                    {
                        type: "regex",
                        expression: "^(?!\\s)(?!.*\\s$)(?!.*/$).+$",
                        message: "Value cannot have leading/trailing spaces or trailing slashes",
                        negate: false,
                    },
                ],
            },
        };

        // Empty value should not trigger validator
        const values = { optional_field: "" };
        const result = validateInputs(index, values);
        expect(result).toEqual(null);
    });

    it("test validators skip null values", () => {
        const index = {
            optional_field: {
                optional: true, // Make it optional so null is allowed
                validators: [
                    {
                        type: "regex",
                        expression: "^(?!\\s)(?!.*\\s$)(?!.*/$).+$",
                        message: "Value cannot have leading/trailing spaces or trailing slashes",
                        negate: false,
                    },
                ],
            },
        };

        // Null value should not trigger validator for optional fields
        const values = { optional_field: null };
        const result = validateInputs(index, values);
        expect(result).toEqual(null);
    });

    it("test length validator", () => {
        const index = {
            username: {
                validators: [
                    {
                        type: "length",
                        min: 3,
                        max: 20,
                        message: "Username must be between 3 and 20 characters",
                    },
                ],
            },
        };

        // Valid length
        let values = { username: "john" };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        values = { username: "a".repeat(20) };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        // Too short
        values = { username: "ab" };
        result = validateInputs(index, values);
        expect(result).toEqual(["username", "Username must be between 3 and 20 characters"]);

        // Too long
        values = { username: "a".repeat(21) };
        result = validateInputs(index, values);
        expect(result).toEqual(["username", "Username must be between 3 and 20 characters"]);
    });

    it("test in_range validator", () => {
        const index = {
            age: {
                validators: [
                    {
                        type: "in_range",
                        min: 18,
                        max: 120,
                        message: "Age must be between 18 and 120",
                    },
                ],
            },
        };

        // Valid range
        let values = { age: 25 };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        values = { age: 18 };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        values = { age: 120 };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        // Too small
        values = { age: 17 };
        result = validateInputs(index, values);
        expect(result).toEqual(["age", "Age must be between 18 and 120"]);

        // Too large
        values = { age: 121 };
        result = validateInputs(index, values);
        expect(result).toEqual(["age", "Age must be between 18 and 120"]);
    });

    it("test in_range validator with non-numeric value", () => {
        const index = {
            age: {
                validators: [
                    {
                        type: "in_range",
                        min: 18,
                        max: 120,
                        message: "Age must be between 18 and 120",
                    },
                ],
            },
        };

        // Non-numeric value
        const values = { age: "not a number" };
        const result = validateInputs(index, values);
        expect(result).toEqual(["age", "Value must be numeric for range validation"]);
    });

    it("test validators run on optional fields with values", () => {
        const index = {
            optional_field: {
                optional: true,
                validators: [
                    {
                        type: "length",
                        min: 3,
                        message: "Must be at least 3 characters",
                    },
                ],
            },
        };

        // Empty value should not trigger validator
        let values = { optional_field: "" };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        // null value should not trigger validator
        values = { optional_field: null };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        // undefined should not trigger validator
        values = { optional_field: undefined };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        // But when optional field has a value, validator should run
        values = { optional_field: "ab" }; // Too short
        result = validateInputs(index, values);
        expect(result).toEqual(["optional_field", "Must be at least 3 characters"]);

        // Valid value should pass
        values = { optional_field: "valid" };
        result = validateInputs(index, values);
        expect(result).toEqual(null);
    });

    it("test validators with undefined min or max values", () => {
        const index = {
            field_with_undefined_min: {
                validators: [
                    {
                        type: "length",
                        min: undefined,
                        max: 10,
                        message: "Must be at most 10 characters",
                    },
                ],
            },
        };

        // Should only validate max when min is undefined
        let values = { field_with_undefined_min: "a" }; // Very short but no min
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        values = { field_with_undefined_min: "a".repeat(11) }; // Too long
        result = validateInputs(index, values);
        expect(result).toEqual(["field_with_undefined_min", "Must be at most 10 characters"]);
    });

    it("test validators with null min or max values", () => {
        const index = {
            field_with_null_max: {
                validators: [
                    {
                        type: "length",
                        min: 3,
                        max: null,
                        message: "Must be at least 3 characters",
                    },
                ],
            },
        };

        // Should only validate min when max is null
        let values = { field_with_null_max: "a".repeat(100) }; // Very long but no max
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        values = { field_with_null_max: "ab" }; // Too short
        result = validateInputs(index, values);
        expect(result).toEqual(["field_with_null_max", "Must be at least 3 characters"]);
    });

    it("test required field validation with empty string vs undefined vs null", () => {
        const index = {
            required_field: {
                optional: false,
            },
        };

        // Empty string should PASS for required field (default behavior - allowEmptyValueOnRequiredInput=false)
        // Note: allowEmptyValueOnRequiredInput is misnamed - it should be rejectEmptyRequiredInputs
        let values = { required_field: "" };
        let result = validateInputs(index, values);
        expect(result).toEqual(null);

        // undefined should fail for required field
        values = { required_field: undefined };
        result = validateInputs(index, values);
        expect(result).toEqual(["required_field", "Please provide a value for this option."]);

        // null should fail for required field
        values = { required_field: null };
        result = validateInputs(index, values);
        expect(result).toEqual(["required_field", "Please provide a value for this option."]);

        // Non-empty value should pass
        values = { required_field: "value" };
        result = validateInputs(index, values);
        expect(result).toEqual(null);

        // When allowEmptyValueOnRequiredInput=true (misnamed, really means reject empty strings too),
        // empty string should also fail
        values = { required_field: "" };
        result = validateInputs(index, values, true);
        expect(result).toEqual(["required_field", "Please provide a value for this option."]);
    });

    describe("visitAllInputs", () => {
        it("should match visitInputs traversal order for non-conditional inputs", () => {
            const inputs = [
                { name: "a", type: "text", value: "a" },
                {
                    name: "r",
                    type: "repeat",
                    cache: [
                        [
                            { name: "x", type: "text", value: "x" },
                            { name: "y", type: "text", value: "y" },
                        ],
                    ],
                    inputs: [
                        { name: "x", type: "text", value: "x" },
                        { name: "y", type: "text", value: "y" },
                    ],
                },
                {
                    name: "s",
                    type: "section",
                    inputs: [
                        { name: "p", type: "text", value: "p" },
                        { name: "q", type: "text", value: "q" },
                    ],
                },
            ];
            const visitOrder = [];
            const allOrder = [];
            visitInputs(inputs, (node, name) => visitOrder.push(name));
            visitAllInputs(inputs, (node, name) => allOrder.push(name));
            expect(allOrder).toEqual(visitOrder);
        });

        it("should visit all conditional cases, not just the active one", () => {
            const inputs = [
                {
                    name: "cond",
                    type: "conditional",
                    test_param: { name: "tp", type: "select", value: "a" },
                    cases: [
                        {
                            value: "a",
                            inputs: [{ name: "in_a", type: "text", value: "va" }],
                        },
                        {
                            value: "b",
                            inputs: [{ name: "in_b", type: "text", value: "vb" }],
                        },
                    ],
                },
            ];
            const visitNames = [];
            const allNames = [];
            visitInputs(inputs, (node, name) => visitNames.push(name));
            visitAllInputs(inputs, (node, name) => allNames.push(name));
            // visitInputs only visits active case (a)
            expect(visitNames).toEqual(["cond|tp", "cond|in_a"]);
            // visitAllInputs visits both cases
            expect(allNames).toEqual(["cond|tp", "cond|in_a", "cond|in_b"]);
        });

        it("should visit nested conditional inside repeat", () => {
            const inputs = [
                {
                    name: "rep",
                    type: "repeat",
                    cache: [
                        [
                            {
                                name: "nested_cond",
                                type: "conditional",
                                test_param: { name: "sel", type: "select", value: "x" },
                                cases: [
                                    {
                                        value: "x",
                                        inputs: [{ name: "leaf_x", type: "text", value: "1" }],
                                    },
                                    {
                                        value: "y",
                                        inputs: [{ name: "leaf_y", type: "text", value: "2" }],
                                    },
                                ],
                            },
                        ],
                    ],
                    inputs: [],
                },
            ];
            const allNames = [];
            visitAllInputs(inputs, (node, name) => allNames.push(name));
            expect(allNames).toEqual(["rep_0|nested_cond|sel", "rep_0|nested_cond|leaf_x", "rep_0|nested_cond|leaf_y"]);
        });
    });
});
