import { describe, expect, test, vi } from "vitest";

import { fetchDatasets } from "@/utils/upload";

import { UploadQueue } from "./upload-queue.js";

vi.mock("@/utils/upload", async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        fetchDatasets: vi.fn(),
        submitUpload: vi.fn((config) => {
            // Simulate calling success callback
            config.success?.();
        }),
    };
});

function StubFile(name = null, size = 0, mode = "local") {
    return { name, size, mode };
}

function instrumentedUploadQueue(options = {}) {
    const uploadQueue = new UploadQueue(options);
    uploadQueue.encountedErrors = false;
    uploadQueue.opts.error = function (d, m) {
        uploadQueue.encountedErrors = true;
    };
    return uploadQueue;
}

describe("UploadQueue", () => {
    test("a queue is initialized to correct state", () => {
        const q = instrumentedUploadQueue({ foo: 1 });
        expect(q.size).toEqual(0);
        expect(q.isRunning).toBe(false);
        expect(q.opts.foo).toEqual(1); // passed as options
        expect(q.opts.multiple).toBe(true); // default value
        expect(q.encountedErrors).toBeFalsy();
    });

    test("resetting the queue removes all files from it", () => {
        const q = instrumentedUploadQueue();
        q.add([StubFile("a"), StubFile("b")]);
        expect(q.size).toEqual(2);
        q.reset();
        expect(q.size).toEqual(0);
        expect(q.encountedErrors).toBeFalsy();
    });

    test("calling configure updates options", () => {
        const q = instrumentedUploadQueue({ foo: 1 });
        expect(q.opts.foo).toEqual(1);
        expect(q.opts.bar).toBeUndefined();
        q.configure({ bar: 2 }); // overwrite bar
        expect(q.opts.foo).toEqual(1); // value unchangee
        expect(q.opts.bar).toEqual(2); // value overwritten
        expect(q.encountedErrors).toBeFalsy();
    });

    test("calling start sets isRunning to true", () => {
        const q = instrumentedUploadQueue();
        q._process = vi.fn(); // mock this, otherwise it'll reset isRunning after it's done.
        expect(q.isRunning).toBe(false);
        q.start();
        expect(q.isRunning).toBe(true);
        expect(q.encountedErrors).toBeFalsy();
    });

    test("calling start is a noop if queue is running", () => {
        const q = instrumentedUploadQueue();
        const mockedProcess = vi.fn();
        q._process = mockedProcess;
        q.isRunning = true;
        q.start();
        expect(mockedProcess.mock.calls.length).toBe(0); // function was not called
        expect(q.encountedErrors).toBeFalsy();
    });

    test("calling start processes all files in queue", () => {
        const fileEntries = {};
        const q = instrumentedUploadQueue({
            get: (index) => fileEntries[index],
            announce: (index, file) => {
                fileEntries[index] = {
                    fileMode: file.mode,
                    fileName: file.name,
                    fileSize: file.size,
                    fileContent: "fileContent",
                    fileData: new File(["test content"], file.name || "test.txt", { type: "text/plain" }),
                    targetHistoryId: "mockhistoryid",
                };
            },
        });
        const spy = vi.spyOn(q, "_process");
        const mockedSubmit = vi.fn(() => q._process());
        q._processSubmit = mockedSubmit;
        q.add([StubFile("a"), StubFile("b")]);
        q.start();
        expect(q.size).toEqual(0);
        expect(q.encountedErrors).toBeFalsy();
        expect(spy.mock.calls.length).toEqual(3); // called for 2, 1, 0 files.
        spy.mockRestore(); // not necessary, but safer, in case we later modify implementation.
    });

    test("calling stop sets isPaused to true", () => {
        const q = instrumentedUploadQueue();
        q.start();
        expect(q.isPaused).toBe(false);
        q.stop();
        expect(q.isPaused).toBe(true);
    });

    test("adding files increases the queue size by the number of files", () => {
        const q = instrumentedUploadQueue();
        expect(q.size).toEqual(0);
        q.add([StubFile("a"), StubFile("b")]);
        expect(q.nextIndex).toEqual(2);
        expect(q.size).toEqual(2);
        q.add([StubFile("c")]);
        expect(q.size).toEqual(3);
    });

    test("adding files increases the next index by the number of files", () => {
        const q = instrumentedUploadQueue();
        expect(q.nextIndex).toEqual(0);
        q.add([StubFile("a"), StubFile("b")]);
        expect(q.nextIndex).toEqual(2);
    });

    test("duplicate files are not added to the queue, unless the mode is set to 'new'", () => {
        const q = instrumentedUploadQueue();
        const file1 = StubFile("a", 1);
        const file2 = StubFile("a", 1);
        const file3 = StubFile("a", 1, "new");
        q.add([file1, file2]); // file2 is a duplicate of file1, so only 1 file is added
        expect(q.size).toEqual(1); // queue size incremented by 1
        expect(q.nextIndex).toEqual(1); // next index value incremented by 1
        q.add([file3]); // file3 is a duplicate of file1 and file2, but its mode is "new"
        expect(q.size).toEqual(2); // queue size incremented by 1
        expect(q.nextIndex).toEqual(2); // next index value incremented by 1
    });

    test("adding a file calls opts.announce with correct arguments", () => {
        const mockAnnounce = vi.fn();
        const q = instrumentedUploadQueue({ announce: mockAnnounce });
        const file = StubFile("a");
        expect(mockAnnounce.mock.calls.length).toBe(0);
        q.add([file]);
        expect(mockAnnounce.mock.calls.length).toBe(1); // called once
        expect(mockAnnounce.mock.calls[0][0]).toBe("0"); // first arg is index=0
        expect(mockAnnounce.mock.calls[0][1]).toBe(file); // second arg is file
    });

    test("removing a file reduces the queue size by 1", () => {
        const fileEntries = {};
        const q = instrumentedUploadQueue({
            announce: (index, file) => {
                fileEntries[index] = file;
            },
        });
        q.add([StubFile("a"), StubFile("b")]);
        expect(q.size).toEqual(2);
        q.remove("0");
        expect(q.size).toEqual(1);
    });

    test("removing a file by index out of sequence is allowed", () => {
        const q = instrumentedUploadQueue();
        const file1 = StubFile("a");
        const file2 = StubFile("b");
        const file3 = StubFile("c");
        q.add([file1, file2, file3]);
        expect(q.size).toEqual(3);
        q.remove("1"); // remove file2 (which has index=1)
        expect(q.size).toEqual(2);
        expect(q.queue.get("0")).toBe(file1);
        expect(q.queue.get("1")).toBeUndefined();
        expect(q.queue.get("2")).toBe(file3);
        expect(q.encountedErrors).toBeFalsy();
    });

    test("removing a file via _processIndex, obeys FIFO protocol", () => {
        const q = instrumentedUploadQueue();
        q.add([StubFile("a"), StubFile("b")]);
        let nextIndex = q._processIndex();
        expect(nextIndex).toEqual("0");
        q.remove(nextIndex);
        nextIndex = q._processIndex();
        expect(nextIndex).toEqual("1");
        q.remove(nextIndex);
        expect(q._processIndex()).toBeUndefined();
        expect(q.encountedErrors).toBeFalsy();
    });

    test("remote file batch", () => {
        const fileEntries = {};
        const q = instrumentedUploadQueue({
            historyId: "historyId",
            announce: (index, file) => {
                fileEntries[index] = {
                    deferred: true,
                    fileContent: `http://test.me.${index}`,
                    fileMode: "url",
                    fileName: file.name,
                    fileSize: 100,
                    spaceToTab: true,
                    status: "queued",
                    toPosixLines: false,
                    targetHistoryId: "historyId",
                };
            },
            get: (index) => fileEntries[index],
        });
        q.add([StubFile("a"), StubFile("b"), StubFile("c")]);
        expect(q.size).toEqual(3);
        q.start();
        expect(fetchDatasets.mock.calls[0][0]).toEqual({
            auto_decompress: true,
            files: [],
            history_id: "historyId",
            targets: [
                {
                    auto_decompress: false,
                    destination: { type: "hdas" },
                    elements: [
                        {
                            auto_decompress: false,
                            dbkey: "?",
                            deferred: true,
                            ext: "auto",
                            name: "a",
                            space_to_tab: true,
                            src: "url",
                            to_posix_lines: false,
                            url: "http://test.me.0",
                        },
                        {
                            auto_decompress: false,
                            dbkey: "?",
                            deferred: true,
                            ext: "auto",
                            name: "b",
                            space_to_tab: true,
                            src: "url",
                            to_posix_lines: false,
                            url: "http://test.me.1",
                        },
                        {
                            auto_decompress: false,
                            dbkey: "?",
                            deferred: true,
                            ext: "auto",
                            name: "c",
                            space_to_tab: true,
                            src: "url",
                            to_posix_lines: false,
                            url: "http://test.me.2",
                        },
                    ],
                },
            ],
        });
        expect(q.encountedErrors).toBeFalsy();
    });
});
