// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import * as common from "./common.js";

const NUMERIC_TYPES = [
    "i8",
    "ui8",
    "i16",
    "ui16",
    "i32",
    "ui32",
    "i64",
    "ui64",
    "f32",
    "f64",
];

/**
 * Return all possible permutations of unary operators for all numeric types.
 */
function generate_unary_operations(operator) {
    const expressions = [];
    for (const expr of NUMERIC_TYPES) {
        expressions.push(`${operator}"${expr}"`);
    }
    return expressions;
}

/**
 * Return all possible permutations of binary operators for all numeric types.
 */
function generate_binary_operations(operator) {
    const expressions = [];
    for (const expr of NUMERIC_TYPES) {
        for (let i = 0; i < NUMERIC_TYPES.length; i++) {
            expressions.push(`"${expr}" ${operator} "${NUMERIC_TYPES[i]}"`);
        }
    }
    return expressions;
}

function calc_unary(operator, value) {
    switch (operator) {
        case "+":
            return +value;
        case "-":
            return -value;
        default:
            throw new Error("Unknown operator");
    }
}

function calc_binary(operator, left, right) {
    switch (operator) {
        case "+":
            return left + right;
        case "-":
            return left - right;
        case "*":
            return left * right;
        case "/":
            return left / right;
        case "%":
            return left % right;
        case "^":
            return Math.pow(left, right);
        case "==":
            return left == right ? 1 : 0;
        case "!=":
            return left != right ? 1 : 0;
        case ">":
            return left > right ? 1 : 0;
        case "<":
            return left < right ? 1 : 0;
        case ">=":
            return left >= right ? 1 : 0;
        case "<=":
            return left <= right ? 1 : 0;
        default:
            throw new Error("Unknown operator");
    }
}

/**
 * Validate the results of operations against all numeric types.
 *
 * @param {*} operator
 */
function validate_unary_operations(output, expressions, operator) {
    for (const expr of expressions) {
        const output_col = output[expr];
        const input = expr.substr(2, expr.length - 3);
        expect({
            expr,
            result: output_col,
        }).toEqual({
            expr,
            result: output[input].map((v) => calc_unary(operator, v)),
        });
    }
}

/**
 * Validate the results of operations against all numeric types.
 *
 * @param {*} operator
 */
function validate_binary_operations(output, expressions, operator) {
    for (const expr of expressions) {
        const output_col = output[expr];
        const inputs = expr.split(` ${operator} `);
        const left = inputs[0].substr(1, inputs[0].length - 2);
        const right = inputs[1].substr(1, inputs[1].length - 2);
        expect({
            expr,
            result: output_col,
        }).toEqual({
            expr,
            result: output[left].map((v, idx) =>
                calc_binary(operator, v, output[right][idx]),
            ),
        });
    }
}

/**
 * Tests the correctness of expressions allowed by exprtk, using as many
 * of the expression structures as we can, such as conditionals, loops, etc.
 */
((perspective) => {
    test.describe("Numeric operations", function () {
        test.describe("All numeric types", function () {
            test.describe("aggregates", function () {
                test(
                    "integer",
                    async function () {
                        const table = await perspective.table({
                            a: "integer",
                        });

                        const view = await table.view({
                            expressions: { '"a"': '"a"' },
                        });

                        await table.update({
                            a: [10, 15, 20, 30],
                        });

                        const result = await view.to_columns();
                        expect(result['"a"']).toEqual([10, 15, 20, 30]);
                        expect(result["a"]).toEqual([10, 15, 20, 30]);
                        await view.delete();
                        await table.delete();
                    },

                    test("expression integer column sum", async function () {
                        const table = await perspective.table({
                            a: [10, 15, 20, 30],
                            b: [1, 2, 3, 4],
                        });

                        const view = await table.view({
                            group_by: ["b"],
                            aggregates: {
                                a: "sum",
                                b: "sum",
                            },
                            expressions: { '"a"': '"a"' },
                        });

                        await table.update({
                            a: [40, 50, 60, 70],
                        });

                        expect(await view.to_columns()).toEqual({
                            __ROW_PATH__: [[], [null], [1], [2], [3], [4]],
                            a: [295, 220, 10, 15, 20, 30],
                            b: [10, 0, 1, 2, 3, 4],
                            '"a"': [295, 220, 10, 15, 20, 30],
                        });
                        await view.delete();
                        await table.delete();
                    }),
                );
            });

            test.describe("unary", function () {
                test("negative", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_unary_operations("+");
                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_unary_operations(result, expressions, "+");
                    await view.delete();
                    await table.delete();
                });
            });

            test.describe("binary", function () {
                test("add", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_binary_operations("+");
                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "+");
                    await view.delete();
                    await table.delete();
                });

                test("subtract", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_binary_operations("-");
                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "-");
                    await view.delete();
                    await table.delete();
                });

                test("multiply", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_binary_operations("*");
                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "*");
                    await view.delete();
                    await table.delete();
                });

                test("divide", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_binary_operations("/");

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "/");
                    await view.delete();
                    await table.delete();
                });

                test("modulus", async function () {
                    const table = await perspective.table(
                        common.all_types_arrow.slice(),
                    );
                    const expressions = generate_binary_operations("%");

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "%");
                    await view.delete();
                    await table.delete();
                });

                test("power", async function () {
                    const table = await perspective.table(common.arrow.slice());
                    const expressions = generate_binary_operations("^");
                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();
                    validate_binary_operations(result, expressions, "^");
                });

                test("==", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" == "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` == `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v == result[right][idx],
                            ),
                        );
                    }
                });

                test("!=", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" != "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` != `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v != result[right][idx],
                            ),
                        );
                    }
                });

                test(">", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" > "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` > `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v > result[right][idx],
                            ),
                        );
                    }
                });

                test("<", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" < "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` < `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v < result[right][idx],
                            ),
                        );
                    }
                });

                test(">=", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" >= "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` >= `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v >= result[right][idx],
                            ),
                        );
                    }
                });

                test("<=", async function () {
                    const table = await perspective.table(
                        common.all_types_multi_arrow.slice(),
                    );
                    const expressions = [];

                    // comparisons only work when the two types are the same
                    for (const expr of NUMERIC_TYPES) {
                        expressions.push(`"${expr}" <= "${expr} 2"`);
                    }

                    const view = await table.view({
                        expressions,
                    });

                    const col_names = await view.column_paths();

                    for (const expr of expressions) {
                        expect(col_names.includes(expr)).toBe(true);
                    }

                    const result = await view.to_columns();

                    for (const expr of expressions) {
                        const output_col = result[expr];
                        const inputs = expr.split(` <= `);
                        const left = inputs[0].substr(1, inputs[0].length - 2);
                        const right = inputs[1].substr(1, inputs[1].length - 2);
                        expect(output_col).toEqual(
                            result[left].map(
                                (v, idx) => v <= result[right][idx],
                            ),
                        );
                    }
                });

                test("Numeric comparisons should be false for different types", async function () {
                    const table = await perspective.table({
                        a: "integer",
                        b: "float",
                        c: "integer",
                        d: "float",
                    });
                    const view = await table.view({
                        expressions: {
                            '"a" == "b"': '"a" == "b"',
                            '"a" != "b"': '"a" != "b"',
                        },
                    });
                    await table.update({
                        a: [1, 2, 3, 4],
                        b: [1.0, 2.0, 3.0, 4.0],
                        c: [1, 0, 1, 0],
                        d: [1.0, 0, 3.0, 0.0],
                    });
                    const result = await view.to_columns();
                    expect(result['"a" == "b"']).toEqual([
                        false,
                        false,
                        false,
                        false,
                    ]);
                    expect(result['"a" != "b"']).toEqual([
                        true,
                        true,
                        true,
                        true,
                    ]);
                    await view.delete();
                    await table.delete();
                });

                test("Numeric comparisons should be true for same types", async function () {
                    const table = await perspective.table({
                        a: "integer",
                        b: "float",
                        c: "integer",
                        d: "float",
                    });
                    const view = await table.view({
                        expressions: [
                            '"a" == "c"',
                            '"a" != "c"',
                            '"b" == "d"',
                            '"b" != "d"',
                        ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                    });
                    await table.update({
                        a: [1, 2, 3, 4],
                        b: [1.0, 2.0, 3.0, 4.0],
                        c: [1, 0, 1, 0],
                        d: [1.0, 0, 3.0, 0.0],
                    });
                    const result = await view.to_columns();
                    expect(result['"a" == "c"']).toEqual([
                        true,
                        false,
                        false,
                        false,
                    ]);
                    expect(result['"a" != "c"']).toEqual([
                        false,
                        true,
                        true,
                        true,
                    ]);
                    expect(result['"b" == "d"']).toEqual([
                        true,
                        false,
                        true,
                        false,
                    ]);
                    expect(result['"b" != "d"']).toEqual([
                        false,
                        true,
                        false,
                        true,
                    ]);
                    await view.delete();
                    await table.delete();
                });

                test("Should not divide by 0", async function () {
                    const table = await perspective.table({
                        a: [1, 2, 3, 4],
                        b: [0, 0, 0, 0],
                        c: [1.5, 2.123, 3.125, 4.123809],
                    });

                    const view = await table.view({
                        expressions: {
                            '"a" / "b"': '"a" / "b"',
                            '"c" / "b"': '"c" / "b"',
                        },
                    });

                    const result = await view.to_columns();
                    expect(result['"a" / "b"']).toEqual([
                        null,
                        null,
                        null,
                        null,
                    ]);
                    expect(result['"c" / "b"']).toEqual([
                        null,
                        null,
                        null,
                        null,
                    ]);
                    await view.delete();
                    await table.delete();
                });

                test("Should not modulo by 0", async function () {
                    const table = await perspective.table({
                        a: [1, 2, 3, 4],
                        b: [0, 0, 0, 0],
                        c: [1.5, 2.123, 3.125, 4.123809],
                    });

                    const view = await table.view({
                        expressions: {
                            '"a" % "b"': '"a" % "b"',
                            '"c" % "b"': '"c" % "b"',
                        },
                    });

                    const result = await view.to_columns();
                    expect(result['"a" % "b"']).toEqual([
                        null,
                        null,
                        null,
                        null,
                    ]);
                    expect(result['"c" % "b"']).toEqual([
                        null,
                        null,
                        null,
                        null,
                    ]);
                    await view.delete();
                    await table.delete();
                });
            });
        });

        test.describe("Integers", function () {
            test("Integer outputs with negative values dont overflow", async function () {
                var data = [
                    { x: -1, y: "a", z: true },
                    { x: -2, y: "b", z: false },
                    { x: -3, y: "c", z: true },
                    { x: -4, y: "d", z: false },
                ];

                var table = await perspective.table({
                    x: "integer",
                    y: "string",
                    z: "boolean",
                });

                await table.update(data);

                var view = await table.view({
                    group_by: ["z"],
                    columns: ["x", "w"],
                    expressions: {
                        w: 'integer("x")',
                    },
                });

                var answer = [
                    { __ROW_PATH__: [], x: -10, w: -10 },
                    { __ROW_PATH__: [false], x: -6, w: -6 },
                    { __ROW_PATH__: [true], x: -4, w: -4 },
                ];

                let result = await view.to_json();
                expect(result).toEqual(answer);
                view.delete();
                table.delete();
            });
        });

        test.describe("Functions", function () {
            test("min", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const expressions = [
                    "min(1)",
                    'min("a")',
                    'min("a", -10, -10.001)',
                    'min("b")',
                    'min("b", 0.00000000001, -10, -100, -100.1)',
                    'min("a", "b")',
                ];

                const view = await table.view({
                    expressions,
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const schema = await view.expression_schema();
                for (const expr of expressions) {
                    expect(schema[expr]).toEqual("float");
                }

                const result = await view.to_columns();
                expect(result["min(1)"]).toEqual([1, 1, 1, 1]);
                expect(result['min("a")']).toEqual([1, 2, 3, 4]);
                expect(result['min("a", -10, -10.001)']).toEqual([
                    -10.001, -10.001, -10.001, -10.001,
                ]);
                expect(result['min("b")']).toEqual([1.5, 2.5, 3.5, 4.5]);
                expect(
                    result['min("b", 0.00000000001, -10, -100, -100.1)'],
                ).toEqual([-100.1, -100.1, -100.1, -100.1]);
                expect(result['min("a", "b")']).toEqual([1, 2, 3, 4]);
                await view.delete();
                await table.delete();
            });

            test("max", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const expressions = [
                    "max(1)",
                    "max(2000000000000.11, 2000000000000.1)",
                    'max("a")',
                    'max("a", -1.00001)',
                    'max("a", "b")',
                    'max("b")',
                    'max("a", 10, 20, 0.1, 0.00000001)',
                    'max("b", -1, -100, 100)',
                ];

                const view = await table.view({
                    expressions,
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const schema = await view.expression_schema();
                for (const expr of expressions) {
                    expect(schema[expr]).toEqual("float");
                }

                const result = await view.to_columns();
                expect(result["max(1)"]).toEqual([1, 1, 1, 1]);
                expect(
                    result["max(2000000000000.11, 2000000000000.1)"],
                ).toEqual([
                    2000000000000.11, 2000000000000.11, 2000000000000.11,
                    2000000000000.11,
                ]);
                expect(result['max("a")']).toEqual([1, 2, 3, 4]);
                expect(result['max("a", "b")']).toEqual([1.5, 2.5, 3.5, 4.5]);
                expect(result['max("b")']).toEqual([1.5, 2.5, 3.5, 4.5]);
                expect(result['max("a", 10, 20, 0.1, 0.00000001)']).toEqual([
                    20, 20, 20, 20,
                ]);
                expect(result['max("b", -1, -100, 100)']).toEqual([
                    100, 100, 100, 100,
                ]);

                await view.delete();
                await table.delete();
            });

            test("inrange", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                // works for floats only
                const view = await table.view({
                    expressions: {
                        'inrange(9, "b", 20)': 'inrange(9, "b", 20)',
                    },
                });

                await table.update({
                    a: [10, 15, 20, 30],
                    b: [10.5, 15.5, 20.5, 30.5],
                });

                const result = await view.to_columns();
                expect(result['inrange(9, "b", 20)']).toEqual([
                    true,
                    true,
                    false,
                    false,
                ]);
                await view.delete();
                await table.delete();
            });

            test("iclamp", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'iclamp(10, "a", 20)': 'iclamp(10, "a", 20)',
                        'iclamp(10, "b", 20)': 'iclamp(10, "b", 20)',
                    },
                });

                await table.update({
                    a: [10, 15, 20, 30],
                    b: [10.5, 15.5, 20.5, 30.5],
                });

                const result = await view.to_columns();
                expect(result['iclamp(10, "a", 20)']).toEqual([10, 15, 20, 30]);
                expect(result['iclamp(10, "b", 20)']).toEqual([
                    10, 20, 20.5, 30.5,
                ]);
                await view.delete();
                await table.delete();
            });

            test("pow", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'pow("a", 1)': 'pow("a", 1)',
                        'pow("b", 3)': 'pow("b", 3)',
                    },
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const result = await view.to_columns();
                expect(result['pow("a", 1)']).toEqual([1, 2, 3, 4]);
                expect(result['pow("b", 3)']).toEqual(
                    [1.5, 2.5, 3.5, 4.5].map((x) => Math.pow(x, 3)),
                );
                await view.delete();
                await table.delete();
            });

            test.skip("logn", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'logn("a", 5)': 'logn("a", 5)',
                        'logn("b", 3)': 'logn("b", 3)',
                    },
                });

                await table.update({
                    a: [100, 200, 300, 400],
                    b: [100.5, 200.5, 300.5, 400.5],
                });

                const result = await view.to_columns();
                expect(result['logn("a", 5)']).toEqual(
                    [100, 200, 300, 400].map((x) => Math.log(x) / Math.log(5)),
                );
                expect(result['logn("b", 3)']).toEqual(
                    [100.5, 200.5, 300.5, 400.5].map(
                        (x) => Math.log(x) / Math.log(3),
                    ),
                );

                await view.delete();
                await table.delete();
            });

            test("root", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'root("a", 5)': 'root("a", 5)',
                        'root("b", 3)': 'root("b", 3)',
                    },
                });

                await table.update({
                    a: [100, 200, 300, 400],
                    b: [100.5, 200.5, 300.5, 400.5],
                });

                const result = await view.to_columns();
                expect(
                    result['root("a", 5)'].map((x) => Number(x.toFixed(5))),
                ).toEqual(
                    [100, 200, 300, 400].map((x) =>
                        Number(Math.pow(x, 1 / 5).toFixed(5)),
                    ),
                );
                expect(
                    result['root("b", 3)'].map((x) => Number(x.toFixed(5))),
                ).toEqual(
                    [100.5, 200.5, 300.5, 400.5].map((x) =>
                        Number(Math.pow(x, 1 / 3).toFixed(5)),
                    ),
                );
                await view.delete();
                await table.delete();
            });

            test("avg", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'avg("a", 10, 20, 30, 40, "a")',
                        'avg("b", 3, 4, 5, "b")',
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const result = await view.to_columns();
                expect(result['avg("a", 10, 20, 30, 40, "a")']).toEqual(
                    [1, 2, 3, 4].map((x) => (x + x + 10 + 20 + 30 + 40) / 6),
                );
                expect(result['avg("b", 3, 4, 5, "b")']).toEqual(
                    [1.5, 2.5, 3.5, 4.5].map((x) => (x + x + 3 + 4 + 5) / 5),
                );
                await view.delete();
                await table.delete();
            });

            test("sum", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'sum("a", 10, 20, 30, 40, "a")',
                        'sum("b", 3, 4, 5, "b")',
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const result = await view.to_columns();
                expect(result['sum("a", 10, 20, 30, 40, "a")']).toEqual(
                    [1, 2, 3, 4].map((x) => x + x + 10 + 20 + 30 + 40),
                );
                expect(result['sum("b", 3, 4, 5, "b")']).toEqual(
                    [1.5, 2.5, 3.5, 4.5].map((x) => x + x + 3 + 4 + 5),
                );
                await view.delete();
                await table.delete();
            });

            test("trunc", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'trunc("a")': 'trunc("a")',
                        'trunc("b")': 'trunc("b")',
                    },
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [1.5, 2.5, 3.5, 4.5],
                });

                const result = await view.to_columns();
                expect(result['trunc("a")']).toEqual([1, 2, 3, 4]);
                expect(result['trunc("b")']).toEqual([1, 2, 3, 4]);
                await view.delete();
                await table.delete();
            });

            test("d2r", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'deg2rad("a")': 'deg2rad("a")',
                        'deg2rad("b")': 'deg2rad("b")',
                    },
                });

                await table.update({
                    a: [30, 60, 90, 120],
                    b: [25.5, 45.5, 88.721282, 91.12983],
                });

                const result = await view.to_columns();
                expect(result['deg2rad("a")']).toEqual(
                    [30, 60, 90, 120].map((x) => x * (Math.PI / 180)),
                );
                expect(result['deg2rad("b")']).toEqual(
                    [25.5, 45.5, 88.721282, 91.12983].map(
                        (x) => x * (Math.PI / 180),
                    ),
                );
                await view.delete();
                await table.delete();
            });

            test.skip("OG - r2d", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'rad2deg("a")': 'rad2deg("a")',
                        'rad2deg("b")': 'rad2deg("b")',
                    },
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [25.5, 45.5, 88.721282, 91.12983].map(
                        (x) => x * (Math.PI / 180),
                    ),
                });

                const result = await view.to_columns();
                expect(result['rad2deg("a")']).toEqual(
                    [1, 2, 3, 4].map((x) => x * (180 / Math.PI)),
                );
                expect(result['rad2deg("b")']).toEqual([
                    25.5, 45.5, 88.721282, 91.12983,
                ]);
                await view.delete();
                await table.delete();
            });

            test("r2d", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: {
                        'rad2deg("a")': 'rad2deg("a")',
                        'rad2deg("b")': 'rad2deg("b")',
                    },
                });

                await table.update({
                    a: [1, 2, 3, 4],
                    b: [25.5, 45.5, 88.721282, 91.12983].map(
                        (x) => x * (Math.PI / 180),
                    ),
                });

                const result = await view.to_columns();
                expect(result['rad2deg("a")']).toEqual(
                    [1, 2, 3, 4].map((x) => x * (180 / Math.PI)),
                );
                const expected = [25.5, 45.5, 88.721282, 91.12983];
                expect(result['rad2deg("b")'].length).toEqual(expected.length);
                result['rad2deg("b")'].forEach((x, idx) =>
                    expect(x).toBeCloseTo(expected[idx], 5),
                );
                await view.delete();
                await table.delete();
            });

            test("is_null", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'is_null("a")',
                        'is_null("b")',
                        'if(is_null("a")) 100; else 0;',
                        'if(is_null("b")) 100; else 0;',
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [1, null, null, 4],
                    b: [null, 2.5, null, 4.5],
                });

                const result = await view.to_columns();
                expect(result['is_null("a")']).toEqual([
                    false,
                    true,
                    true,
                    false,
                ]);
                expect(result['is_null("b")']).toEqual([
                    true,
                    false,
                    true,
                    false,
                ]);
                expect(result['if(is_null("a")) 100; else 0;']).toEqual([
                    0, 100, 100, 0,
                ]);
                expect(result['if(is_null("b")) 100; else 0;']).toEqual([
                    100, 0, 100, 0,
                ]);
                await view.delete();
                await table.delete();
            });

            test("is_not_null", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'is_not_null("a")',
                        'is_not_null("b")',
                        'if(is_not_null("a")) 100; else 0;',
                        'if(is_not_null("b")) 100; else 0;',
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [1, null, null, 4],
                    b: [null, 2.5, null, 4.5],
                });

                const result = await view.to_columns();
                expect(result['is_not_null("a")']).toEqual([
                    true,
                    false,
                    false,
                    true,
                ]);
                expect(result['is_not_null("b")']).toEqual([
                    false,
                    true,
                    false,
                    true,
                ]);
                expect(result['if(is_not_null("a")) 100; else 0;']).toEqual([
                    100, 0, 0, 100,
                ]);
                expect(result['if(is_not_null("b")) 100; else 0;']).toEqual([
                    0, 100, 0, 100,
                ]);
                await view.delete();
                await table.delete();
            });

            test("coalesce returns first non-null arg", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "integer",
                });

                const view = await table.view({
                    expressions: {
                        coalesce_ab: 'coalesce("a", "b")',
                        coalesce_ab_default: 'coalesce("a", "b", 99)',
                    },
                });

                await table.update({
                    a: [1, null, null, 4],
                    b: [10, 20, null, 40],
                });

                const result = await view.to_columns();
                expect(result["coalesce_ab"]).toEqual([1, 20, null, 4]);
                expect(result["coalesce_ab_default"]).toEqual([1, 20, 99, 4]);
                await view.delete();
                await table.delete();
            });

            test("coalesce promotes mixed numeric inputs to float", async function () {
                const table = await perspective.table({
                    i: "integer",
                    f: "float",
                });

                const view = await table.view({
                    expressions: {
                        coalesce_if: 'coalesce("i", "f")',
                        coalesce_if_default: 'coalesce("i", "f", 0.5)',
                    },
                });

                await table.update({
                    i: [1, null, null, 4],
                    f: [null, 2.5, null, 4.5],
                });

                const result = await view.to_columns();
                const schema = await view.expression_schema();
                expect(schema["coalesce_if"]).toEqual("float");
                expect(schema["coalesce_if_default"]).toEqual("float");
                expect(result["coalesce_if"]).toEqual([1, 2.5, null, 4]);
                expect(result["coalesce_if_default"]).toEqual([1, 2.5, 0.5, 4]);

                await view.delete();
                await table.delete();
            });

            test("coalesce with all-null inputs returns null", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "integer",
                });

                const view = await table.view({
                    expressions: { coalesce_nulls: 'coalesce("a", "b")' },
                });

                await table.update({
                    a: [null, null, null],
                    b: [null, null, null],
                });

                const result = await view.to_columns();
                expect(result["coalesce_nulls"]).toEqual([null, null, null]);
                await view.delete();
                await table.delete();
            });

            test("coalesce fails validation for incompatible types", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "string",
                });

                const validated = await table.validate_expressions([
                    'coalesce("a", "b")',
                    "coalesce(\"a\", 'fallback')",
                ]);

                expect(validated.expression_schema).toEqual({});
                expect(validated.errors['coalesce("a", "b")']).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });

                expect(validated.errors["coalesce(\"a\", 'fallback')"]).toEqual(
                    {
                        column: 0,
                        line: 0,
                        error_message:
                            "Type Error - inputs do not resolve to a valid expression.",
                    },
                );

                await table.delete();
            });

            test("null", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        "float(null)",
                        "string(null)",
                        "integer(null)",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [1, null, null, 4],
                    b: [null, 2.5, null, 4.5],
                });

                const result = await view.to_columns();
                expect(result["float(null)"]).toEqual([null, null, null, null]);
                expect(result["string(null)"]).toEqual([
                    null,
                    null,
                    null,
                    null,
                ]);

                expect(result["integer(null)"]).toEqual([
                    null,
                    null,
                    null,
                    null,
                ]);

                await view.delete();
                await table.delete();
            });

            test("percent_of", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'percent_of("a", 500)',
                        'percent_of("a", "b")',
                        "percent_of(1, 3)",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [100, 200, 300, 400],
                    b: [100.5, 200.5, 300.5, 400.5],
                });

                const result = await view.to_columns();
                expect(result['percent_of("a", 500)']).toEqual(
                    [100, 200, 300, 400].map((x) => (x / 500) * 100),
                );
                expect(result['percent_of("a", "b")']).toEqual(
                    [100, 200, 300, 400].map(
                        (x, idx) => (x / result["b"][idx]) * 100,
                    ),
                );
                expect(result["percent_of(1, 3)"]).toEqual([
                    33.33333333333333, 33.33333333333333, 33.33333333333333,
                    33.33333333333333,
                ]);

                await view.delete();
                await table.delete();
            });

            test("bucket", async function () {
                const table = await perspective.table({
                    a: "integer",
                    b: "float",
                });

                const view = await table.view({
                    expressions: [
                        'bucket("a", 5)',
                        'bucket("b", 2.5)',
                        `bucket("b", 10)`,
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });

                await table.update({
                    a: [15, 15, 35, 40, 1250, 1255],
                    b: [2.25, 2, 3.5, 16.5, 28, 8],
                });

                const result = await view.to_columns();
                expect(result['bucket("a", 5)']).toEqual([
                    15, 15, 35, 40, 1250, 1255,
                ]);
                expect(result['bucket("b", 2.5)']).toEqual([
                    0, 0, 2.5, 15, 27.5, 7.5,
                ]);
                expect(result['bucket("b", 10)']).toEqual([0, 0, 0, 10, 20, 0]);
                await view.delete();
                await table.delete();
            });

            test("random", async function () {
                const table = await perspective.table({
                    x: "float",
                });

                const data = [];
                for (let i = 0; i < 1000; i++) {
                    data.push(i);
                }
                await table.update({ x: data });
                const view = await table.view({
                    expressions: { "random()": "random()" },
                });
                const schema = await view.expression_schema();
                expect(schema).toEqual({ "random()": "float" });
                const result = await view.to_columns();

                for (let i = 0; i < 1000; i++) {
                    const res = result["random()"][i];
                    expect(res >= 0 && res <= 1).toBe(true);
                }

                await view.delete();
                await table.delete();
            });
        });

        test.describe("Booleans", function () {
            test("AND", async function () {
                const table = await perspective.table(common.comparison_data);
                const view = await table.view({
                    expressions: [
                        '"u" and "u"',
                        '"u" and "z"',
                        '"z" and "z"',
                        "0 and 0",
                        "1 and 1",
                        "1 and 100",
                        "true and false",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();
                expect(result['"u" and "u"']).toEqual([
                    false,
                    true,
                    false,
                    true,
                ]);
                expect(result['"u" and "z"']).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result['"u" and "z"']).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result["0 and 0"]).toEqual([false, false, false, false]);
                expect(result["1 and 1"]).toEqual([true, true, true, true]);
                expect(result["1 and 100"]).toEqual([true, true, true, true]);
                expect(result["true and false"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                await view.delete();
                await table.delete();
            });

            test("mand", async function () {
                const table = await perspective.table(common.comparison_data);
                const view = await table.view({
                    expressions: [
                        'mand("u" and "u", "u" and "z", "z" and "z")',
                        "mand(true, true, true, true)",
                        "mand(is_null(null), is_not_null(null))",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();

                expect(
                    result['mand("u" and "u", "u" and "z", "z" and "z")'],
                ).toEqual(Array(4).fill(false));

                expect(
                    result["mand(is_null(null), is_not_null(null))"],
                ).toEqual(Array(4).fill(false));

                expect(result["mand(true, true, true, true)"]).toEqual(
                    Array(4).fill(true),
                );

                await view.delete();
                await table.delete();
            });

            test("mand should fail validation for non boolean inputs", async function () {
                const table = await perspective.table(common.comparison_data);
                const validated = await table.validate_expressions([
                    'mand("w", "x", "y")',
                    "mand(100, 200, 300)",
                    "mand('a', 'b', 'cdef')",
                ]);

                expect(validated.expression_schema).toEqual({});
                expect(validated.errors['mand("w", "x", "y")']).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });
                expect(validated.errors["mand(100, 200, 300)"]).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });
                expect(validated.errors["mand('a', 'b', 'cdef')"]).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });

                await table.delete();
            });

            test("OR", async function () {
                const table = await perspective.table({
                    a: [0, 1, 0, 1],
                    b: [0, 1, 0, 1],
                    c: [0, 0, 0, 0],
                    d: [1, 1, 1, 1],
                    e: [false, false, false, false],
                    f: [true, true, true, true],
                });
                const view = await table.view({
                    expressions: {
                        '"a" or "b"': '"a" or "b"',
                        '"c" or "d"': '"c" or "d"',
                        '"e" or "f"': '"e" or "f"',
                        "0 or 1": "0 or 1",
                        "true or false": "true or false",
                        "false or false": "false or false",
                        filtered: '"a" > 0.5 or "d" < 0.5',
                    },
                });
                const result = await view.to_columns();
                expect(result['"a" or "b"']).toEqual([
                    false,
                    true,
                    false,
                    true,
                ]);
                expect(result['"c" or "d"']).toEqual([true, true, true, true]);
                expect(result['"e" or "f"']).toEqual([true, true, true, true]);
                expect(result["0 or 1"]).toEqual([true, true, true, true]);
                expect(result["true or false"]).toEqual([
                    true,
                    true,
                    true,
                    true,
                ]);
                expect(result["false or false"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result["filtered"]).toEqual([true, true, true, true]);
                await view.delete();
                await table.delete();
            });

            test("mor", async function () {
                const table = await perspective.table(common.comparison_data);
                const view = await table.view({
                    expressions: [
                        'mor("u" and "u", "u" and "z", "z" and "z")',
                        "mor(false, false, false)",
                        "mor(false, true, false)",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();
                expect(
                    result['mor("u" and "u", "u" and "z", "z" and "z")'],
                ).toEqual([true, true, true, true]);

                // The boolean false is a false
                expect(result["mor(false, false, false)"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);

                expect(result["mor(false, true, false)"]).toEqual([
                    true,
                    true,
                    true,
                    true,
                ]);

                await view.delete();
                await table.delete();
            });

            test("mor should fail validation for non boolean inputs", async function () {
                const table = await perspective.table(common.comparison_data);
                const validated = await table.validate_expressions([
                    'mor("w", "x", "y")',
                    "mor(100, 200, 300)",
                    "mor('a', 'b', 'cdef')",
                ]);

                expect(validated.expression_schema).toEqual({});
                expect(validated.errors['mor("w", "x", "y")']).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });
                expect(validated.errors["mor(100, 200, 300)"]).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });
                expect(validated.errors["mor('a', 'b', 'cdef')"]).toEqual({
                    column: 0,
                    line: 0,
                    error_message:
                        "Type Error - inputs do not resolve to a valid expression.",
                });

                await table.delete();
            });

            test("NAND", async function () {
                const table = await perspective.table(common.comparison_data);
                const view = await table.view({
                    expressions: [
                        '"u" nand "u"',
                        '"u" nand "z"',
                        '"z" nand "z"',
                        "0 nand 0",
                        "1 nand 1",
                        "1 nand 100",
                        "true nand true",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();
                expect(result['"u" nand "u"']).toEqual([
                    true,
                    false,
                    true,
                    false,
                ]);
                expect(result['"u" nand "z"']).toEqual([
                    true,
                    true,
                    true,
                    true,
                ]);
                expect(result['"u" nand "z"']).toEqual([
                    true,
                    true,
                    true,
                    true,
                ]);
                expect(result["0 nand 0"]).toEqual([true, true, true, true]);
                expect(result["1 nand 1"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result["1 nand 100"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result["true nand true"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                await view.delete();
                await table.delete();
            });

            test("NOR", async function () {
                const table = await perspective.table({
                    a: [0, 1, 0, 1],
                    b: [0, 1, 0, 1],
                    c: [0, 0, 0, 0],
                    d: [1, 1, 1, 1],
                    e: [false, false, false, false],
                    f: [true, true, true, true],
                });
                const view = await table.view({
                    expressions: [
                        '"a" nor "b"',
                        '"c" nor "d"',
                        '"e" nor "f"',
                        "0 nor 1",
                        "false nor false",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();
                expect(result['"a" nor "b"']).toEqual([
                    true,
                    false,
                    true,
                    false,
                ]);
                expect(result['"c" nor "d"']).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result['"e" nor "f"']).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result["0 nor 1"]).toEqual([false, false, false, false]);
                expect(result["false nor false"]).toEqual([
                    true,
                    true,
                    true,
                    true,
                ]);
                await view.delete();
                await table.delete();
            });

            test("XOR", async function () {
                const table = await perspective.table({
                    a: [0, 1, 0, 1],
                    b: [0, 1, 0, 1],
                    c: [0, 0, 0, 0],
                    d: [1, 1, 1, 1],
                    e: [false, false, false, false],
                    f: [true, true, true, true],
                });
                const view = await table.view({
                    expressions: [
                        '"a" xor "b"',
                        '"c" xor "d"',
                        '"e" xor "f"',
                        "0 xor 1",
                        "false xor false",
                    ].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
                });
                const result = await view.to_columns();
                expect(result['"a" xor "b"']).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                expect(result['"c" xor "d"']).toEqual([true, true, true, true]);
                expect(result['"e" xor "f"']).toEqual([true, true, true, true]);
                expect(result["0 xor 1"]).toEqual([true, true, true, true]);
                expect(result["false xor false"]).toEqual([
                    false,
                    false,
                    false,
                    false,
                ]);
                await view.delete();
                await table.delete();
            });
        });
    });
})(perspective);
