// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 fs from "node:fs";

import { createRequire } from "node:module";

const require = createRequire(import.meta.url);

const arr = fs.readFileSync(
    require.resolve("superstore-arrow/superstore.lz4.arrow"),
).buffer;

/**
 * Run a function in a loop, comparing before-and-after wasm heap for leaks.
 * Emscripten allocates in pages, and Perspective is hard coded to start at
 * 16777216b, so rather than check `end - start`, we just test that any
 * allocation ever occurs.
 * @param {*} test
 * @param {*} num_iterations
 */
async function leak_test(test, num_iterations = 10_000) {
    // warmup
    await test();
    await test();

    // TODO Playwright uses the same host instance so this may have grown by
    // the time the suite runs. Could fix with a nod eagent (and test in other
    // browsers).

    const start = (await perspective.system_info()).heap_size;
    const start_used = (await perspective.system_info()).used_size;

    for (var i = 0; i < num_iterations; i++) {
        await test();
    }

    const final_used = (await perspective.system_info()).used_size;
    expect((await perspective.system_info()).heap_size).toEqual(start);
    expect(Number(final_used) / Number(start_used)).toBeGreaterThanOrEqual(0.8);
    expect(Number(final_used) / Number(start_used)).toBeLessThanOrEqual(1.5);
}

/**
 * Given columns a (int), b (float), c (string) and d (datetime),
 * generate expressions that use all columns and scalar values.
 */
function generate_expressions() {
    const expressions = {
        "concat('abcd', \"c\", 'efg')": "concat('abcd', \"c\", 'efg')",
    };

    for (const op of ["+", "-", "*", "/", "^", "%"]) {
        expressions[`("a" ${op} "b") + ${Math.floor(Math.random() * 100)}`] =
            `("a" ${op} "b") + ${Math.floor(Math.random() * 100)}`;
    }

    for (const fn of ["sqrt", "log10", "deg2rad"]) {
        expressions[`${fn}("b")`] = `${fn}("b")`;
    }

    for (const fn of ["upper", "lower", "length"]) {
        expressions[`${fn}("c")`] = `${fn}("c")`;
    }

    for (const unit of ["m", "D"]) {
        expressions[`bucket("d", '${unit}')`] = `bucket("d", '${unit}')`;
    }

    const rand =
        Object.keys(expressions)[
            Math.floor(Math.random() * Object.keys(expressions).length)
        ];

    return { [rand]: expressions[rand] };
}

test.describe("leaks", function () {
    test.describe("view", function () {
        test.describe("1-sided", function () {
            test("to_json does not leak", async () => {
                test.setTimeout(60000);
                const table = await perspective.table(arr.slice());
                const view = await table.view({ group_by: ["State"] });
                await leak_test(async function () {
                    let json = await view.to_json();
                    expect(json.length).toEqual(50);
                });
                await view.delete();
                await table.delete();
            });

            test.skip("OG - to_columns_string does not leak", async () => {
                const table = await perspective.table(arr.slice());
                const view = await table.view({ group_by: ["State"] });
                await leak_test(async function () {
                    let json = await view.to_columns_string();
                    expect(json.length).toEqual(6722);
                });
                await view.delete();
                await table.delete();
            });

            // The length of this string changed due to
            // some of the trailing fractional digits in the floats.
            test("to_columns_string does not leak", async () => {
                const table = await perspective.table(arr.slice());
                const view = await table.view({ group_by: ["State"] });
                await leak_test(async function () {
                    let json = await view.to_columns_string();
                    expect(json.length).toEqual(6669);
                });
                await view.delete();
                await table.delete();
            });
        });
    });

    test.describe("table", function () {
        test("update does not leak", async () => {
            const table = await perspective.table(
                { x: "integer", y: "string" },
                { index: "x" },
            );
            let count = 0;
            const view = await table.view();
            view.on_update(function () {
                count += 1;
            });

            await table.update([{ x: 1, y: "TestTestTest" }]);
            await leak_test(async function () {
                table.update([{ x: 1, y: "TestTestTest" }]);
                expect(await table.size()).toEqual(1);
            });

            // await table.update([{ x: 1, y: "TestTestTest" }]);

            expect(count).toBeGreaterThan(0);
            await view.delete();
            await table.delete();
        });

        test.skip("csv loading does not leak", async () => {
            const table = await perspective.table(arr.slice());
            const view = await table.view();
            const csv = await view.to_csv({ end_row: 10 });
            await view.delete();
            await table.delete();
            await leak_test(async function () {
                const table = await perspective.table(csv);
                expect(await table.size()).toEqual(10);
                await table.delete();
            });
        });
    });

    test.describe("expression columns", function () {
        test("0 sided does not leak", async () => {
            test.setTimeout(60000);
            const table = await perspective.table({
                a: [1, 2, 3, 4],
                b: [1.5, 2.5, 3.5, 4.5],
                c: ["a", "b", "c", "d"],
                d: [new Date(), new Date(), new Date(), new Date()],
            });

            const expressions = generate_expressions();

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

                const expression_schema = await view.expression_schema();
                expect(Object.keys(expression_schema).length).toEqual(
                    Object.keys(expressions).length,
                );

                await view.delete();
            });

            await table.delete();
        });

        /**
         * Because the expression vocab and the regex cache is per-table and
         * not per-view, we should be able to leak test the table creation
         * and view creation.
         */
        test.skip("0 sided regex does not leak", async () => {
            const expressions = {
                "match(\"a\", '.{1}')": "match(\"a\", '.{1}')",
                "match_all(\"a\", '[a-z]{1}')": "match_all(\"a\", '[a-z]{1}')",
                "search(\"a\", '.')": "search(\"a\", '.')",
            };

            await leak_test(async () => {
                const table = await perspective.table({
                    a: "abcdefghijklmnopqrstuvwxyz".split(""),
                });

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

                const expression_schema = await view.expression_schema();
                expect(Object.keys(expression_schema).length).toEqual(
                    Object.keys(expressions).length,
                );

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

        test.skip("0 sided string does not leak", async () => {
            const table = await perspective.table({
                a: "abcdefghijklmnopqrstuvwxyz".split(""),
            });

            const expressions = {
                "var x := 'abcdefghijklmnopqrstuvwxyz'; concat(\"a\", x, 'abc')":
                    "var x := 'abcdefghijklmnopqrstuvwxyz'; concat(\"a\", x, 'abc')",
                "var x := 'abcdefghijklmnopqrstuvwxyz'; var y := 'defhijklmnopqrst'; concat(\"a\", x, 'abc', y)":
                    "var x := 'abcdefghijklmnopqrstuvwxyz'; var y := 'defhijklmnopqrst'; concat(\"a\", x, 'abc', y)",
            };

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

                const expression_schema = await view.expression_schema();
                expect(Object.keys(expression_schema).length).toEqual(
                    Object.keys(expressions).length,
                );

                await view.delete();
            });

            await table.delete();
        });

        test("1 sided does not leak", async () => {
            const table = await perspective.table({
                a: [1, 2, 3, 4],
                b: [1.5, 2.5, 3.5, 4.5],
                c: ["a", "b", "c", "d"],
                d: [new Date(), new Date(), new Date(), new Date()],
            });

            const columns = ["a", "b", "c", "d"];
            const expressions = generate_expressions();
            await leak_test(async () => {
                const view = await table.view({
                    group_by: [
                        columns[Math.floor(Math.random() * columns.length)],
                    ],
                    expressions,
                });

                const expression_schema = await view.expression_schema();
                expect(Object.keys(expression_schema).length).toEqual(
                    Object.keys(expressions).length,
                );

                await view.delete();
            }, 3000);

            await table.delete();
        });

        test("2 sided does not leak", async () => {
            const table = await perspective.table({
                a: [1, 2, 3, 4],
                b: [1.5, 2.5, 3.5, 4.5],
                c: ["a", "b", "c", "d"],
                d: [new Date(), new Date(), new Date(), new Date()],
            });

            const columns = ["a", "b", "c", "d"];
            const expressions = generate_expressions();
            await leak_test(async () => {
                const view = await table.view({
                    group_by: [
                        columns[Math.floor(Math.random() * columns.length)],
                    ],
                    split_by: [
                        columns[Math.floor(Math.random() * columns.length)],
                    ],
                    expressions,
                });

                const expression_schema = await view.expression_schema();
                expect(Object.keys(expression_schema).length).toEqual(
                    Object.keys(expressions).length,
                );

                await view.delete();
            }, 3000);

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