"use strict";

import * as chai from "chai";

import { addToObjectLink } from "../../../source/dom/attributes.mjs";
import { customElementUpdaterLinkSymbol } from "../../../source/dom/constants.mjs";
import { ID } from "../../../source/types/id.mjs";
import { Observer } from "../../../source/types/observer.mjs";
import { ProxyObserver } from "../../../source/types/proxyobserver.mjs";
import { chaiDom } from "../../util/chai-dom.mjs";
import { initJSDOM } from "../../util/jsdom.mjs";

let expect = chai.expect;

chai.use(chaiDom);

let html1 = `
    <template id="current">
        <li data-monster-replace="path:current | tojson"></li>
    </template>
    <div id="test1">
        <ul data-monster-insert="current path:a.b">
        </ul>
    </div>
    <div id="test2">
        <ul data-monster-insert="current path:a.b | doit">
        </ul>
    </div>
    <div id="test3">
        <div data-monster-attributes="class path:a.b">
            <input data-monster-attributes="value path:a.c" id="input1">
            <input data-monster-attributes="checked path:a.checkbox" type="checkbox" name="checkbox" id="checkbox">
            <input data-monster-attributes="value path:a.text" type="text" name="text" id="text">
            <input data-monster-attributes="checked path:a.radio" type="radio" name="radio" value="r1" id="radio">
            <input type="radio" name="radio" value="r2" id="r2">
            <input type="radio" name="radio" value="rx" id="rx">

            <select data-monster-attributes="value path:a.select" name="select" id="select">
                <option value="other-value">value1</option>
                <option>value2</option>
            </select>

            <select data-monster-attributes="value path:a.multiselect" name="multiselect" multiple id="multiselect">
                <option>value1</option>
                <option>value2</option>
                <option>value3</option>
                <option>value4</option>
                <option value="other-value5">value5</option>
            </select>

            <textarea name="textarea" id="textarea" data-monster-attributes="value path:a.textarea"></textarea>

            <input id="property-checkbox" type="checkbox" data-monster-properties="checked path:a.checkboxBool">
            <input id="property-input" type="text" data-monster-properties="value path:a.checkboxBool">
            <monster-test-property id="property-custom" data-monster-properties="active path:a.checkboxBool, payload path:a.payload"></monster-test-property>
            <monster-test-option-control id="property-option-control" data-monster-properties="option:features.enabled path:a.checkboxBool, option:labels.count path:a.optionCount"></monster-test-option-control>

        </div>
    </div>

`;

let html2 = `
    <div id="test1">
        <div data-monster-replace="path:text | tolower"></div>
        <div data-monster-replace="path:text | call:myformatter"></div>
        <div data-monster-replace="static:hello\\ "></div>
    </div>
`;

let html3 = `

    <template id="myinnerid">
        <span data-monster-replace="path:myinnerid | tojson" data-monster-properties="title path:myinnerid.i"></span>
    </template>

    <template id="myid">
        <p data-monster-insert="myinnerid path:a.b"></p>
    </template>

    <div id="test1">
        <div data-monster-insert="myid path:a.b"></div>
    </div>

`;

let html4 = `
    <div>
        <form id="form1">
            <input type="checkbox" value="checked" name="checkbox" data-monster-bind="path:state">
            <input type="text" name="text">
            <input type="radio" name="radio" value="r1" id="r1" data-monster-bind="path:radio">
            <input type="radio" name="radio" value="r2" id="r2" data-monster-bind="path:radio">
            <input type="radio" name="radio" value="rx" id="rx" data-comment="not called because no bind attribute">
            <input type="button" name="button">

            <select name="select1" id="select1" data-monster-bind="path:select">
                <option>value1</option>
                <option>value2</option>
            </select>

            <select name="select2" multiple id="select2" data-monster-bind="path:multiselect">
                <option>value1</option>
                <option>value2</option>
                <option>value3</option>
                <option>value4</option>
                <option>value5</option>
            </select>

            <textarea name="textarea" id="textarea" data-monster-bind="path:textarea">
            
        </textarea>

        </form>
    </div>

`;

let htmlNested = `
    <div id="parent">
        <input id="parent-input" type="text" data-monster-bind="path:parentVal">
        <div id="child">
            <input id="child-input" type="text" data-monster-bind="path:childVal">
        </div>
    </div>
`;

let htmlStatefulReplace = `
    <div id="test-stateful">
        <div data-monster-replace="path:content"></div>
    </div>
`;

let htmlPatch = `
    <template id="patch-item">
        <li data-monster-patch="path:patch-item.label"></li>
    </template>
    <div id="test-patch">
        <div id="patch-text" data-monster-patch="path:text"></div>
        <div id="patch-node" data-monster-patch="path:contentNode"></div>
        <div id="patch-fragment" data-monster-patch="path:fragmentNode"></div>
        <div id="patch-array" data-monster-patch="path:itemsArray"></div>
        <div id="patch-keyed" data-monster-patch="path:keyedItems" data-monster-patch-key="call:getPatchKey"></div>
        <div id="patch-keyed-render" data-monster-patch="path:keyedDataItems" data-monster-patch-key="path:id" data-monster-patch-render="path:label"></div>
        <ul id="patch-list" data-monster-insert="patch-item path:items"></ul>
    </div>
`;

let htmlPatchVsReplace = `
    <div id="test-patch-vs-replace">
        <div id="replace-target" data-monster-replace="path:replaceHtml"></div>
        <div
            id="patch-target"
            data-monster-patch="path:patchItems"
            data-monster-patch-key="path:id"
            data-monster-patch-render="call:renderPatchItem"
        ></div>
        <div
            id="replace-race-target"
            data-monster-replace="path:replaceRaceHtml"
        ></div>
        <div
            id="patch-race-target"
            data-monster-patch="path:patchRaceItems"
            data-monster-patch-key="path:id"
            data-monster-patch-render="call:renderPatchRaceItem"
        ></div>
    </div>
`;

describe("DOM", function () {
  let Updater = null;

  before(function (done) {
    const options = {};
    initJSDOM(options).then(() => {
      import("../../../source/dom/updater.mjs")
        .then((m) => {
          Updater = m.Updater;

          if (!customElements.get("monster-test-property")) {
            class MonsterTestProperty extends HTMLElement {
              set active(value) {
                this._active = value;
              }

              get active() {
                return this._active;
              }

              set payload(value) {
                this._payload = value;
              }

              get payload() {
                return this._payload;
              }
            }

            customElements.define("monster-test-property", MonsterTestProperty);
          }

          if (!customElements.get("monster-test-option-control")) {
            class MonsterTestOptionControl extends HTMLElement {
              constructor() {
                super();
                this._options = {
                  features: {
                    enabled: false,
                  },
                  labels: {
                    count: 0,
                  },
                };
              }

              getOption(path) {
                return path.split(".").reduce((ref, part) => ref?.[part], this._options);
              }

              setOption(path, value) {
                const parts = path.split(".");
                let ref = this._options;
                while (parts.length > 1) {
                  const part = parts.shift();
                  if (ref[part] === undefined || ref[part] === null) {
                    ref[part] = {};
                  }
                  ref = ref[part];
                }
                ref[parts[0]] = value;
              }
            }

            customElements.define(
              "monster-test-option-control",
              MonsterTestOptionControl,
            );
          }

          if (!customElements.get("monster-test-churn-item")) {
            class MonsterTestChurnItem extends HTMLElement {
              static stats = {
                replace: { created: 0, connected: 0, disconnected: 0 },
                patch: { created: 0, connected: 0, disconnected: 0 },
              };

              connectedCallback() {
                const mode = this.getAttribute("data-mode") || "replace";
                if (this.__countedCreated !== true) {
                  this.__countedCreated = true;
                  MonsterTestChurnItem.stats[mode].created++;
                }
                MonsterTestChurnItem.stats[mode].connected++;
              }

              disconnectedCallback() {
                const mode = this.getAttribute("data-mode") || "replace";
                MonsterTestChurnItem.stats[mode].disconnected++;
              }

              static resetStats() {
                MonsterTestChurnItem.stats = {
                  replace: { created: 0, connected: 0, disconnected: 0 },
                  patch: { created: 0, connected: 0, disconnected: 0 },
                };
              }
            }

            customElements.define(
              "monster-test-churn-item",
              MonsterTestChurnItem,
            );
          }

          if (!customElements.get("monster-test-race-item")) {
            class MonsterTestRaceItem extends HTMLElement {
              static stats = {
                replace: {
                  connected: 0,
                  disconnected: 0,
                  firedConnected: 0,
                  firedDisconnected: 0,
                },
                patch: {
                  connected: 0,
                  disconnected: 0,
                  firedConnected: 0,
                  firedDisconnected: 0,
                },
              };
              static pendingCallbacks = [];

              connectedCallback() {
                const mode = this.getAttribute("data-mode") || "replace";
                MonsterTestRaceItem.stats[mode].connected++;

                MonsterTestRaceItem.pendingCallbacks.push(() => {
                  if (this.isConnected) {
                    MonsterTestRaceItem.stats[mode].firedConnected++;
                  } else {
                    MonsterTestRaceItem.stats[mode].firedDisconnected++;
                  }
                });
              }

              disconnectedCallback() {
                const mode = this.getAttribute("data-mode") || "replace";
                MonsterTestRaceItem.stats[mode].disconnected++;
              }

              static flushPendingCallbacks() {
                const pending = [...MonsterTestRaceItem.pendingCallbacks];
                MonsterTestRaceItem.pendingCallbacks = [];
                for (const callback of pending) {
                  callback();
                }
              }

              static resetStats() {
                MonsterTestRaceItem.stats = {
                  replace: {
                    connected: 0,
                    disconnected: 0,
                    firedConnected: 0,
                    firedDisconnected: 0,
                  },
                  patch: {
                    connected: 0,
                    disconnected: 0,
                    firedConnected: 0,
                    firedDisconnected: 0,
                  },
                };
                MonsterTestRaceItem.pendingCallbacks = [];
              }
            }

            customElements.define(
              "monster-test-race-item",
              MonsterTestRaceItem,
            );
          }

          done();
        })
        .catch((e) => {
          done(e);
        });
    });
  });

  beforeEach(() => {
    let mocks = document.getElementById("mocks");
    mocks.innerHTML = html1;
  });

  afterEach(() => {
    let mocks = document.getElementById("mocks");
    mocks.innerHTML = "";
  });

  // REFACTOR: Ein einziger, klar benannter describe-Block für zusammengehörige Tests.
  describe("Setup, Configuration, and Error Handling", function () {
    let element;

    // REFACTOR: Code-Wiederholung durch einen beforeEach-Hook vermeiden.
    beforeEach(() => {
      // Annahme: Die HTML-Struktur aus der vorherigen Anfrage ist im DOM vorhanden.
      // Falls nicht, müsste hier das entsprechende HTML geladen werden.
      element = document.getElementById("test1");
    });

	    describe("Configuration Methods", function () {
      it("setEventTypes() should be chainable", function () {
        const u = new Updater(element);
        expect(u.setEventTypes(["touch"])).to.be.instanceof(Updater);
      });

      it("getSubject() should return the subject proxy", function () {
        const subject = { a: 1 };
        const u = new Updater(element, subject);
        expect(u.getSubject().a).to.be.equal(1);
      });

      it("enableEventProcessing() should be chainable", function () {
        const u = new Updater(element);
        expect(u.enableEventProcessing()).to.be.instanceof(Updater);
      });

	      it("disableEventProcessing() should be chainable", function () {
	        const u = new Updater(element);
	        expect(u.disableEventProcessing()).to.be.instanceof(Updater);
	      });

	      it("dispose() should stop future DOM updates", function (done) {
	        const subject = { text: "first" };
	        const target = document.createElement("div");
	        target.innerHTML = `<div data-monster-replace="path:text"></div>`;
	        document.getElementById("mocks").appendChild(target);

	        const u = new Updater(target, subject);
	        u.run()
	          .then(() => {
	            setTimeout(() => {
	              try {
	                expect(target).contain.html("<div data-monster-replace=\"path:text\">first</div>");
	                u.dispose();
	                u.getSubject().text = "second";

	                setTimeout(() => {
	                  try {
	                    expect(target).contain.html("<div data-monster-replace=\"path:text\">first</div>");
	                    done();
	                  } catch (e) {
	                    done(e);
	                  }
	                }, 20);
	              } catch (e) {
	                done(e);
	              }
	            }, 0);
	          })
	          .catch((e) => done(e));
	      });
	    });

    describe("Constructor Error Handling", function () {
      it("should throw a TypeError if no HTMLElement is provided", function () {
        expect(() => new Updater()).to.throw(TypeError);
      });

      it("should throw a TypeError if the subject is not an object", function () {
        expect(() => new Updater(element, null)).to.throw(TypeError);
      });
    });
    describe("Setup, Configuration, and Error Handling", function () {
      let element;

      beforeEach(() => {
        element = document.getElementById("test1");
      });

      describe("Configuration Methods", function () {
        it("setEventTypes() should be chainable", function () {
          const u = new Updater(element);
          expect(u.setEventTypes(["touch"])).to.be.instanceof(Updater);
        });

        it("getSubject() should return the subject proxy", function () {
          const subject = { a: 1 };
          const u = new Updater(element, subject);
          expect(u.getSubject().a).to.be.equal(1);
        });

        it("enableEventProcessing() should be chainable", function () {
          const u = new Updater(element);
          expect(u.enableEventProcessing()).to.be.instanceof(Updater);
        });

        it("disableEventProcessing() should be chainable", function () {
          const u = new Updater(element);
          expect(u.disableEventProcessing()).to.be.instanceof(Updater);
        });
      });

      describe("Constructor Error Handling", function () {
        it("should throw a TypeError if no HTMLElement is provided", function () {
          expect(() => new Updater()).to.throw(TypeError);
        });

        it("should throw a TypeError if the subject is not an object", function () {
          expect(() => new Updater(element, null)).to.throw(TypeError);
        });
      });

      describe("Runtime Error Handling", function () {
        it("should add an error attribute if a value for data-monster-insert is not iterable", function (done) {
          const element = document.getElementById("test1");
          const u = new Updater(element, { a: { b: null } });
          u.run()
            .then(() => {
              const expectedErrorMessage = "the value is not iterable";
              expect(element).to.have.attribute(
                "data-monster-error",
                expectedErrorMessage,
              );
              done();
            })
            .catch((err) => {
              done(
                new Error(
                  "Promise was rejected but should have been resolved. Error: " +
                    err,
                ),
              );
            });
        });
      });
    });
  });

  describe("Updater()", function () {
    describe("new Updater", function () {
      it("should return document object", function () {
        let element = document.getElementById("test1");

        let d = new Updater(element, {});

        expect(typeof d).is.equal("object");
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = htmlPatchVsReplace;
      customElements.get("monster-test-churn-item").resetStats();
      customElements.get("monster-test-race-item").resetStats();
    });

    describe("Patch versus Replace", function () {
      it("should show less lifecycle churn for keyed patch than for replace under repeated updates", function (done) {
        const buildReplaceHtml = (items) =>
          items
            .map(
              ({ id, label }) =>
                `<monster-test-churn-item data-mode="replace" data-id="${id}">${label}</monster-test-churn-item>`,
            )
            .join("");

        const patchNodes = new Map();
        const renderPatchItem = (item) => {
          let node = patchNodes.get(item.id);
          if (!(node instanceof HTMLElement)) {
            node = document.createElement("monster-test-churn-item");
            node.setAttribute("data-mode", "patch");
            node.setAttribute("data-id", item.id);
            patchNodes.set(item.id, node);
          }
          node.textContent = item.label;
          return node;
        };

        const createItems = (suffix = "") => [
          { id: "a", label: `Alpha${suffix}` },
          { id: "b", label: `Beta${suffix}` },
          { id: "c", label: `Gamma${suffix}` },
          { id: "d", label: `Delta${suffix}` },
        ];

        const subject = {
          replaceHtml: buildReplaceHtml(createItems()),
          patchItems: createItems(),
        };

        const updater = new Updater(
          document.getElementById("test-patch-vs-replace"),
          subject,
        );
        updater.setCallback("renderPatchItem", renderPatchItem);

        updater
          .run()
          .then(() => {
            setTimeout(() => {
              try {
                const sequences = [
                  ["d", "a", "b", "c"],
                  ["c", "d", "a", "b"],
                  ["b", "c", "d", "a"],
                  ["a", "b", "c", "d"],
                ];

                for (let i = 0; i < 6; i++) {
                  const next = sequences[i % sequences.length].map((id) => {
                    const labelMap = {
                      a: "Alpha",
                      b: "Beta",
                      c: "Gamma",
                      d: "Delta",
                    };
                    return {
                      id,
                      label: `${labelMap[id]}-${i}`,
                    };
                  });
                  updater.getSubject().replaceHtml = buildReplaceHtml(next);
                  updater.getSubject().patchItems = next;
                }

                setTimeout(() => {
                  try {
                    const churnStats =
                      customElements.get("monster-test-churn-item").stats;
                    expect(churnStats.patch.created).to.equal(4);
                    expect(churnStats.replace.created).to.be.greaterThan(
                      churnStats.patch.created,
                    );
                    expect(churnStats.replace.disconnected).to.be.greaterThan(0);
                    expect(churnStats.replace.created).to.be.greaterThan(
                      churnStats.replace.disconnected / 2,
                    );
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 40);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should reduce disconnected async callback surface with patch compared to replace", function (done) {
        const buildReplaceRaceHtml = (items) =>
          items
            .map(
              ({ id, label }) =>
                `<monster-test-race-item data-mode="replace" data-id="${id}">${label}</monster-test-race-item>`,
            )
            .join("");

        const patchNodes = new Map();
        const renderPatchRaceItem = (item) => {
          let node = patchNodes.get(item.id);
          if (!(node instanceof HTMLElement)) {
            node = document.createElement("monster-test-race-item");
            node.setAttribute("data-mode", "patch");
            node.setAttribute("data-id", item.id);
            patchNodes.set(item.id, node);
          }
          node.textContent = item.label;
          return node;
        };

        const sequences = [
          [
            { id: "a", label: "Alpha-0" },
            { id: "b", label: "Beta-0" },
            { id: "c", label: "Gamma-0" },
          ],
          [
            { id: "c", label: "Gamma-1" },
            { id: "a", label: "Alpha-1" },
            { id: "b", label: "Beta-1" },
          ],
          [
            { id: "b", label: "Beta-2" },
            { id: "c", label: "Gamma-2" },
            { id: "a", label: "Alpha-2" },
          ],
        ];

        const updater = new Updater(
          document.getElementById("test-patch-vs-replace"),
          {
            replaceRaceHtml: buildReplaceRaceHtml(sequences[0]),
            patchRaceItems: sequences[0],
          },
        );
        updater.setCallback("renderPatchRaceItem", renderPatchRaceItem);

        updater
          .run()
          .then(() => {
            setTimeout(() => {
              try {
                updater.getSubject().replaceRaceHtml = buildReplaceRaceHtml(
                  sequences[1],
                );
                updater.getSubject().patchRaceItems = sequences[1];

                updater.getSubject().replaceRaceHtml = buildReplaceRaceHtml(
                  sequences[2],
                );
                updater.getSubject().patchRaceItems = sequences[2];

                setTimeout(() => {
                  try {
                    const raceItem = customElements.get("monster-test-race-item");
                    raceItem.flushPendingCallbacks();
                    const raceStats = raceItem.stats;
                    expect(raceStats.patch.firedDisconnected).to.equal(0);
                    expect(raceStats.replace.firedDisconnected).to.be.greaterThan(
                      0,
                    );
                    expect(raceStats.replace.firedDisconnected).to.be.greaterThan(
                      raceStats.patch.firedDisconnected,
                    );
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 50);
              } catch (e) {
                done(e);
              }
            }, 0);
          })
          .catch((e) => done(new Error(e)));
      });
    });
  });

  describe("Updater()", function () {
    describe("Repeat", function () {
      it("should build 6 li elements from an array", function (done) {
        let element = document.getElementById("test1");
        const testData = {
          a: {
            b: [
              { i: "0" },
              { i: "1" },
              { i: "2" },
              { i: "3" },
              { i: "4" },
              { i: "5" },
            ],
          },
        };
        let d = new Updater(element, testData);

        d.run()
          .then(() => {
            const ul = element.querySelector("ul");
            const listItems = ul.children;

            // 1. Prüfe die korrekte Anzahl der Elemente
            expect(listItems.length).to.equal(6);

            // 2. Stichprobenartige Prüfung von Elementen
            const firstItem = listItems[0];
            expect(firstItem.tagName).to.equal("LI");
            expect(firstItem).to.have.attribute(
              "data-monster-insert-reference",
              "current-0",
            );
            expect(firstItem.textContent).to.equal('{"i":"0"}');

            const lastItem = listItems[5];
            expect(lastItem).to.have.attribute(
              "data-monster-insert-reference",
              "current-5",
            );
            expect(lastItem.textContent).to.equal('{"i":"5"}');

            done();
          })
          .catch(done);
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = html4;
    });

    describe("Eventhandling", function () {
      let updater, form1, proxyobserver;
      beforeEach(() => {
        proxyobserver = new ProxyObserver({});
        updater = new Updater(document.getElementById("form1"), proxyobserver);
        form1 = document.getElementById("form1");
      });

      // here click events are thrown on the checkbox and the setting of the value is observed via the proxyobserver.
      it("should handle checkbox click events", function (done) {
        updater.enableEventProcessing();

        let subject = updater.getSubject();
        expect(subject).is.equal(proxyobserver.getSubject());

        let expected = ["checked", undefined, "checked"];
        // here the notation with function is important, because the pointer is set.
        proxyobserver.attachObserver(
          new Observer(function () {
            let e = expected.shift();
            console.log(e, this.getSubject());
            if (e === undefined && expected.length === 0)
              done(new Error("to many calls"));

            if (this.getSubject()["state"] !== e)
              done(new Error(this.getSubject()["state"] + " should " + e));
            if (expected.length === 0) {
              done();
            } else {
              setTimeout(() => {
                form1.querySelector("[name=checkbox]").click();
              }, 10);
            }
          }),
        );

        setTimeout(() => {
          form1.querySelector("[name=checkbox]").click();
        }, 10);
      });

      it("should handle radio click events 1", function (done) {
        updater.enableEventProcessing();

        let subject = updater.getSubject();
        expect(subject).is.equal(proxyobserver.getSubject());

        let expected = ["r1", "r2", "r1"];
        let clickTargets = ["r2", "r1"];
        // here the notation with function is important, because the this pointer is set.
        proxyobserver.attachObserver(
          new Observer(function () {
            let e = expected.shift();
            if (e === undefined && expected.length === 0)
              done(new Error("to many calls"));

            let v = this.getSubject()["radio"];
            if (v !== e) done(new Error(v + " should " + e));
            if (expected.length === 0) {
              done();
            } else {
              setTimeout(() => {
                document.getElementById(clickTargets.shift()).click();
              }, 10);
            }
          }),
        );

        setTimeout(() => {
          document.getElementById("r1").click();
        }, 10);

        // no handler // bind
        setTimeout(() => {
          document.getElementById("rx").click();
        }, 20);
      });

      it("should handle select click events 2", function (done) {
        let selectElement = document.getElementById("select1");

        updater.enableEventProcessing();

        let subject = updater.getSubject();
        expect(subject).is.equal(proxyobserver.getSubject());

        let expected = ["value2", "value1", "value2"];
        // here the notation with function is important, because the this pointer is set.
        proxyobserver.attachObserver(
          new Observer(function () {
            let e = expected.shift();
            if (e === undefined && expected.length === 0)
              done(new Error("to many calls"));

            let v = this.getSubject()["select"];
            if (v !== e) done(new Error(v + " should " + e));
            if (expected.length === 0) {
              done();
            } else {
              setTimeout(() => {
                selectElement.selectedIndex =
                  selectElement.selectedIndex === 1 ? 0 : 1;
                selectElement.click();
              }, 10);
            }
          }),
        );

        setTimeout(() => {
          // set value and simulate click event for bubble
          selectElement.selectedIndex = 1;
          selectElement.click();
        }, 20);
      });

      it("should handle textarea events", function (done) {
        let textareaElement = document.getElementById("textarea");

        updater.enableEventProcessing();

        let subject = updater.getSubject();
        expect(subject).is.equal(proxyobserver.getSubject());

        let expected = ["testX", "lorem ipsum", ""];
        let testValues = ["lorem ipsum", ""];
        // here the notation with function is important, because the this pointer is set.
        proxyobserver.attachObserver(
          new Observer(function () {
            let e = expected.shift();
            if (e === undefined && expected.length === 0)
              done(new Error("to many calls"));

            let v = this.getSubject()["textarea"];
            if (JSON.stringify(v) !== JSON.stringify(e))
              done(
                new Error(JSON.stringify(v) + " should " + JSON.stringify(e)),
              );
            if (expected.length === 0) {
              done();
            } else {
              setTimeout(() => {
                textareaElement.value = testValues.shift();
                textareaElement.click();
              }, 10);
            }
          }),
        );

        setTimeout(() => {
          // set value and simulate click event for bubble
          textareaElement.value = "testX";
          textareaElement.click();
        }, 20);
      });

      it("should handle multiple select events", function (done) {
        let selectElement = document.getElementById("select2");

        updater.enableEventProcessing();

        let subject = updater.getSubject();
        expect(subject).is.equal(proxyobserver.getSubject());

        let expected = [
          ["value1"],
          ["value2", "value3", "value4"],
          ["value1", "value4"],
        ];

        let testSelections = [
          [false, true, true, true],
          [true, false, false, true],
        ];

        // here the notation with function is important, because the this pointer is set.
        proxyobserver.attachObserver(
          new Observer(function () {
            let e = expected.shift();
            if (e === undefined && expected.length === 0)
              done(new Error("to many calls"));

            let v = this.getSubject()["multiselect"];

            if (JSON.stringify(v) !== JSON.stringify(e))
              done(
                new Error(JSON.stringify(v) + " should " + JSON.stringify(e)),
              );
            if (expected.length === 0) {
              done();
            } else {
              setTimeout(() => {
                let v = testSelections.shift();
                selectElement.options[0].selected = v[0];
                selectElement.options[1].selected = v[1];
                selectElement.options[2].selected = v[2];
                selectElement.options[3].selected = v[3];
                selectElement.click();
              }, 10);
            }
          }),
        );

        setTimeout(() => {
          selectElement.options[0].selected = true;
          selectElement.options[1].selected = false;
          selectElement.options[2].selected = false;
          selectElement.options[3].selected = false;
          selectElement.click();
        }, 20);
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = htmlNested;
    });

    describe("Nested Eventhandling", function () {
      it("should not propagate child events to parent updater", function (done) {
        const parentObserver = new ProxyObserver({});
        const childObserver = new ProxyObserver({});
        const parentUpdater = new Updater(
          document.getElementById("parent"),
          parentObserver,
        );
        const childUpdater = new Updater(
          document.getElementById("child"),
          childObserver,
        );

        parentUpdater.enableEventProcessing();
        childUpdater.enableEventProcessing();

        let parentNotified = false;
        parentObserver.attachObserver(
          new Observer(function () {
            parentNotified = true;
          }),
        );

        childObserver.attachObserver(
          new Observer(function () {
            try {
              expect(childUpdater.getSubject()["childVal"]).to.equal("child");
              expect(parentUpdater.getSubject()).to.not.have.property(
                "childVal",
              );
              setTimeout(() => {
                if (parentNotified) {
                  done(
                    new Error(
                      "parent updater should not react to child bind events",
                    ),
                  );
                } else {
                  done();
                }
              }, 10);
            } catch (e) {
              done(e);
            }
          }),
        );

        const childInput = document.getElementById("child-input");
        childInput.value = "child";
        childInput.click();
      });

      it("should handle parent events without triggering child updater", function (done) {
        const parentObserver = new ProxyObserver({});
        const childObserver = new ProxyObserver({});
        const parentUpdater = new Updater(
          document.getElementById("parent"),
          parentObserver,
        );
        const childUpdater = new Updater(
          document.getElementById("child"),
          childObserver,
        );

        parentUpdater.enableEventProcessing();
        childUpdater.enableEventProcessing();

        let childNotified = false;
        childObserver.attachObserver(
          new Observer(function () {
            childNotified = true;
          }),
        );

        parentObserver.attachObserver(
          new Observer(function () {
            try {
              expect(parentUpdater.getSubject()["parentVal"]).to.equal("parent");
              expect(childUpdater.getSubject()).to.not.have.property(
                "parentVal",
              );
              setTimeout(() => {
                if (childNotified) {
                  done(
                    new Error(
                      "child updater should not react to parent bind events",
                    ),
                  );
                } else {
                  done();
                }
              }, 10);
            } catch (e) {
              done(e);
            }
          }),
        );

        const parentInput = document.getElementById("parent-input");
        parentInput.value = "parent";
        parentInput.click();
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = html2;
    });

    describe("Replace", function () {
      it("should add lower hello and HELLOyes!", function (done) {
        let element = document.getElementById("test1");

        let d = new Updater(element, {
          text: "HALLO",
        });

        d.setCallback("myformatter", function (a) {
          return a + "yes!";
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              expect(typeof d).is.equal("object");
              expect(element).contain.html(
                '<div data-monster-replace="path:text | tolower">hallo</div>',
              );
              expect(element).contain.html(
                '<div data-monster-replace="path:text | call:myformatter">HALLOyes!</div>',
              );
              expect(element).contain.html(
                '<div data-monster-replace="static:hello\\ ">hello </div>',
              );

              return done();
            }, 100);
          })
          .catch((e) => {
            done(new Error(e));
          });
      });

      it("should dispose linked updaters when replacing a mounted stateful subtree", function (done) {
        let mocks = document.getElementById("mocks");
        mocks.innerHTML = htmlStatefulReplace;

        const statefulElement = document.createElement("div");
        statefulElement.innerHTML = "<span>stateful</span>";

        let disposeCount = 0;
        addToObjectLink(
          statefulElement,
          customElementUpdaterLinkSymbol,
          new Set([
            {
              dispose() {
                disposeCount++;
              },
            },
          ]),
        );

        let d = new Updater(document.getElementById("test-stateful"), {
          content: statefulElement,
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                expect(statefulElement.isConnected).to.equal(true);
                d.getSubject().content = "<div>replaced</div>";

                setTimeout(() => {
                  try {
                    expect(disposeCount).to.equal(1);
                    expect(statefulElement.isConnected).to.equal(false);
                    expect(document.getElementById("test-stateful")).contain.html(
                      "<div>replaced</div>",
                    );
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 40);
              } catch (e) {
                done(e);
              }
            }, 0);
          })
          .catch((e) => {
            done(new Error(e));
          });
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = htmlPatch;
    });

    describe("Patch", function () {
      it("should patch primitive values as text content without parsing HTML", function (done) {
        let element = document.getElementById("test-patch");
        let d = new Updater(element, {
          text: "<strong>Hello</strong>",
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-text");
                expect(target.innerHTML).to.equal("&lt;strong&gt;Hello&lt;/strong&gt;");
                expect(target.textContent).to.equal("<strong>Hello</strong>");
                done();
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should patch HTMLElement values without reparsing HTML", function (done) {
        let element = document.getElementById("test-patch");
        const stateful = document.createElement("monster-message-state-button");
        stateful.innerHTML = "Save";

        let d = new Updater(element, {
          text: "",
          contentNode: stateful,
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-node");
                expect(target.firstElementChild).to.equal(stateful);
                expect(stateful.isConnected).to.equal(true);
                done();
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should rewrite nested patch paths in inserted templates", function (done) {
        let element = document.getElementById("test-patch");
        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [],
          items: [{ label: "One" }, { label: "Two" }],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const list = document.getElementById("patch-list");
                expect(list.children.length).to.equal(2);
                expect(list.children[0].getAttribute("data-monster-patch")).to.equal(
                  "path:items.0.label",
                );
                expect(list.children[0].textContent).to.equal("One");
                expect(list.children[1].getAttribute("data-monster-patch")).to.equal(
                  "path:items.1.label",
                );
                expect(list.children[1].textContent).to.equal("Two");
                done();
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should patch DocumentFragment values without reparsing HTML", function (done) {
        let element = document.getElementById("test-patch");
        const fragment = document.createDocumentFragment();
        const first = document.createElement("span");
        const second = document.createElement("strong");
        first.textContent = "Alpha";
        second.textContent = "Beta";
        fragment.appendChild(first);
        fragment.appendChild(second);

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: fragment,
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-fragment");
                expect(target.children.length).to.equal(2);
                expect(target.children[0].textContent).to.equal("Alpha");
                expect(target.children[1].textContent).to.equal("Beta");
                done();
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should patch arrays of primitives and nodes without using innerHTML", function (done) {
        let element = document.getElementById("test-patch");
        const badge = document.createElement("span");
        badge.textContent = "Node";

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: ["Alpha", badge, "Omega"],
          keyedItems: [],
          keyedDataItems: [],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-array");
                expect(target.childNodes.length).to.equal(3);
                expect(target.childNodes[0].textContent).to.equal("Alpha");
                expect(target.childNodes[1]).to.equal(badge);
                expect(target.childNodes[2].textContent).to.equal("Omega");
                expect(target.innerHTML).to.equal("Alpha<span>Node</span>Omega");
                done();
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should reorder keyed nodes without recreating them", function (done) {
        let element = document.getElementById("test-patch");
        const a = document.createElement("span");
        const b = document.createElement("span");
        const c = document.createElement("span");
        a.dataset.key = "a";
        b.dataset.key = "b";
        c.dataset.key = "c";
        a.textContent = "A";
        b.textContent = "B";
        c.textContent = "C";

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [a, b, c],
          keyedDataItems: [],
          items: [],
        });
        d.setCallback("getPatchKey", (value) => value?.dataset?.key);

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-keyed");
                expect(Array.from(target.children)).to.deep.equal([a, b, c]);

                d.getSubject().keyedItems = [c, a, b];

                setTimeout(() => {
                  try {
                    expect(Array.from(target.children)).to.deep.equal([c, a, b]);
                    expect(target.children[0]).to.equal(c);
                    expect(target.children[1]).to.equal(a);
                    expect(target.children[2]).to.equal(b);
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 20);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should remove stale keyed nodes and keep surviving node identity", function (done) {
        let element = document.getElementById("test-patch");
        const a = document.createElement("span");
        const b = document.createElement("span");
        const dNode = document.createElement("span");
        a.dataset.key = "a";
        b.dataset.key = "b";
        dNode.dataset.key = "d";
        a.textContent = "A";
        b.textContent = "B";
        dNode.textContent = "D";

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [a, b],
          keyedDataItems: [],
          items: [],
        });
        d.setCallback("getPatchKey", (value) => value?.dataset?.key);

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-keyed");
                d.getSubject().keyedItems = [b, dNode];

                setTimeout(() => {
                  try {
                    expect(Array.from(target.children)).to.deep.equal([b, dNode]);
                    expect(b.isConnected).to.equal(true);
                    expect(a.isConnected).to.equal(false);
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 20);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should reorder keyed data items via patch-render without recreating text nodes", function (done) {
        let element = document.getElementById("test-patch");

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [
            { id: "a", label: "Alpha" },
            { id: "b", label: "Beta" },
            { id: "c", label: "Gamma" },
          ],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-keyed-render");
                const initialNodes = Array.from(target.childNodes);
                expect(initialNodes.map((node) => node.textContent)).to.deep.equal([
                  "Alpha",
                  "Beta",
                  "Gamma",
                ]);

                d.getSubject().keyedDataItems = [
                  { id: "c", label: "Gamma" },
                  { id: "a", label: "Alpha" },
                  { id: "b", label: "Beta" },
                ];

                setTimeout(() => {
                  try {
                    const reorderedNodes = Array.from(target.childNodes);
                    expect(reorderedNodes.map((node) => node.textContent)).to.deep.equal([
                      "Gamma",
                      "Alpha",
                      "Beta",
                    ]);
                    expect(reorderedNodes[0]).to.equal(initialNodes[2]);
                    expect(reorderedNodes[1]).to.equal(initialNodes[0]);
                    expect(reorderedNodes[2]).to.equal(initialNodes[1]);
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 20);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should update keyed data item text in place via patch-render", function (done) {
        let element = document.getElementById("test-patch");

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [
            { id: "a", label: "Alpha" },
            { id: "b", label: "Beta" },
          ],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-keyed-render");
                const betaNode = target.childNodes[1];

                d.getSubject().keyedDataItems = [
                  { id: "a", label: "Alpha" },
                  { id: "b", label: "Bravo" },
                ];

                setTimeout(() => {
                  try {
                    expect(target.childNodes[1]).to.equal(betaNode);
                    expect(target.childNodes[1].textContent).to.equal("Bravo");
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 20);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });

      it("should remove stale keyed data items and append new ones via patch-render", function (done) {
        let element = document.getElementById("test-patch");

        let d = new Updater(element, {
          text: "",
          contentNode: document.createElement("div"),
          fragmentNode: document.createDocumentFragment(),
          itemsArray: [],
          keyedItems: [],
          keyedDataItems: [
            { id: "a", label: "Alpha" },
            { id: "b", label: "Beta" },
          ],
          items: [],
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              try {
                const target = document.getElementById("patch-keyed-render");
                const alphaNode = target.childNodes[0];
                const betaNode = target.childNodes[1];

                d.getSubject().keyedDataItems = [
                  { id: "b", label: "Beta" },
                  { id: "d", label: "Delta" },
                ];

                setTimeout(() => {
                  try {
                    expect(Array.from(target.childNodes).map((node) => node.textContent)).to.deep.equal([
                      "Beta",
                      "Delta",
                    ]);
                    expect(target.childNodes[0]).to.equal(betaNode);
                    expect(alphaNode.isConnected).to.equal(false);
                    done();
                  } catch (e) {
                    done(e);
                  }
                }, 20);
              } catch (e) {
                done(e);
              }
            }, 20);
          })
          .catch((e) => done(new Error(e)));
      });
    });
  });

  describe("Updater()", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = html3;
    });

    describe("Replace", function () {
      it("should ", function (done) {
        let element = document.getElementById("test1");

        let d = new Updater(element, {
          a: {
            b: [{ i: "0" }],
          },
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              expect(typeof d).is.equal("object");
              expect(element).contain.html(
                '<div data-monster-insert="myid path:a.b">',
              );
              expect(element).contain.html(
                '<p data-monster-insert="myinnerid path:a.b" data-monster-insert-reference="myid-0">',
              );
              expect(element).contain.html(
                'data-monster-replace="path:a.b.0 | tojson"',
              );
              expect(element).contain.html(
                'data-monster-properties="title path:a.b.0.i"',
              );
              expect(element).contain.html('title="0"');

              done();
            }, 100);
          })
          .catch((e) => {
            done(new Error(e));
          });
      });
    });
  });

  describe("Updater()", function () {
    describe("Attributes", function () {
      it("should change attributes", function (done) {
        let element = document.getElementById("test3");

        let text = document.getElementById("text");
        expect(text.value).to.be.equal("");

        let radio = document.getElementById("radio");
        expect(radio.checked).to.be.false;

        let checkbox = document.getElementById("checkbox");
        expect(checkbox.checked).to.be.false;

        let select = document.getElementById("select");
        expect(select.selectedIndex).to.be.equal(0);

        let multiselect = document.getElementById("multiselect");
        expect(multiselect.selectedIndex).to.be.equal(-1);

        let textarea = document.getElementById("textarea");
        expect(textarea.value).to.be.equal("");

        let propertyCheckbox = document.getElementById("property-checkbox");
        expect(propertyCheckbox.checked).to.be.false;

        let propertyInput = document.getElementById("property-input");
        expect(propertyInput.value).to.be.equal("");

        let propertyCustom = document.getElementById("property-custom");
        expect(propertyCustom.active).to.be.equal(undefined);
        expect(propertyCustom.payload).to.be.equal(undefined);

        let propertyOptionControl = document.getElementById(
          "property-option-control",
        );
        expect(propertyOptionControl.getOption("features.enabled")).to.be.false;
        expect(propertyOptionControl.getOption("labels.count")).to.be.equal(0);

        let d = new Updater(element, {
          a: {
            b: "div-class",
            c: "hello",
            text: "hello",
            radio: "true",
            textarea: "test",
            multiselect: ["value3", "value4", "other-value5"],
            select: "value2",
            checkbox: "true",
            checkboxBool: true,
            optionCount: 1234,
            payload: {
              state: "ok",
            },
          },
        });

        d.run()
          .then(() => {
            setTimeout(() => {
              expect(element).contain.html(
                '<div data-monster-attributes="class path:a.b" class="div-class">',
              );
              expect(element).contain.html(
                '<input data-monster-attributes="value path:a.c" id="input1" value="hello">',
              );
              expect(element).contain.html(
                '<textarea name="textarea" id="textarea" data-monster-attributes="value path:a.textarea" value="test">',
              );
              expect(element).contain.html(
                '<input data-monster-attributes="checked path:a.radio" type="radio" name="radio" value="r1" id="radio" checked="true">',
              );

              expect(text.value, "text control").to.be.equal(
                d.getSubject()["a"]["c"],
              );
              expect(radio.checked, "radio control").to.be.equal(true);
              expect(textarea.value, "textarea control").to.be.equal(
                d.getSubject()["a"]["textarea"],
              );
              expect(select.selectedIndex, "select control").to.be.equal(1); // [0=>other-value, 1=>value2]

              let multiselectSelectedOptions = [];
              for (const [index, obj] of Object.entries(
                multiselect.selectedOptions,
              )) {
                multiselectSelectedOptions.push(obj.value);
              }

              expect(
                JSON.stringify(multiselectSelectedOptions),
                "multiselect control",
              ).to.be.equal(JSON.stringify(d.getSubject()["a"]["multiselect"]));
              expect(checkbox.checked, "checkbox control").to.be.true;
              expect(propertyCheckbox.checked, "property checkbox control").to.be
                .true;
              expect(propertyInput.value, "property input value").to.be.equal(
                "true",
              );
              expect(propertyCustom.active, "typed property boolean").to.be.true;
              expect(propertyCustom.payload, "typed property object").to.deep.equal(
                {
                  state: "ok",
                },
              );
              expect(propertyCustom.getAttribute("active")).to.be.equal(null);
              expect(
                propertyOptionControl.getOption("features.enabled"),
                "control option boolean",
              ).to.be.true;
              expect(
                propertyOptionControl.getOption("labels.count"),
                "control option number",
              ).to.be.equal(1234);

              done();
            }, 100);
          })
          .catch((e) => {
            done(new Error(e));
          });
      });
    });
  });

  describe("Get Attribute Pipe", function () {
    let id, mocks;
    beforeEach(() => {
      mocks = document.getElementById("mocks");
      id = new ID("monster");
      mocks.innerHTML =
        ` <div id="` +
        id +
        `"
            data-monster-replace="path:a | if:value:\\ "></div>`;
    });

    afterEach(() => {
      mocks.innerHTML = "";
    });

    it("should include space", function () {
      const div = document.getElementById(id.toString());

      const pipe = div.getAttribute("data-monster-replace");
      expect(pipe.length).to.be.equal(20);
    });
  });

  describe("manuel update", function () {
    let id, mocks;

    beforeEach(() => {
      mocks = document.getElementById("mocks");
      id = new ID("monster").toString();
      mocks.innerHTML =
        `<input id="` + id + `"data-monster-bind="path:myvalue">`;
    });

    afterEach(() => {
      mocks.innerHTML = "";
    });

    it("should get value", function () {
      document.getElementById(id).value = "hello";
      const updater = new Updater(mocks);
      const subject = updater.getSubject();

      expect(subject).to.not.have.property("myvalue");
      updater.retrieve();
      expect(subject).to.have.property("myvalue");
    });
  });

  /**
   * https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/112
   */
  describe("Updater() 20220107", function () {
    beforeEach(() => {
      let mocks = document.getElementById("mocks");
      // language=HTML
      mocks.innerHTML = `

                <div id="container">
                    <div data-monster-replace="path:content"></div>
                </div>


            `;
    });

    describe("Bugfix #112", function () {
      it("should add ", function (done) {
        let containerElement = document.getElementById("container");
        let newElement = document.createElement("div");
        newElement.innerHTML = "yeah! <b>Test</b>!";

        const containerHTML = containerElement.innerHTML;
        const newHTML = newElement.innerHTML;

        let d = new Updater(containerElement, {
          content: newElement,
        });
        setTimeout(() => {
          d.run().then(() => {
            setTimeout(() => {
              try {
                expect(containerElement).contain.html(
                  "<div>yeah! <b>Test</b>!</div>",
                );
              } catch (e) {
                return done(e);
              }

              done();
            }, 100);
          });
        }, 100);

        // d.setCallback('myformatter', function (a) {
        //     return a + 'yes!'
        // })
        //
        // setTimeout(() => {
        //     d.run().then(() => {
        //
        //         expect(typeof d).is.equal('object');
        //         expect(element).contain.html('<div data-monster-replace="path:text | tolower">hallo</div>');
        //         expect(element).contain.html('<div data-monster-replace="path:text | call:myformatter">HALLOyes!</div>');
        //         expect(element).contain.html('<div data-monster-replace="static:hello\\ ">hello </div>');
        //
        //         return done();
        //     }).catch(
        //         e => {
        //             done(new Error(e))
        //         })
        // }, 100)
      });
    });
  });

  describe("Updater reactive updates", function () {
    it("should update the DOM when the subject changes", function (done) {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = html1;

      const element = document.getElementById("test1");
      const updater = new Updater(element, { a: { b: ["one"] } });

      updater
        .run()
        .then(() => {
          expect(element).contain.html("one");

          const subject = updater.getSubject();
          subject.a.b = ["two"];

          setTimeout(() => {
            try {
              expect(element).contain.html("two");
              done();
            } catch (e) {
              done(e);
            }
          }, 50);
        })
        .catch((e) => done(e));
    });

    it("should update every parent-path binding when a nested path changes", function (done) {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = `
        <div id="nested-parent-1" data-monster-replace="path:a | tojson"></div>
        <div id="nested-leaf" data-monster-replace="path:a.b"></div>
        <div id="nested-parent-2" data-monster-replace="path:a | tojson"></div>
      `;

      const updater = new Updater(mocks, { a: { b: "first" } });

      updater
        .run()
        .then(() => {
          updater.getSubject().a.b = "second";

          setTimeout(() => {
            try {
              expect(document.getElementById("nested-parent-1").textContent).to.equal(
                '{"b":"second"}',
              );
              expect(document.getElementById("nested-leaf").textContent).to.equal(
                "second",
              );
              expect(document.getElementById("nested-parent-2").textContent).to.equal(
                '{"b":"second"}',
              );
              done();
            } catch (e) {
              done(e);
            }
          }, 50);
        })
        .catch((e) => done(e));
    });

    it('should keep "0" for optional number bindings', function (done) {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = `
        <input id="optional-number" data-monster-bind="path:value" data-monster-bind-type="number?">
      `;

      const updater = new Updater(mocks, {});
      updater.enableEventProcessing();

      const input = document.getElementById("optional-number");
      input.value = "0";
      input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));

      setTimeout(() => {
        try {
          expect(updater.getSubject().value).to.equal(0);
          done();
        } catch (e) {
          done(e);
        }
      }, 80);
    });

    it("should clear select attributes when bound values become undefined", function (done) {
      let mocks = document.getElementById("mocks");
      mocks.innerHTML = `
        <select id="select-one" data-monster-attributes="value path:single">
          <option value="a">a</option>
          <option value="b">b</option>
        </select>
        <select id="select-multiple" multiple data-monster-attributes="value path:multiple">
          <option value="a">a</option>
          <option value="b">b</option>
        </select>
      `;

      const updater = new Updater(mocks, {
        single: "b",
        multiple: ["a"],
      });

      updater
        .run()
        .then(() => {
          updater.getSubject().single = undefined;
          updater.getSubject().multiple = undefined;

          setTimeout(() => {
            try {
              const selectOne = document.getElementById("select-one");
              const selectMultiple = document.getElementById("select-multiple");
              expect(selectOne.selectedIndex).to.equal(-1);
              expect(Array.from(selectMultiple.selectedOptions)).to.have.length(0);
              expect(mocks.hasAttribute("data-monster-error")).to.be.false;
              done();
            } catch (e) {
              done(e);
            }
          }, 50);
        })
        .catch((e) => done(e));
    });
  });
});
