import { expect, test } from "@odoo/hoot";
import { setupEditor, testEditor } from "../_helpers/editor";
import { unformat } from "../_helpers/format";
import { setSelection, setContent, getContent } from "../_helpers/selection";
import { deleteBackward, insertText, undo } from "../_helpers/user_actions";
import { waitFor, waitForNone } from "@odoo/hoot-dom";
import { parseHTML } from "@html_editor/utils/html";
import { Plugin } from "@html_editor/plugin";
import { MAIN_PLUGINS } from "@html_editor/plugin_sets";

test("should ignore protected elements children mutations (true)", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div><p>a[]</p></div>
                <div data-oe-protected="true"><p>a</p></div>
                `),
        stepFunction: async (editor) => {
            await insertText(editor, "bc");
            const protectedParagraph = editor.editable.querySelector(
                '[data-oe-protected="true"] > p'
            );
            protectedParagraph.append(document.createTextNode("b"));
            editor.dispatch("ADD_STEP");
            editor.dispatch("HISTORY_UNDO");
        },
        contentAfterEdit: unformat(`
                <div><p>ab[]</p></div>
                <div data-oe-protected="true" contenteditable="false"><p>ab</p></div>
                `),
    });
});

test("should not ignore unprotected elements children mutations (false)", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div><p>a[]</p></div>
                <div data-oe-protected="true"><div data-oe-protected="false"><p>a</p></div></div>
                `),
        stepFunction: async (editor) => {
            await insertText(editor, "bc");
            const unProtectedParagraph = editor.editable.querySelector(
                '[data-oe-protected="false"] > p'
            );
            setSelection({ anchorNode: unProtectedParagraph, anchorOffset: 1 });
            await insertText(editor, "bc");
            editor.dispatch("HISTORY_UNDO");
        },
        contentAfterEdit: unformat(`
                <div><p>abc</p></div>
                <div data-oe-protected="true" contenteditable="false"><div data-oe-protected="false" contenteditable="true"><p>ab[]</p></div></div>
                `),
    });
});

test("should not normalize protected elements children (true)", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div>
                    <p><i class="fa"></i></p>
                    <ul><li>abc<p><br></p></li></ul>
                </div>
                <div data-oe-protected="true">
                    <p><i class="fa"></i></p>
                    <ul><li>abc<p><br></p></li></ul>
                </div>
                `),
        stepFunction: async (editor) => editor.dispatch("NORMALIZE", { node: editor.editable }),
        contentAfterEdit: unformat(`
                <div>
                    <p><i class="fa" contenteditable="false">\u200B</i></p>
                    <ul><li><p>abc</p><p><br></p></li></ul>
                </div>
                <div data-oe-protected="true" contenteditable="false">
                    <p><i class="fa"></i></p>
                    <ul><li>abc<p><br></p></li></ul>
                </div>
                `),
    });
});

test("should not remove/merge empty (identical) protecting nodes", async () => {
    const { el, editor } = await setupEditor(`<p><span data-oe-protected="true"></span>[]</p>`);
    editor.shared.domInsert(parseHTML(editor.document, `<span data-oe-protected="true"></span>`));
    editor.dispatch("ADD_STEP");
    expect(getContent(el)).toBe(
        unformat(
            `<p>
                <span data-oe-protected="true" contenteditable="false"></span>
                <span data-oe-protected="true" contenteditable="false"></span>[]
            </p>`
        )
    );
});

test("should normalize unprotected elements children (false)", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div data-oe-protected="true">
                    <p><i class="fa"></i></p>
                    <ul><li>abc<p><br></p></li></ul>
                    <div data-oe-protected="false">
                        <p><i class="fa"></i></p>
                        <ul><li>abc<p><br></p></li></ul>
                    </div>
                </div>
                `),
        stepFunction: async (editor) => editor.dispatch("NORMALIZE", { node: editor.editable }),
        contentAfterEdit: unformat(`
                <div data-oe-protected="true" contenteditable="false">
                    <p><i class="fa"></i></p>
                    <ul><li>abc<p><br></p></li></ul>
                    <div data-oe-protected="false" contenteditable="true">
                        <p><i class="fa" contenteditable="false">\u200B</i></p>
                        <ul><li><p>abc</p><p><br></p></li></ul>
                    </div>
                </div>
                `),
    });
});

test("should not handle table selection in protected elements children (true)", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div data-oe-protected="true">
                    <p>a[bc</p><table><tbody><tr><td>a]b</td><td>cd</td><td>ef</td></tr></tbody></table>
                </div>
                `),
        contentAfterEdit: unformat(`
                <div data-oe-protected="true" contenteditable="false">
                    <p>a[bc</p><table><tbody><tr><td>a]b</td><td>cd</td><td>ef</td></tr></tbody></table>
                </div>
                `),
    });
});

test("should handle table selection in unprotected elements", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div data-oe-protected="true">
                    <div data-oe-protected="false">
                        <p>a[bc</p><table><tbody><tr><td>a]b</td><td>cd</td><td>ef</td></tr></tbody></table>
                    </div>
                </div>
                `),
        contentAfterEdit: unformat(`
                <div data-oe-protected="true" contenteditable="false">
                    <div data-oe-protected="false" contenteditable="true">
                        <p>a[bc</p>
                        <table class="o_selected_table"><tbody><tr>
                            <td class="o_selected_td">a]b</td>
                            <td class="o_selected_td">cd</td>
                            <td class="o_selected_td">ef</td>
                        </tr></tbody></table>
                    </div>
                </div>
                `),
    });
});

test("should not remove contenteditable attribute of a protected node", async () => {
    await testEditor({
        contentBefore: unformat(`
                <div data-oe-protected="true">
                    <p contenteditable="true">content</p>
                    <table contenteditable="true">
                        <tbody><tr><td>ab</td></tr></tbody>
                    </table>
                    <div contenteditable="true">
                        <p>content</p>
                    </div>
                </div>
            `),
        contentAfterEdit: unformat(`
                <div data-oe-protected="true" contenteditable="false">
                    <p contenteditable="true">content</p>
                    <table contenteditable="true">
                        <tbody><tr><td>ab</td></tr></tbody>
                    </table>
                    <div contenteditable="true">
                        <p>content</p>
                    </div>
                </div>
            `),
    });
});

test("should not select a protected table even if it is contenteditable='true'", async () => {
    // Individually protected cells are not yet supported for simplicity
    // since there is no need for that currently.
    await testEditor({
        contentBefore: unformat(`
                <div data-oe-protected="true">
                    <table contenteditable="true"><tbody><tr>
                        <td>[ab</td>
                    </tr></tbody></table>
                    <table><tbody><tr>
                        <td>cd]</td>
                    </tr></tbody></table>
                </div>
            `),
        contentAfterEdit: unformat(`
                <div data-oe-protected="true" contenteditable="false">
                    <table contenteditable="true"><tbody><tr>
                        <td>[ab</td>
                    </tr></tbody></table>
                    <table><tbody><tr>
                        <td>cd]</td>
                    </tr></tbody></table>
                </div>
            `),
    });
});

test("select a protected element shouldn't open the toolbar", async () => {
    const { el } = await setupEditor(
        `<div><p>[a]</p></div><div data-oe-protected="true"><p>b</p><div data-oe-protected="false">c</div></div>`
    );
    await waitFor(".o-we-toolbar");
    expect(".o-we-toolbar").toHaveCount(1);

    setContent(
        el,
        `<div><p>a</p></div><div data-oe-protected="true"><p>[b]</p><div data-oe-protected="false">c</div></div>`
    );
    await waitForNone(".o-we-toolbar");
    expect(".o-we-toolbar").toHaveCount(0);

    setContent(
        el,
        `<div><p>a</p></div><div data-oe-protected="true"><p>b</p><div data-oe-protected="false">[c]</div></div>`
    );
    await waitFor(".o-we-toolbar");
    expect(".o-we-toolbar").toHaveCount(1);
});

test("should protect disconnected nodes", async () => {
    const { editor, el, plugins } = await setupEditor(
        `<div data-oe-protected="true"><p>a</p></div><p>a</p>`
    );
    const div = el.querySelector("div");
    const protectedP = div.querySelector("p");
    protectedP.remove();
    div.remove();
    editor.dispatch("ADD_STEP");
    const lastStep = editor.shared.getHistorySteps().at(-1);
    expect(lastStep.mutations.length).toBe(1);
    expect(lastStep.mutations[0].type).toBe("remove");
    expect(plugins.get("history").unserializeNode(lastStep.mutations[0].node).outerHTML).toBe(
        `<div contenteditable="false" data-oe-protected="true"></div>`
    );
});

test("should not crash when changing attributes and removing a protecting anchor", async () => {
    const { editor, el, plugins } = await setupEditor(
        `<div data-oe-protected="true" data-attr="value"><p>a</p></div><p>a</p>`
    );
    const div = el.querySelector("div");
    div.dataset.attr = "other";
    div.remove();
    editor.dispatch("ADD_STEP");
    const lastStep = editor.shared.getHistorySteps().at(-1);
    expect(lastStep.mutations.length).toBe(2);
    expect(lastStep.mutations[0].type).toBe("attributes");
    expect(lastStep.mutations[1].type).toBe("remove");
    expect(plugins.get("history").unserializeNode(lastStep.mutations[1].node).outerHTML).toBe(
        `<div contenteditable="false" data-attr="other" data-oe-protected="true"><p>a</p></div>`
    );
});

test("removing a protected node should be undo-able", async () => {
    const { editor, el } = await setupEditor(
        `<div data-oe-protected="true"><p>a</p></div><p>[]a</p>`
    );
    deleteBackward(editor);
    expect(getContent(el)).toBe(`<p>[]a</p>`);
    undo(editor);
    expect(getContent(el)).toBe(
        `<div data-oe-protected="true" contenteditable="false"><p>a</p></div><p>[]a</p>`
    );
});

test("removing a recursively protected then unprotected node should be undo-able", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <p>a</p>
                <div data-oe-protected="false">
                    <p>b</p>
                    <div data-oe-protected="true">
                        <p>c</p>
                        <div data-oe-protected="false">
                            <p>d</p>
                        </div>
                        <p>w</p>
                    </div>
                    <p>x</p>
                </div>
                <p>y</p>
            </div>
            <p>[]z</p>
        `)
    );
    const protectPlugin = plugins.get("protected_node");
    const protectingNodes = [...el.querySelectorAll(`[data-oe-protected="true"]`)];
    const unprotectingNodes = [...el.querySelectorAll(`[data-oe-protected="false"]`)];
    const unprotectedDescendants = [];
    for (const unprotectingNode of unprotectingNodes) {
        unprotectedDescendants.push([...unprotectingNode.childNodes]);
    }
    expect(protectPlugin.filterDescendantsToRemove(protectingNodes[0])).toEqual(
        unprotectedDescendants[0]
    );
    expect(protectPlugin.filterDescendantsToRemove(protectingNodes[1])).toEqual(
        unprotectedDescendants[1]
    );
    deleteBackward(editor);
    expect([...unprotectingNodes[0].childNodes]).toEqual([]);
    expect([...unprotectingNodes[1].childNodes]).toEqual([]);
    expect(getContent(el)).toBe(`<p>[]z</p>`);
    undo(editor);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <p>a</p>
                <div data-oe-protected="false" contenteditable="true">
                    <p>b</p>
                    <div data-oe-protected="true" contenteditable="false">
                        <p>c</p>
                        <div data-oe-protected="false" contenteditable="true">
                            <p>d</p>
                        </div>
                        <p>w</p>
                    </div>
                    <p>x</p>
                </div>
                <p>y</p>
            </div>
            <p>[]z</p>
        `)
    );
});

test("removing a protected node and then removing its protected parent should be ignored", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div class="a">
                    <div class="b"></div>
                </div>
            </div>
        `)
    );
    const historyPlugin = plugins.get("history");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    const a = el.querySelector(".a");
    const b = el.querySelector(".b");
    b.remove();
    a.remove();
    editor.dispatch("ADD_STEP");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    expect(getContent(el)).toBe(`<div data-oe-protected="true" contenteditable="false"></div>`);
});

test("removing a protected ancestor, then a protected descendant, then its protected parent should be ignored", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div class="a">
                    <div class="b">
                        <div class="c"></div>
                    </div>
                </div>
            </div>
        `)
    );
    const historyPlugin = plugins.get("history");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    const a = el.querySelector(".a");
    const b = el.querySelector(".b");
    const c = el.querySelector(".c");
    a.remove();
    c.remove();
    b.remove();
    editor.dispatch("ADD_STEP");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    expect(getContent(el)).toBe(`<div data-oe-protected="true" contenteditable="false"></div>`);
});

test("moving a protected node at an unprotected location, only remove should be ignored", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div class="b" data-oe-protected="false"></div>
            </div>
            <div data-oe-protected="true">
                <div class="a"></div>
            </div>
        `)
    );
    const historyPlugin = plugins.get("history");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    const a = el.querySelector(".a");
    const b = el.querySelector(".b");
    b.append(a);
    editor.dispatch("ADD_STEP");
    const historySteps = editor.shared.getHistorySteps();
    expect(historySteps.length).toBe(2);
    const lastStep = historySteps.at(-1);
    expect(lastStep.mutations.length).toBe(1);
    expect(lastStep.mutations[0].type).toBe("add");
    expect(historyPlugin.idToNodeMap.get(lastStep.mutations[0].id)).toBe(a);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div class="b" data-oe-protected="false" contenteditable="true">
                    <div class="a"></div>
                </div>
            </div>
            <div data-oe-protected="true" contenteditable="false"></div>
        `)
    );
});

test("moving an unprotected node at a protected location, only add should be ignored", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div data-oe-protected="false">
                    <div class="a"></div>
                </div>
            </div>
            <div class="b" data-oe-protected="true"></div>
        `)
    );
    const historyPlugin = plugins.get("history");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    const a = el.querySelector(".a");
    const b = el.querySelector(".b");
    b.append(a);
    editor.dispatch("ADD_STEP");
    const historySteps = editor.shared.getHistorySteps();
    expect(historySteps.length).toBe(2);
    const lastStep = historySteps.at(-1);
    expect(lastStep.mutations.length).toBe(1);
    expect(lastStep.mutations[0].type).toBe("remove");
    expect(historyPlugin.idToNodeMap.get(lastStep.mutations[0].id)).toBe(a);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div data-oe-protected="false" contenteditable="true"></div>
            </div>
            <div class="b" data-oe-protected="true" contenteditable="false">
                <div class="a"></div>
            </div>
        `)
    );
});

test("sequentially added nodes under a protecting parent are correctly protected", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                content
            </div>
        `)
    );
    const protectedPlugin = plugins.get("protected_node");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    const protecting = el.querySelector("[data-oe-protected='true']");
    const element = editor.document.createElement("div");
    const node = editor.document.createTextNode("a");
    protecting.prepend(element);
    element.prepend(node);
    editor.dispatch("ADD_STEP");
    expect(protectedPlugin.protectedNodes.has(element)).toBe(true);
    expect(protectedPlugin.protectedNodes.has(node)).toBe(true);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div>a</div>
                content
            </div>
        `)
    );
    node.remove();
    editor.dispatch("ADD_STEP");
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div></div>
                content
            </div>
        `)
    );
    expect(editor.shared.getHistorySteps().length).toBe(1);
});

test("don't protect a node under data-oe-protected='false' through delete and undo", async () => {
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div data-oe-protected="false">
                    <p>a</p>
                </div>
            </div>
            <p>[]a</p>
        `)
    );
    const protectedPlugin = plugins.get("protected_node");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    const protecting = el.querySelector("[data-oe-protected='false']");
    const paragraph = editor.document.createElement("p");
    const node = editor.document.createTextNode("b");
    protecting.prepend(paragraph);
    paragraph.prepend(node);
    editor.dispatch("ADD_STEP");
    expect(editor.shared.getHistorySteps().length).toBe(2);
    expect(protectedPlugin.protectedNodes.has(paragraph)).toBe(false);
    expect(protectedPlugin.protectedNodes.has(node)).toBe(false);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div data-oe-protected="false" contenteditable="true">
                    <p>b</p>
                    <p>a</p>
                </div>
            </div>
            <p>[]a</p>
        `)
    );
    deleteBackward(editor);
    undo(editor);
    expect(getContent(el)).toBe(
        unformat(`
            <div data-oe-protected="true" contenteditable="false">
                <div data-oe-protected="false" contenteditable="true">
                    <p>b</p>
                    <p>a</p>
                </div>
            </div>
            <p>[]a</p>
        `)
    );
    expect(editor.shared.getHistorySteps().length).toBe(4);
});

test("protected plugin is robust against other plugins which can filter mutations", async () => {
    class FilterPlugin extends Plugin {
        static name = "filter_plugin";
        resources = {
            is_mutation_record_savable: this.isMutationRecordSavable.bind(this),
        };
        isMutationRecordSavable(record) {
            if (
                record.type === "childList" &&
                record.removedNodes.length === 1 &&
                [...record.removedNodes][0] === a
            ) {
                // Artificially hide the removal of `a` node
                return false;
            }
            return true;
        }
    }
    const { editor, el, plugins } = await setupEditor(
        unformat(`
            <div data-oe-protected="true">
                <div class="a">
                    <div class="b"></div>
                </div>
            </div>
        `),
        // Put FilterPlugin as the first plugin, so that its filter is applied before
        // protected_node_plugin.
        { config: { Plugins: [FilterPlugin, ...MAIN_PLUGINS] } }
    );
    const historyPlugin = plugins.get("history");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    const a = el.querySelector(".a");
    const b = el.querySelector(".b");
    a.remove();
    b.remove();
    editor.dispatch("ADD_STEP");
    expect(editor.shared.getHistorySteps().length).toBe(1);
    expect(historyPlugin.currentStep.mutations).toEqual([]);
    expect(getContent(el)).toBe(`<div data-oe-protected="true" contenteditable="false"></div>`);
});
