/** @odoo-module */

import { nextTick } from "@web/../tests/helpers/utils";
import { LoadingDataError } from "@spreadsheet/o_spreadsheet/errors";
import { BatchEndpoint, Request, ServerData } from "@spreadsheet/data_sources/server_data";

QUnit.module("spreadsheet server data", {}, () => {
    QUnit.test("simple synchronous get", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(
            () => serverData.get("partner", "get_something", [5]),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        await nextTick();
        assert.verifySteps(["partner/get_something", "data-fetched-notification"]);
        assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
        assert.verifySteps([]);
    });

    QUnit.test("synchronous get which returns an error", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                throw new Error("error while fetching data");
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(
            () => serverData.get("partner", "get_something", [5]),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        await nextTick();
        assert.verifySteps(["partner/get_something", "data-fetched-notification"]);
        assert.throws(() => serverData.get("partner", "get_something", [5]), Error);
        assert.verifySteps([]);
    });

    QUnit.test("simple async fetch", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        const result = await serverData.fetch("partner", "get_something", [5]);
        assert.deepEqual(result, 5);
        assert.verifySteps(["partner/get_something"]);
        assert.deepEqual(await serverData.fetch("partner", "get_something", [5]), 5);
        assert.verifySteps([]);
    });

    QUnit.test("async fetch which throws an error", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                throw new Error("error while fetching data");
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.rejects(serverData.fetch("partner", "get_something", [5]));
        assert.verifySteps(["partner/get_something"]);
        assert.rejects(serverData.fetch("partner", "get_something", [5]));
        assert.verifySteps([]);
    });

    QUnit.test("two identical concurrent async fetch", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        const [result1, result2] = await Promise.all([
            serverData.fetch("partner", "get_something", [5]),
            serverData.fetch("partner", "get_something", [5]),
        ]);
        assert.verifySteps(["partner/get_something"], "it should have fetch the data once");
        assert.deepEqual(result1, 5);
        assert.deepEqual(result2, 5);
        assert.verifySteps([]);
    });

    QUnit.test("batch get with a single item", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 5),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        await nextTick();
        assert.verifySteps(["partner/get_something_in_batch", "data-fetched-notification"]);
        assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 5), 5);
        assert.verifySteps([]);
    });

    QUnit.test("batch get with multiple items", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 5),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 6),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        await nextTick();
        assert.verifySteps(["partner/get_something_in_batch", "data-fetched-notification"]);
        assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 5), 5);
        assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 6), 6);
        assert.verifySteps([]);
    });

    QUnit.test("batch get with one error", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                if (args[0].includes(5)) {
                    throw new Error("error while fetching data");
                }
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 4),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 5),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        assert.throws(
            () => serverData.batch.get("partner", "get_something_in_batch", 6),
            LoadingDataError,
            "it should throw when it's not loaded"
        );
        await nextTick();
        assert.verifySteps([
            // one call for the batch
            "partner/get_something_in_batch",
            // retries one by one
            "partner/get_something_in_batch",
            "partner/get_something_in_batch",
            "partner/get_something_in_batch",
            "data-fetched-notification",
        ]);
        assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 4), 4);
        assert.throws(() => serverData.batch.get("partner", "get_something_in_batch", 5), Error);
        assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 6), 6);
        assert.verifySteps([]);
    });

    QUnit.test("concurrently fetch then get the same request", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        const promise = serverData.fetch("partner", "get_something", [5]);
        assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
        const result = await promise;
        await nextTick();
        assert.verifySteps(
            ["partner/get_something", "partner/get_something", "data-fetched-notification"],
            "it loads the data independently"
        );
        assert.deepEqual(result, 5);
        assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
        assert.verifySteps([]);
    });

    QUnit.test("concurrently get then fetch the same request", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
        const result = await serverData.fetch("partner", "get_something", [5]);
        assert.verifySteps(
            ["partner/get_something", "partner/get_something", "data-fetched-notification"],
            "it should have fetch the data once"
        );
        assert.deepEqual(result, 5);
        assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
        assert.verifySteps([]);
    });

    QUnit.test("concurrently batch get then fetch the same request", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(() => serverData.batch.get("partner", "get_something", 5), LoadingDataError);
        const result = await serverData.fetch("partner", "get_something", [5]);
        await nextTick();
        assert.verifySteps(
            ["partner/get_something", "partner/get_something", "data-fetched-notification"],
            "it should have fetch the data once"
        );
        assert.deepEqual(result, 5);
        assert.deepEqual(serverData.batch.get("partner", "get_something", 5), 5);
        assert.verifySteps([]);
    });

    QUnit.test("concurrently get and batch get the same request", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                assert.step(`${model}/${method}`);
                return args[0];
            },
        };
        const serverData = new ServerData(orm, {
            whenDataIsFetched: () => assert.step("data-fetched-notification"),
        });
        assert.throws(() => serverData.batch.get("partner", "get_something", 5), LoadingDataError);
        assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
        await nextTick();
        assert.verifySteps(
            ["partner/get_something", "data-fetched-notification"],
            "it should have fetch the data once"
        );
        assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
        assert.deepEqual(serverData.batch.get("partner", "get_something", 5), 5);
        assert.verifySteps([]);
    });

    QUnit.test("Call the correct callback after a batch result", async (assert) => {
        const orm = {
            call: async (model, method, args) => {
                if (args[0].includes(5)) {
                    throw new Error("error while fetching data");
                }
                return args[0];
            },
        };
        const batchEndpoint = new BatchEndpoint(orm, "partner", "get_something", {
            whenDataIsFetched: () => {},
            successCallback: () => assert.step("success-callback"),
            failureCallback: () => assert.step("failure-callback"),
        });
        const request = new Request("partner", "get_something", [4]);
        const request2 = new Request("partner", "get_something", [5]);
        batchEndpoint.call(request);
        batchEndpoint.call(request2);
        assert.verifySteps([]);
        await nextTick();
        assert.verifySteps(["success-callback", "failure-callback"]);
    });
});
