import { getLocalVue } from "@tests/vitest/helpers";
import { setupMockConfig } from "@tests/vitest/mockConfig";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { useServerMock } from "@/api/client/__mocks__";
import { rootResponse } from "@/components/FilesDialog/testingData";

import FormDirectory from "./FormDirectory.vue";
import FilesDialog from "@/components/FilesDialog/FilesDialog.vue";

const localVue = getLocalVue();
vi.mock("app");

const { server, http } = useServerMock();
setupMockConfig({});

async function init(wrapper, data) {
    // the file dialog modal should exist
    const filesDialogComponent = wrapper.findComponent(FilesDialog);
    expect(filesDialogComponent.exists()).toBe(true);
    filesDialogComponent.vm.callback({ url: data.url });
    // HACK: URL implementation in test environment is not the same as global node
    wrapper.vm.pathChunks = [...data.pathChunks];
    await flushPromises();
    await validateLatestEmittedPath(wrapper, data.url);
}

async function validateLatestEmittedPath(wrapper, expectedPath) {
    const latestEmitIndex = wrapper.emitted()["input"].length - 1;
    const latestPath = wrapper.emitted()["input"][latestEmitIndex][0];
    expect(latestPath).toBe(expectedPath);

    // also manually change prop value to be able to test the value being displayed
    await wrapper.setProps({ value: latestPath });
    const fullPathDisplayed = wrapper.find("[data-description='directory full path']");
    expect(fullPathDisplayed.text()).toBe(`Directory Path:${expectedPath}`);
}

describe("DirectoryPathEditableBreadcrumb", () => {
    let wrapper;
    let spyOnUrlSet;
    let spyOnAddPath;
    let spyOnUpdateURL;

    const testingData = {
        url: "gxfiles://directory/subdirectory",
        protocol: "gxfiles:",
        expectedNumberOfPaths: 4,
        pathChunks: [{ pathChunk: "directory" }, { pathChunk: "subdirectory" }],
    };
    const validPath = "validpath";
    const invalidPath = "./];";

    const testingDataWithSpecialChars = {
        url: "gxfiles://directory/sub directory/with%20percent",
        pathChunks: [{ pathChunk: "directory" }, { pathChunk: "sub%20directory" }, { pathChunk: "with%2520percent" }],
    };

    const saveNewChunk = async (path) => {
        // enter a new path chunk
        const input = wrapper.find("#path-input-breadcrumb");
        await input.setValue(path);
        expect(input.element.value).toBe(path);

        input.trigger("keyup.enter");
        return input;
    };

    beforeEach(async () => {
        spyOnUrlSet = vi.spyOn(FormDirectory.methods, "setUrl");
        spyOnAddPath = vi.spyOn(FormDirectory.methods, "addPath");
        spyOnUpdateURL = vi.spyOn(FormDirectory.methods, "updateURL");

        server.use(
            http.get("/api/remote_files/plugins", ({ response }) => {
                return response(200).json(rootResponse);
            }),
        );

        const pinia = createPinia();

        wrapper = mount(FormDirectory, {
            propsData: {
                value: null,
            },
            localVue: localVue,
            pinia,
        });
        await flushPromises();
    });
    afterEach(async () => {
        if (wrapper) {
            wrapper.destroy();
        }
        wrapper = undefined;
    });

    it("should render Breadcrumb", async () => {
        await init(wrapper, testingData);
        // after initial folder is chosen, setUrl() should be called and modal disappear
        expect(spyOnUrlSet).toHaveBeenCalled();
        expect(wrapper.findComponent(FilesDialog).exists()).toBe(false);
        expect(wrapper.find("ol.breadcrumb").exists()).toBe(true);

        await flushPromises();

        // check breadcrumb items
        const breadcrumbPaths = wrapper.findAll("li.breadcrumb-item");
        expect(breadcrumbPaths.length).toBe(testingData.expectedNumberOfPaths);
        expect(wrapper.find(".pathname").text()).toBe(testingData.protocol);
        const regularPathElements = wrapper.findAll("li.breadcrumb-item button[aria-disabled='true']");

        expect(regularPathElements.length).toBe(testingData.pathChunks.length);

        const chunks = testingData.pathChunks.map((e) => e.pathChunk);
        // every item should be rendered
        for (let i = 0; i < regularPathElements.length; i++) {
            const text = regularPathElements.at(i).text();
            expect(chunks.includes(text)).toBe(true);
        }
    });

    it("should prevent invalid Paths", async () => {
        await init(wrapper, testingData);
        // enter a new path chunk
        const input = await saveNewChunk(invalidPath);
        await flushPromises();
        // after new entry is entered the value of input should be an empty string
        expect(input.element.value).toBe(invalidPath);
        // should be the same name plus additional item
        expect(wrapper.findAll("li.breadcrumb-item").length).toBe(testingData.expectedNumberOfPaths);
    });

    it("should save and remove new Paths", async () => {
        await init(wrapper, testingData);
        // enter a new path chunk
        const input = await saveNewChunk(validPath);

        await flushPromises();
        expect(spyOnAddPath).toHaveBeenCalled();
        // after new entry is entered the value of input should be an empty string
        expect(input.element.value).toBe("");

        // should be the same name plus additional item
        expect(wrapper.findAll("li.breadcrumb-item").length).toBe(testingData.expectedNumberOfPaths + 1);
        // find newly added chunk
        const addedChunk = wrapper.findAll("li.breadcrumb-item button").wrappers.find((e) => e.text() === validPath);

        await validateLatestEmittedPath(wrapper, `${testingData.url}/${validPath}`);

        // remove chunk from the path
        await addedChunk.trigger("click");
        await flushPromises();
        // number of elements should be the same again
        expect(wrapper.findAll("li.breadcrumb-item").length).toBe(testingData.expectedNumberOfPaths);

        await validateLatestEmittedPath(wrapper, testingData.url);
    });

    it("should update new path", async () => {
        await init(wrapper, testingData);
        // enter a new path chunk
        expect(spyOnUpdateURL).toHaveBeenCalled();
        await saveNewChunk(validPath);
        await flushPromises();

        expect(spyOnUpdateURL).toHaveBeenCalled();
    });

    it("should retain special characters in path", async () => {
        // the init function itself validates that the emits and path display
        // retain special characters in the url
        await init(wrapper, testingDataWithSpecialChars);
    });

    it("should save on blur", async () => {
        await init(wrapper, testingData);
        // enter a new path chunk
        const input = wrapper.find("#path-input-breadcrumb");
        await input.setValue(validPath);
        expect(input.element.value).toBe(validPath);

        await input.trigger("blur");
        await flushPromises();

        expect(spyOnAddPath).toHaveBeenCalled();
        expect(input.element.value).toBe("");
        expect(wrapper.findAll("li.breadcrumb-item").length).toBe(testingData.expectedNumberOfPaths + 1);
        await validateLatestEmittedPath(wrapper, `${testingData.url}/${validPath}`);
    });
});
