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

let server;
let port;

test.describe("WebSocketManager", function () {
    test.beforeEach(() => {
        server = new perspective.WebSocketServer({ port: 0 });
        port = server._server.address().port;
    });

    test.afterEach(() => {
        server.close();
    });

    test("sends initial data client on subscribe", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        const client_view = await client_table.view();
        const client_data = await client_view.to_json();
        expect(client_data).toEqual(data);
        await client.terminate();

        // // TODO Can't await delete here because `terminate()` doesn't
        // // guarantee server-side cleanup happens immediately. So this test
        // // leaks a small `Table` sometimes, as do several others in this
        // // suite.

        await new Promise((x) => setTimeout(x, 10));
        await table.delete();
    });

    test("Throws an exception when the server is detached", async () => {
        const data = [{ x: 1 }];
        const _table = await perspective.table(data, { name: "test2" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test2");
        const client_view = await client_table.view();
        await server.close();
        const client_data = client_view.to_json();
        await expect(client_data).rejects.toThrow();
    });

    test("passes back errors from server", async () => {
        expect.assertions(2);
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        client_table.view({ columns: ["z"] }).catch((error) => {
            expect(error.message).toContain(
                "Abort(): Invalid column 'z' found in View columns.\n",
            );
        });

        const client_view = await client_table.view();
        const client_data = await client_view.to_json();
        expect(client_data).toEqual(data);
        await client.terminate();

        await new Promise((x) => setTimeout(x, 10));
        await table.delete();
    });

    test("sends initial data multiple client on subscribe", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client_1 = await perspective.websocket(`ws://localhost:${port}`);
        const client_2 = await perspective.websocket(`ws://localhost:${port}`);
        const client_1_table = await client_1.open_table("test");
        const client_2_table = await client_2.open_table("test");
        const client_1_view = await client_1_table.view();
        const client_2_view = await client_2_table.view();
        const client_1_data = await client_1_view.to_json();
        const client_2_data = await client_2_view.to_json();
        await client_1.terminate();
        await client_2.terminate();
        expect(client_1_data).toEqual(data);
        expect(client_2_data).toEqual(data);

        await new Promise((x) => setTimeout(x, 10));
        await table.delete();
    });

    test("passes back errors with multiple client on subscribe", async () => {
        expect.assertions(3);
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client_1 = await perspective.websocket(`ws://localhost:${port}`);
        const client_2 = await perspective.websocket(`ws://localhost:${port}`);
        const client_1_table = await client_1.open_table("test");
        const client_2_table = await client_2.open_table("test");
        client_1_table.view({ columns: ["z"] }).catch((error) => {
            expect(error.message).toContain(
                "Abort(): Invalid column 'z' found in View columns.\n",
            );
        });

        const client_1_view = await client_1_table.view();
        const client_2_view = await client_2_table.view();
        const client_1_data = await client_1_view.to_json();
        const client_2_data = await client_2_view.to_json();
        await client_1.terminate();
        await client_2.terminate();
        expect(client_1_data).toEqual(data);
        expect(client_2_data).toEqual(data);

        await new Promise((x) => setTimeout(x, 10));
        await table.delete();
    });

    test("sends updates to client on subscribe", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        const client_view = await client_table.view();
        let done;
        let result = new Promise((x) => {
            done = x;
        });

        const on_update = () => {
            client_view.to_json().then(async (updated_data) => {
                await client_view.delete();
                await table.delete();
                expect(updated_data).toEqual([{ x: 1 }, { x: 2 }]);
                await client.terminate();
                setTimeout(done);
            });
        };

        client_view.on_update(on_update);
        client_view.to_json().then((client_data) => {
            expect(client_data).toEqual(data);
            return table.update([{ x: 2 }]);
        });

        await result;
    });

    test("Calls `update` and sends arraybuffers using `binary_length`", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const view = await table.view();
        const arrow = await view.to_arrow();
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        await client_table.update(arrow);
        const client_view = await client_table.view();
        const client_data = await client_view.to_json();
        expect(client_data).toEqual([{ x: 1 }, { x: 1 }]);
        await client_view.delete();
        await client.terminate();
        await view.delete();
        await table.delete();
    });

    test("Calls `update` and sends `Uint8Array` using `binary_length` multiple times", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const view = await table.view();
        const arrow = await view.to_arrow();
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        await client_table.update(arrow);
        await client_table.update(arrow);
        await client_table.update(arrow);
        const client_view = await client_table.view();
        const client_data = await client_view.to_json();
        expect(client_data).toEqual([{ x: 1 }, { x: 1 }, { x: 1 }, { x: 1 }]);
        await client_view.delete();
        await view.delete();
        await table.delete();
    });

    test("Calls `update` and sends `Uint8Array` using `on_update`", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const view = await table.view();
        const arrow = await view.to_arrow();
        let update_port;
        let done;
        let result = new Promise((x) => {
            done = x;
        });

        const updater = async (updated) => {
            expect(updated.port_id).toEqual(update_port);
            expect(updated.delta instanceof Uint8Array).toEqual(true);
            expect(updated.delta.byteLength).toBeGreaterThan(0);
            await view.delete();
            await table.delete();
            await client.terminate();

            done();
        };

        view.on_update(updater, { mode: "row" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const client_table = await client.open_table("test");
        for (let i = 0; i < 5; i++) {
            // take up some ports on the remote table
            await client_table.make_port();
        }

        update_port = await client_table.make_port();
        await client_table.update(arrow, { port_id: update_port });
        await result;
    });

    // TODO ping loop is not yet implemented
    test.skip("disables ping loop on disconnect", async () => {
        const data = [{ x: 1 }];
        const table = await perspective.table(data, { name: "test" });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const _client_table = await client.open_table("test");
        expect(await _client_table.size()).toEqual(1);
        expect(client._ping_loop).toBeDefined();
        await server.close();
        expect(client._ping_loop).toBeUndefined();
        await table.delete();
    });

    test("Can get hosted table names", async () => {
        const data = [{ x: 1 }];
        const host_table_name = "test";
        const table = await perspective.table(data, { name: host_table_name });
        const client = await perspective.websocket(`ws://localhost:${port}`);
        const table_names = await client.get_hosted_table_names();
        expect(table_names).toContain(host_table_name);
        await client.terminate();
        await table.delete();
    });
});
