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

const data = {
    w: [1.5, 2.5, 3.5, 4.5],
    x: [1, 2, 3, 4],
    y: ["a", "b", "c", "d"],
    z: [true, false, true, false],
};

const get_random_int = function (min, max) {
    // liberally copied from stack
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
};

function it_old_behavior(name, capture) {
    test(name, async function () {
        let done;
        let result = new Promise((x) => {
            done = x;
        });

        await capture(done);
        await result;
    });
}

((perspective) => {
    test.describe("ports", function () {
        test("Should create port IDs in incremental order", async function () {
            const table = await perspective.table(data);
            const port_ids = [];
            for (let i = 0; i < 10; i++) {
                port_ids.push(await table.make_port());
            }
            expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
            table.delete();
        });

        test("Should create port IDs in incremental order and allow updates on each port", async function () {
            const table = await perspective.table(data);
            const port_ids = [];

            for (let i = 0; i < 10; i++) {
                port_ids.push(await table.make_port());
            }

            expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

            for (const port_id of port_ids) {
                await table.update(
                    {
                        w: [1.5],
                        x: [port_id],
                        y: ["d"],
                        z: [true],
                    },
                    { port_id },
                );
            }

            const view = await table.view();
            const output = await view.to_columns();

            expect(await table.size()).toEqual(14);

            const expected = {
                w: [
                    1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
                    1.5, 1.5,
                ],
                x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                y: [
                    "a",
                    "b",
                    "c",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                    "d",
                ],
                z: [
                    true,
                    false,
                    true,
                    false,
                    true,
                    true,
                    true,
                    true,
                    true,
                    true,
                    true,
                    true,
                    true,
                    true,
                ],
            };

            expect(output).toEqual(expected);

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

        test("Should create port IDs in incremental order and allow updates on each port, indexed", async function () {
            const table = await perspective.table(data, { index: "w" });
            const port_ids = [];

            for (let i = 0; i < 10; i++) {
                port_ids.push(await table.make_port());
            }

            expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

            for (const port_id of port_ids) {
                await table.update(
                    {
                        w: [1.5],
                        x: [1],
                        y: ["a"],
                        z: [true],
                    },
                    { port_id },
                );
            }

            expect(await table.size()).toEqual(4);

            const view = await table.view();
            const output = await view.to_columns();
            expect(output).toEqual(data);

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

        test("Should create port IDs in incremental order and allow random arbitrary updates on each port, indexed", async function () {
            const table = await perspective.table(data, { index: "w" });
            const port_ids = [];

            for (let i = 0; i < 10; i++) {
                port_ids.push(await table.make_port());
            }

            expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

            for (const port_id of port_ids) {
                await table.update(
                    {
                        w: [1.5],
                        x: [1],
                        y: ["a"],
                        z: [true],
                    },
                    { port_id },
                );
            }

            expect(await table.size()).toEqual(4);

            const view = await table.view();
            const output = await view.to_columns();
            expect(output).toEqual(data);

            // arbitarily update from a random port, force append with new pkey
            const port = get_random_int(1, 9);
            const update_data = {
                w: [5.5],
                x: [5],
                y: ["e"],
                z: [true],
            };

            await table.update(update_data, { port_id: 0 });

            const output2 = await view.to_columns();
            expect(output2).toEqual({
                w: [1.5, 2.5, 3.5, 4.5, 5.5],
                x: [1, 2, 3, 4, 5],
                y: ["a", "b", "c", "d", "e"],
                z: [true, false, true, false, true],
            });

            // and do it again but this time with null as pkey
            let port2 = get_random_int(1, 9);

            while (port2 === port) {
                port2 = get_random_int(1, 9);
            }

            const update_data2 = {
                w: [null],
                x: [6],
                y: ["f"],
                z: [true],
            };

            await table.update(update_data2, { port_id: port2 });
            const output3 = await view.to_columns();
            expect(output3).toEqual({
                w: [null, 1.5, 2.5, 3.5, 4.5, 5.5],
                x: [6, 1, 2, 3, 4, 5],
                y: ["f", "a", "b", "c", "d", "e"],
                z: [true, true, false, true, false, true],
            });

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

        test.describe("View notifications from different ports", function () {
            test("All views should be notified by appends on all ports", async function () {
                const table = await perspective.table(data);
                const port_ids = [];

                for (let i = 0; i < 10; i++) {
                    port_ids.push(await table.make_port());
                }

                expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                const view = await table.view();

                const view2 = await table.view({
                    group_by: ["w"],
                });

                const view3 = await table.view({
                    group_by: ["w"],
                    split_by: ["x"],
                });

                for (const port_id of port_ids) {
                    await table.update(
                        {
                            w: [1.5],
                            x: [port_id],
                            y: ["d"],
                            z: [true],
                        },
                        { port_id },
                    );
                }

                const output = await view.to_columns();

                expect(await table.size()).toEqual(14);

                const expected = {
                    w: [
                        1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
                        1.5, 1.5, 1.5,
                    ],
                    x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                    y: [
                        "a",
                        "b",
                        "c",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                        "d",
                    ],
                    z: [
                        true,
                        false,
                        true,
                        false,
                        true,
                        true,
                        true,
                        true,
                        true,
                        true,
                        true,
                        true,
                        true,
                        true,
                    ],
                };

                expect(output).toEqual(expected);

                const output2 = await view2.to_columns();
                expect(output2).toEqual({
                    __ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
                    w: [27, 16.5, 2.5, 3.5, 4.5],
                    x: [65, 56, 2, 3, 4],
                    y: [14, 11, 1, 1, 1],
                    z: [14, 11, 1, 1, 1],
                });

                const output3 = await view3.to_columns();
                expect(output3).toEqual({
                    __ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
                    "1|w": [3, 3, null, null, null],
                    "1|x": [2, 2, null, null, null],
                    "1|y": [2, 2, null, null, null],
                    "1|z": [2, 2, null, null, null],
                    "2|w": [4, 1.5, 2.5, null, null],
                    "2|x": [4, 2, 2, null, null],
                    "2|y": [2, 1, 1, null, null],
                    "2|z": [2, 1, 1, null, null],
                    "3|w": [5, 1.5, null, 3.5, null],
                    "3|x": [6, 3, null, 3, null],
                    "3|y": [2, 1, null, 1, null],
                    "3|z": [2, 1, null, 1, null],
                    "4|w": [6, 1.5, null, null, 4.5],
                    "4|x": [8, 4, null, null, 4],
                    "4|y": [2, 1, null, null, 1],
                    "4|z": [2, 1, null, null, 1],
                    "5|w": [1.5, 1.5, null, null, null],
                    "5|x": [5, 5, null, null, null],
                    "5|y": [1, 1, null, null, null],
                    "5|z": [1, 1, null, null, null],
                    "6|w": [1.5, 1.5, null, null, null],
                    "6|x": [6, 6, null, null, null],
                    "6|y": [1, 1, null, null, null],
                    "6|z": [1, 1, null, null, null],
                    "7|w": [1.5, 1.5, null, null, null],
                    "7|x": [7, 7, null, null, null],
                    "7|y": [1, 1, null, null, null],
                    "7|z": [1, 1, null, null, null],
                    "8|w": [1.5, 1.5, null, null, null],
                    "8|x": [8, 8, null, null, null],
                    "8|y": [1, 1, null, null, null],
                    "8|z": [1, 1, null, null, null],
                    "9|w": [1.5, 1.5, null, null, null],
                    "9|x": [9, 9, null, null, null],
                    "9|y": [1, 1, null, null, null],
                    "9|z": [1, 1, null, null, null],
                    "10|w": [1.5, 1.5, null, null, null],
                    "10|x": [10, 10, null, null, null],
                    "10|y": [1, 1, null, null, null],
                    "10|z": [1, 1, null, null, null],
                });

                view3.delete();
                view2.delete();
                view.delete();
                table.delete();
            });

            test("All views should be notified by partial updates on all ports", async function () {
                const table = await perspective.table(data, { index: "w" });
                const port_ids = [];

                for (let i = 0; i < 10; i++) {
                    port_ids.push(await table.make_port());
                }

                expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                const view = await table.view();

                const view2 = await table.view({
                    group_by: ["w"],
                });

                const view3 = await table.view({
                    group_by: ["w"],
                    split_by: ["x"],
                });

                for (const port_id of port_ids) {
                    await table.update(
                        {
                            w: [1.5],
                            x: [port_id],
                            y: ["d"],
                            z: [false],
                        },
                        { port_id },
                    );
                }

                const output = await view.to_columns();

                expect(await table.size()).toEqual(4);

                const expected = {
                    w: [1.5, 2.5, 3.5, 4.5],
                    x: [10, 2, 3, 4],
                    y: ["d", "b", "c", "d"],
                    z: [false, false, true, false],
                };

                expect(output).toEqual(expected);

                const output2 = await view2.to_columns();
                expect(output2).toEqual({
                    __ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
                    w: [12, 1.5, 2.5, 3.5, 4.5],
                    x: [19, 10, 2, 3, 4],
                    y: [4, 1, 1, 1, 1],
                    z: [4, 1, 1, 1, 1],
                });

                const output3 = await view3.to_columns();
                expect(output3).toEqual({
                    __ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
                    "2|w": [2.5, null, 2.5, null, null],
                    "2|x": [2, null, 2, null, null],
                    "2|y": [1, null, 1, null, null],
                    "2|z": [1, null, 1, null, null],
                    "3|w": [3.5, null, null, 3.5, null],
                    "3|x": [3, null, null, 3, null],
                    "3|y": [1, null, null, 1, null],
                    "3|z": [1, null, null, 1, null],
                    "4|w": [4.5, null, null, null, 4.5],
                    "4|x": [4, null, null, null, 4],
                    "4|y": [1, null, null, null, 1],
                    "4|z": [1, null, null, null, 1],
                    "10|w": [1.5, 1.5, null, null, null],
                    "10|x": [10, 10, null, null, null],
                    "10|y": [1, 1, null, null, null],
                    "10|z": [1, 1, null, null, null],
                });

                view3.delete();
                view2.delete();
                view.delete();
                table.delete();
            });
        });

        test.describe("on_update notifications from different ports", function () {
            it_old_behavior(
                "All calls to on_update should contain the port ID",
                async function (done) {
                    const table = await perspective.table(data);
                    const port_ids = [];

                    for (let i = 0; i < 10; i++) {
                        port_ids.push(await table.make_port());
                    }

                    expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                    const view = await table.view();

                    // because no updates were called in port 0, start at 1
                    let last_port_id = 1;
                    view.on_update(function (updated) {
                        expect(updated.port_id).toEqual(last_port_id);
                        if (last_port_id == 10) {
                            view.delete();
                            table.delete();
                            done();
                        } else {
                            last_port_id++;
                        }
                    });

                    for (const port_id of port_ids) {
                        await table.update(
                            {
                                w: [1.5],
                                x: [1],
                                y: ["a"],
                                z: [true],
                            },
                            { port_id },
                        );
                    }
                },
            );

            it_old_behavior(
                "Only the port that was updated should be notified in on_update",
                async function (done) {
                    const table = await perspective.table(data);
                    const port_ids = [];

                    for (let i = 0; i < 10; i++) {
                        port_ids.push(await table.make_port());
                    }

                    expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                    const view = await table.view();

                    const port_id = get_random_int(1, 9);

                    view.on_update(function (updated) {
                        expect(updated.port_id).toEqual(port_id);
                        view.delete();
                        table.delete();
                        done();
                    });

                    await table.update(
                        {
                            w: [1.5],
                            x: [1],
                            y: ["a"],
                            z: [true],
                        },
                        { port_id },
                    );
                },
            );

            it_old_behavior(
                "All ports should be notified in creation order, regardless of what order the update is called.",
                async function (done) {
                    const table = await perspective.table(data);
                    const port_ids = [];

                    for (let i = 0; i < 10; i++) {
                        port_ids.push(await table.make_port());
                    }

                    expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                    const view = await table.view();

                    let last_port_id = 0;
                    let num_updates = 0;

                    view.on_update(function (updated) {
                        expect(updated.port_id).toEqual(last_port_id);
                        if (num_updates === 10) {
                            view.delete();
                            table.delete();
                            done();
                        } else {
                            num_updates++;
                            last_port_id++;
                        }
                    });

                    const update_order = _.shuffle([
                        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                    ]);

                    for (const port_id of update_order) {
                        table.update(
                            {
                                w: [1.5],
                                x: [1],
                                y: ["a"],
                                z: [true],
                            },
                            { port_id },
                        );
                    }
                },
            );
        });

        test.describe("deltas", function () {
            it_old_behavior(
                "Deltas should be unique to each port",
                async function (done) {
                    const table = await perspective.table(data);
                    const port_ids = [];

                    for (let i = 0; i < 10; i++) {
                        port_ids.push(await table.make_port());
                    }

                    expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

                    const view = await table.view();

                    let update_count = 0;

                    view.on_update(
                        async function (updated) {
                            const _t = await perspective.table(updated.delta);
                            const _v = await _t.view();
                            const result = await _v.to_columns();

                            if (updated.port_id == first_port) {
                                expect(result).toEqual({
                                    w: [100],
                                    x: [first_port],
                                    y: ["first port"],
                                    z: [false],
                                });
                            } else if (updated.port_id == second_port) {
                                expect(result).toEqual({
                                    w: [200],
                                    x: [second_port],
                                    y: ["second port"],
                                    z: [true],
                                });
                            }

                            update_count++;

                            if (update_count === 2) {
                                _v.delete();
                                _t.delete();
                                view.delete();
                                table.delete();
                                done();
                            }
                        },
                        { mode: "row" },
                    );

                    const first_port = get_random_int(0, 10);
                    let second_port = get_random_int(0, 10);

                    while (second_port === first_port) {
                        second_port = get_random_int(0, 10);
                    }

                    await table.update(
                        {
                            w: [100],
                            x: [first_port],
                            y: ["first port"],
                            z: [false],
                        },
                        { port_id: first_port },
                    );

                    await table.update(
                        {
                            w: [200],
                            x: [second_port],
                            y: ["second port"],
                            z: [true],
                        },
                        { port_id: second_port },
                    );
                },
            );
        });

        test.describe("Cross-table port operations", function () {
            it_old_behavior(
                "Should allow a client-server round trip without loops",
                async function (done) {
                    const client_table = await perspective.table(data);
                    const server_table = await perspective.table(data);
                    const client_port = await client_table.make_port();

                    // "get rid" of a random number of ports by creating them
                    for (let i = 0; i < get_random_int(5, 20); i++) {
                        await server_table.make_port();
                    }

                    const server_port = await server_table.make_port();

                    expect(client_port).toBeLessThan(server_port);

                    const client_view = await client_table.view();

                    let CLIENT_TO_SERVER_UPDATES = 0;

                    client_view.on_update(
                        async (updated) => {
                            if (updated.port_id === client_port) {
                                await server_table.update(updated.delta, {
                                    port_id: server_port,
                                });
                                CLIENT_TO_SERVER_UPDATES++;
                            } else {
                                // Should not pass forward that update, and break
                                // the chain.
                                expect(CLIENT_TO_SERVER_UPDATES).toEqual(1);
                            }
                        },
                        { mode: "row" },
                    );

                    const server_view = await server_table.view();

                    server_view.on_update(
                        async (updated) => {
                            if (updated.port_id !== server_port) {
                                await client_table.update(updated.delta);
                            } else {
                                // update is trapped in this state - assert correct
                                // state and exit the test.
                                expect(await client_table.size()).toEqual(8);
                                expect(await server_table.size()).toEqual(8);
                                client_view.delete();
                                server_view.delete();
                                client_table.delete();
                                server_table.delete();
                                done();
                            }
                        },
                        { mode: "row" },
                    );

                    // this should go to the server and round trip back to the
                    // client, but stop when it gets to the client again.
                    await client_table.update(data, { port_id: client_port });
                },
            );

            it_old_behavior(
                "Should allow multi client-server round trips without loops",
                async function (done) {
                    const client_table = await perspective.table(data, {
                        index: "w",
                    });
                    const client_table2 = await perspective.table(data, {
                        index: "w",
                    });
                    const server_table = await perspective.table(data, {
                        index: "w",
                    });
                    const client_port = await client_table.make_port();
                    const client_port2 = await client_table2.make_port();

                    // "get rid" of a random number of ports by creating them
                    for (let i = 0; i < get_random_int(5, 20); i++) {
                        await server_table.make_port();
                    }

                    const server_port = await server_table.make_port();
                    const server_port2 = await server_table.make_port();

                    expect(client_port).toBeLessThan(server_port);
                    expect(client_port2).toBeLessThan(server_port);
                    expect(client_port).toBeLessThan(server_port2);
                    expect(client_port2).toBeLessThan(server_port2);

                    // port numbers can be equal between clients
                    expect(client_port2).toEqual(client_port);

                    const client_view = await client_table.view();
                    const client_view2 = await client_table2.view();

                    client_view.on_update(
                        async (updated) => {
                            // client1 and server handlers are as minimal as can be
                            if (updated.port_id === client_port) {
                                await server_table.update(updated.delta, {
                                    port_id: server_port,
                                });
                            }
                        },
                        { mode: "row" },
                    );

                    client_view2.on_update(
                        async (updated) => {
                            // client2 only reads updates from other clients
                            const results = await client_view2.to_columns();
                            expect(results).toEqual({
                                w: [1.5, 2.5, 3.5, 4.5],
                                x: [1, 2, 300, 4],
                                y: ["a", "b", "ccc", "d"],
                                z: [true, false, true, false],
                            });
                            expect(await client_view.to_columns()).toEqual(
                                results,
                            );
                            expect(await server_view.to_columns()).toEqual(
                                results,
                            );
                            client_view.delete();
                            client_view2.delete();
                            server_view.delete();
                            client_table.delete();
                            client_table2.delete();
                            server_table.delete();
                            done();
                        },
                        { mode: "row" },
                    );

                    const server_view = await server_table.view();

                    // Simulate multiple connections to the server
                    server_view.on_update(
                        async (updated) => {
                            if (updated.port_id !== server_port) {
                                await client_table.update(updated.delta);
                            }
                        },
                        { mode: "row" },
                    );

                    server_view.on_update(
                        async (updated) => {
                            if (updated.port_id !== server_port2) {
                                await client_table2.update(updated.delta);
                            }
                        },
                        { mode: "row" },
                    );

                    // this should go to the server and round trip back to the
                    // client, but stop when it gets to the client again. It should
                    // reflect on the client2 table though.
                    await client_table.update(
                        [
                            {
                                w: 3.5,
                                x: 300,
                                y: "ccc",
                            },
                        ],
                        { port_id: client_port },
                    );
                },
            );
        });
    });
})(perspective);
