import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
import Form from "../../Form";
import FormBuilder from "../../../../../FormBuilder";
import RestSubmit from "./RestSubmit";
import InnerNode from "../../../../../Data/InnerNode";
import EventHandler from "../../../../../Utility/EventHandler";
import Structure from "../../../Structure";

let request: Promise<any>;
let requestResolve: ((value: any) => void)|null = null;
let requestReject: ((reason?: any) => void)|null = null;

beforeEach(() => {
    request = new Promise((resolve, reject) => {
        requestResolve = (body) => {
            const response = {
                json: async () => body
            } as Response;

            resolve(response);
        }
        requestReject = reject;
    });

    request
        .catch(() => {})
        .finally(() => {requestResolve = null; requestReject = null;});

    global.fetch = vi.fn(async () => request) as unknown as typeof fetch;

    global.window = {} as Window & typeof globalThis;
    global.window.addEventListener = vi.fn();
    global.window.removeEventListener = vi.fn();
    global.window.location = {} as string & typeof location;

    global.console.warn = vi.fn();
    global.console.error = vi.fn();
});

afterEach(() => {
    if (requestResolve) requestResolve({status: "redirect", url: "test"});
});

it('should add listeners on construct with save data on success', () => {
    //Arrange
    const form = getMockForm();

    //Act
    new RestSubmit(form, true);

    //Assert
    expect(form.getFormBuilder().onComponentsCreated.add)
        .toHaveBeenCalledWith(expect.any(Function));
    expect(form.onFormChange.add)
        .toHaveBeenCalledWith(expect.any(Function));
});

it('should add listeners on construct with save data on success', () => {
    //Arrange
    const form = getMockForm();

    //Act
    new RestSubmit(form, false);

    //Assert
    expect(form.getFormBuilder().onComponentsCreated.add)
        .toBeCalledTimes(0);
    expect(form.onFormChange.add)
        .toHaveBeenCalledWith(expect.any(Function));
});

it('should do nothing if form has no parent', () => {
    //Arrange
    const form = getMockForm();
    form.getFormBuilder().onComponentsCreated.add = callable => callable(null);
    form.getDataNode = vi.fn();

    //Act
    new RestSubmit(form, true);

    //Assert
    expect(form.getDataNode).toBeCalledTimes(0);
});

it('should remove child data from parent if save on success and parent', () => {
    //Arrange
    const form = getMockForm();
    const parent = {} as Structure;
    const parentData = {} as InnerNode;

    parent.getDataNode = () => parentData;
    parentData.removeChild = vi.fn();
    form.getFormBuilder().onComponentsCreated.add = callable => callable(null);
    form.parent = parent;

    //Act
    new RestSubmit(form, true);

    //Assert
    expect(parentData.removeChild).toHaveBeenCalledWith(form.getDataNode());
});

it('adds leave check to form', async () => {
    //Arrange
    const form = getMockForm();
    form.onFormChange.add = callable => callable(null);

    //Act
    const restSubmit = new RestSubmit(form);

    //Assert
    expect(window.addEventListener)
        .toHaveBeenCalledWith("beforeunload", restSubmit.leaveCheck);
});

it('prevents default on leave check', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const event = {preventDefault: vi.fn()} as any;

    //Act
    restSubmit.leaveCheck(event);

    //Assert
    expect(event.preventDefault)
        .toHaveBeenCalled();
    expect(event.returnValue)
        .toEqual(true);
});

it('should set the form to submitting and remove the message while submitting', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    form.submitting = false;
    form.message = "test";

    //Act
    restSubmit.submit({}).then(); //Async call that will be autocompleted after the test

    //Assert
    expect(form.submitting).toEqual(true);
    expect(form.message).toEqual(null);
});

it('should do a redirect if the response status is redirect', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "redirect", url: "test"});
    await submit;

    //Assert
    expect(window.removeEventListener).toHaveBeenCalledWith("beforeunload", restSubmit.leaveCheck);
    expect(window.location.href).toEqual("test");
});

it('should update the message if the response has a message', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "success", message: "test"});
    await submit;

    //Assert
    expect(form.message).toEqual("test");
    expect(form.submitting).toEqual(false);
});

describe('should display a default message if the response has no message', async () => {
    for ( const params of [
        {status: "success", message: "Succesvol verzonden."},
        {status: "failed", message: "Iets is mis gegaan, probeer het later opnieuw."},
        {status: "server-error", message: "Er is iets misgegaan op de server. Probeer het later opnieuw."},
        {status: "config-error", message: "Er is iets misgegaan met de configuratie. Laat de ons dit graag weten."}
    ]) {
        it('is "' + params.message + '" for ' + params.status, async () => {
            //Arrange
            const form = getMockForm();
            const restSubmit = new RestSubmit(form);

            //Act
            let submit = restSubmit.submit({});
            if (requestResolve) requestResolve({status: params.status});
            await submit;

            //Assert
            expect(form.message).toEqual(params.message);
            expect(form.submitting).toEqual(false);
        });
    }
});

it('should log the error and show an error if the fetch request fails', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const errorObject = {};

    //Act
    let submit = restSubmit.submit({});
    if (requestReject) requestReject(errorObject);
    await submit;

    //Assert
    expect(console.error).toHaveBeenCalledWith(errorObject);
    expect(form.message).toEqual("Er is iets misgegaan, probeer het later opnieuw.");
    expect(form.submitting).toEqual(false);
});

it('should log the response and status if response is not success', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const responseObject = {
        status: "failed",
        message: "test"
    };

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(console.warn).toHaveBeenCalledWith("Error while submitting form, status: failed");
    expect(console.warn).toHaveBeenCalledWith(responseObject);
});

it('should add serverErrors if they are included in the response', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const serverErrors = {
        test: "test"
    };
    const responseObject = {
        status: "failed",
        message: "test",
        errors: serverErrors
    };

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(form.serverSideErrors).toEqual(serverErrors);
});

it('should clear serverErrors if response is success', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    form.serverSideErrors = {
        test: "test"
    };
    const responseObject = {
        status: "success",
        message: "test"
    }

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(form.serverSideErrors).toEqual({});
});

it('should place pad-error class on form if response is not success', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const responseObject = {
        status: "failed",
        message: "test"
    };
    const formNode = {classList: {add: vi.fn() as unknown, remove: vi.fn() as unknown}} as HTMLFormElement;
    form.getPlacedHtml = () => [formNode as HTMLFormElement];

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(formNode.classList.add).toHaveBeenCalledWith("pad-error");
});

it('should remove pad-error class on form if response is success', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const responseObject = {
        status: "success",
        message: "test"
    };
    const formNode = {classList: {add: vi.fn() as unknown, remove: vi.fn() as unknown}} as HTMLFormElement;
    form.getPlacedHtml = () => [formNode as HTMLFormElement];

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(formNode.classList.remove).toHaveBeenCalledWith("pad-error");
});

it('should remove leave check on success', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "success"});
    await submit;

    //Assert
    expect(window.removeEventListener).toHaveBeenCalledWith("beforeunload", restSubmit.leaveCheck);
});

it('should add data from child to parent if save on success and parent', async () => {
    //Arrange
    const form = getMockForm();
    const parent = {} as Structure;
    const parentData = {value: {parentData: "parentData"}} as InnerNode;
    const childData = {value: {childData: "childData"}} as InnerNode;

    parent.getDataNode = () => parentData;
    parentData.removeChild = () => {};

    form.getFormBuilder().onComponentsCreated.add = callable => callable(null);
    form.parent = parent;
    form.getDataNode = () => childData;

    const restSubmit = new RestSubmit(form, true);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "success"});
    await submit;

    //Assert
    expect(parentData.value).toEqual({parentData: "parentData", childData: "childData"});
});

it('should not add data from child to parent if save on success and parent but request failed', async () => {
    //Arrange
    const form = getMockForm();
    const parent = {} as Structure;
    const parentData = {value: {parentData: "parentData"}} as InnerNode;
    const childData = {value: {childData: "childData"}} as InnerNode;

    parent.getDataNode = () => parentData;
    parentData.removeChild = () => {};

    form.getFormBuilder().onComponentsCreated.add = callable => callable(null);
    form.parent = parent;
    form.getDataNode = () => childData;

    const restSubmit = new RestSubmit(form, true);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "failed"});
    await submit;

    //Assert
    expect(parentData.value).toEqual({parentData: "parentData"});
});

it('should not add data from child to parent if save on success is false', async () => {
    //Arrange
    const form = getMockForm();
    const parent = {} as Structure;
    const parentData = {value: {parentData: "parentData"}} as InnerNode;
    const childData = {value: {childData: "childData"}} as InnerNode;

    parent.getDataNode = () => parentData;
    parentData.removeChild = () => {};

    form.getFormBuilder().onComponentsCreated.add = callable => callable(null);
    form.parent = parent;
    form.getDataNode = () => childData;

    const restSubmit = new RestSubmit(form);

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve({status: "success"});
    await submit;

    //Assert
    expect(parentData.value).toEqual({parentData: "parentData"});
});

it('should log response and message if response status is not recognized', async () => {
    //Arrange
    const form = getMockForm();
    const restSubmit = new RestSubmit(form);
    const responseObject = {
        status: "unknown status",
        message: "test"
    };

    //Act
    let submit = restSubmit.submit({});
    if (requestResolve) requestResolve(responseObject);
    await submit;

    //Assert
    expect(console.error).toHaveBeenCalledWith("Unknown response from rest submit");
    expect(console.error).toHaveBeenCalledWith(responseObject);
    expect(form.submitting).toEqual(false);
})

function getMockForm(): Form {
    let dataNode = {} as InnerNode;

    let formBuilder = {} as FormBuilder;
    formBuilder.onComponentsCreated = getMockEvent();

    let form = {} as Form;
    form.getPlacedHtml = () => [];
    form.getFormBuilder = () => formBuilder;
    form.onFormChange = getMockEvent();
    form.getDataNode = () => dataNode;
    return form;
}

function getMockEvent(): EventHandler<any> {
    let event = {} as EventHandler<any>;
    event.add = vi.fn();
    event.emit = vi.fn();
    return event;
}