import { getGlobal } from "../../../../source/types/global.mjs";
import * as chai from "chai";
import { chaiDom } from "../../../util/chai-dom.mjs";
import { initJSDOM } from "../../../util/jsdom.mjs";
import { ResizeObserverMock } from "../../../util/resize-observer.mjs";

let expect = chai.expect;
chai.use(chaiDom);

const global = getGlobal();

let html1 = `
    <div id="test1">
    </div>
`;

let html2 = `
    <div id="test2">
        <monster-message-state-button data-monster-option-labels-button="Save">
            Save
        </monster-message-state-button>
    </div>
`;

let MessageStateButton;

describe("MessageStateButton", function () {
	before(function (done) {
		initJSDOM().then(() => {
			import("element-internals-polyfill").catch((e) => done(e));

			if (!global.ResizeObserver) {
				global.ResizeObserver = ResizeObserverMock;
			}

			import("../../../../source/components/form/message-state-button.mjs")
				.then((m) => {
					MessageStateButton = m["MessageStateButton"];
					done();
				})
				.catch((e) => done(e));
		});
	});

	describe("new MessageStateButton", function () {
		beforeEach(() => {
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = html1;
		});

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

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

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

			it("should contain monster-message-state-button", function () {
				expect(document.getElementById("test2")).contain.html(
					"<monster-message-state-button",
				);
			});
		});

		describe("document.createElement", function () {
			it("should instance of message-state-button", function () {
				expect(
					document.createElement("monster-message-state-button"),
				).is.instanceof(MessageStateButton);
			});
		});
	});

	describe("disabled toggle", function () {
		afterEach(() => {
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = "";
		});

		it("should sync disabled attribute to inner button", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			setTimeout(() => {
				try {
					const inner = button.shadowRoot.querySelector("monster-state-button");
					expect(inner).to.exist;

					button.setAttribute("disabled", "");
					setTimeout(() => {
						try {
							expect(inner.hasAttribute("disabled")).to.be.true;

							button.removeAttribute("disabled");
							setTimeout(() => {
								try {
									expect(inner.hasAttribute("disabled")).to.be.false;

									button.setAttribute("disabled", "");
									setTimeout(() => {
										try {
											expect(inner.hasAttribute("disabled")).to.be.true;
											done();
										} catch (e) {
											done(e);
										}
									}, 0);
								} catch (e) {
									done(e);
								}
							}, 0);
						} catch (e) {
							done(e);
						}
					}, 0);
				} catch (e) {
					done(e);
				}
			}, 0);
		});
	});

	describe("lifecycle safety", function () {
		afterEach(() => {
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = "";
		});

		it("should not throw when auto-hide fires after disconnect", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			const failures = [];
			const onError = (event) => {
				failures.push(event?.error || event);
			};
			window.addEventListener("error", onError);

			setTimeout(() => {
				try {
					button.setMessage("Saved").showMessage(10);
					mocks.removeChild(button);

					setTimeout(() => {
						window.removeEventListener("error", onError);
						try {
							expect(failures).to.have.length(0);
							done();
						} catch (e) {
							done(e);
						}
					}, 30);
				} catch (e) {
					window.removeEventListener("error", onError);
					done(e);
				}
			}, 0);
		});
	});

	describe("message content rendering", function () {
		afterEach(() => {
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = "";
		});

		it("should mount HTMLElement message content without reparsing rich content", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			const wrapper = document.createElement("div");
			const strong = document.createElement("strong");
			const inlineControl = document.createElement("button");
			strong.textContent = "Saved";
			inlineControl.textContent = "Undo";
			wrapper.appendChild(strong);
			wrapper.appendChild(inlineControl);

			setTimeout(() => {
				try {
					button.setMessage(wrapper).showMessage();

					setTimeout(() => {
						try {
							const message = button.shadowRoot.querySelector(
								'[data-monster-role="message"]',
							);
							expect(message.firstElementChild).to.equal(wrapper);
							expect(wrapper.firstElementChild).to.equal(strong);
							expect(wrapper.lastElementChild).to.equal(inlineControl);
							expect(inlineControl.isConnected).to.equal(true);
							done();
						} catch (e) {
							done(e);
						}
					}, 0);
				} catch (e) {
					done(e);
				}
			}, 0);
		});
	});

	describe("popper content presentation", function () {
		afterEach(() => {
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = "";
		});

		it("should resolve plain prose content to default popper clipping", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			setTimeout(() => {
				try {
					button.setMessage(
						"<div><strong>Saved</strong><p>plain html</p></div>",
					);
					button.showMessage();

					setTimeout(() => {
						try {
							const content =
								button.shadowRoot.querySelector('[part="content"]');
							const message = button.shadowRoot.querySelector(
								'[data-monster-role="message"]',
							);
							expect(content).to.exist;
							expect(
								content.getAttribute("data-monster-overflow-mode"),
							).to.equal("both");
							expect(
								content.getAttribute("data-monster-message-layout"),
							).to.equal("prose");
							expect(
								message.getAttribute("data-monster-message-layout"),
							).to.equal("prose");
							done();
						} catch (e) {
							done(e);
						}
					}, 0);
				} catch (e) {
					done(e);
				}
			}, 0);
		});

		it("should recalculate the open popper after checkbox-driven form growth", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			const form = document.createElement("form");
			const label = document.createElement("label");
			const checkbox = document.createElement("input");
			checkbox.type = "checkbox";
			label.appendChild(checkbox);
			label.append(" show more");
			form.appendChild(label);

			setTimeout(() => {
				try {
					button.setMessage(form);
					button.showMessage();

					setTimeout(() => {
						try {
							const popper = button.shadowRoot.querySelector(
								'[data-monster-role="popper"]',
							);
							expect(popper).to.exist;

							const originalHook = popper.monsterBeforeFloatingUpdate;
							let updateCount = 0;
							popper.monsterBeforeFloatingUpdate = () => {
								updateCount += 1;
								if (typeof originalHook === "function") {
									originalHook();
								}
							};

							checkbox.checked = true;
							button.recalcMessage();

							setTimeout(() => {
								try {
									expect(updateCount).to.be.at.least(1);
									done();
								} catch (e) {
									done(e);
								}
							}, 20);
						} catch (e) {
							done(e);
						}
					}, 0);
				} catch (e) {
					done(e);
				}
			}, 0);
		});

		it("should resolve nested select message content to horizontal clipping only", async function () {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			const wrapper = document.createElement("div");
			wrapper.appendChild(document.createElement("monster-select"));
			button.setMessage(wrapper);
			button.showMessage();

			await waitForCondition(() => {
				const content = button.shadowRoot?.querySelector('[part="content"]');
				return (
					content?.getAttribute("data-monster-overflow-mode") === "horizontal"
				);
			});

			const content = button.shadowRoot.querySelector('[part="content"]');
			const message = button.shadowRoot.querySelector(
				'[data-monster-role="message"]',
			);

			expect(content).to.exist;
			expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
				"horizontal",
			);
			expect(content.getAttribute("data-monster-message-layout")).to.equal(
				"overlay",
			);
			expect(message.getAttribute("data-monster-message-layout")).to.equal(
				"overlay",
			);
		});

		it("should resolve wide plain content to the wide layout", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			setTimeout(() => {
				try {
					const wrapper = document.createElement("div");
					const line = document.createElement("div");
					line.setAttribute("style", "white-space: nowrap; overflow-x: auto;");
					line.textContent =
						"this is intentionally a single long line to trigger wide layout";
					wrapper.appendChild(line);

					button.setMessage(wrapper);
					button.showMessage();

					setTimeout(() => {
						try {
							const content =
								button.shadowRoot.querySelector('[part="content"]');
							const message = button.shadowRoot.querySelector(
								'[data-monster-role="message"]',
							);
							expect(content).to.exist;
							expect(
								content.getAttribute("data-monster-overflow-mode"),
							).to.equal("both");
							expect(
								content.getAttribute("data-monster-message-layout"),
							).to.equal("wide");
							expect(
								message.getAttribute("data-monster-message-layout"),
							).to.equal("wide");
							done();
						} catch (e) {
							done(e);
						}
					}, 0);
				} catch (e) {
					done(e);
				}
			}, 0);
		});
	});

	describe("message width behavior", function () {
		let originalInnerWidth;
		let originalGetBoundingClientRect;

		beforeEach(() => {
			originalInnerWidth = window.innerWidth;
			originalGetBoundingClientRect =
				HTMLElement.prototype.getBoundingClientRect;
		});

		afterEach(() => {
			Object.defineProperty(window, "innerWidth", {
				configurable: true,
				writable: true,
				value: originalInnerWidth,
			});
			HTMLElement.prototype.getBoundingClientRect =
				originalGetBoundingClientRect;
			let mocks = document.getElementById("mocks");
			mocks.innerHTML = "";
		});

		it("should keep prose content on a readable max width", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			Object.defineProperty(window, "innerWidth", {
				configurable: true,
				writable: true,
				value: 800,
			});
			HTMLElement.prototype.getBoundingClientRect = function () {
				if (this?.getAttribute?.("data-measurement") === "true") {
					return {
						width: 900,
						height: 120,
						top: 0,
						left: 0,
						right: 900,
						bottom: 120,
					};
				}

				return originalGetBoundingClientRect.call(this);
			};

			setTimeout(() => {
				try {
					button.setMessage(
						"<div><p>This long prose content should wrap instead of forcing the message popper to use the full viewport width.</p></div>",
					);
					button.showMessage();

					const popper = button.shadowRoot.querySelector(
						'[data-monster-role="popper"]',
					);
					expect(popper.style.width).to.equal("512px");
					expect(popper.style.maxWidth).to.equal("512px");
					done();
				} catch (e) {
					done(e);
				}
			}, 0);
		});

		it("should allow wide content to grow until the viewport limit", function (done) {
			let mocks = document.getElementById("mocks");
			const button = document.createElement("monster-message-state-button");
			button.innerHTML = "Save";
			mocks.appendChild(button);

			Object.defineProperty(window, "innerWidth", {
				configurable: true,
				writable: true,
				value: 800,
			});
			HTMLElement.prototype.getBoundingClientRect = function () {
				if (this?.getAttribute?.("data-measurement") === "true") {
					return {
						width: 900,
						height: 120,
						top: 0,
						left: 0,
						right: 900,
						bottom: 120,
					};
				}

				return originalGetBoundingClientRect.call(this);
			};

			setTimeout(() => {
				try {
					const wrapper = document.createElement("div");
					wrapper.setAttribute("data-monster-message-layout", "wide");
					wrapper.textContent =
						"wide content placeholder that should grow until the viewport edge";
					button.setMessage(wrapper);
					button.showMessage();

					const popper = button.shadowRoot.querySelector(
						'[data-monster-role="popper"]',
					);
					expect(popper.style.width).to.equal("768px");
					expect(popper.style.maxWidth).to.equal("768px");
					done();
				} catch (e) {
					done(e);
				}
			}, 0);
		});
	});
});

function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) {
	return new Promise((resolve, reject) => {
		const start = Date.now();

		const poll = () => {
			try {
				if (check()) {
					resolve();
					return;
				}
			} catch (error) {
				reject(error);
				return;
			}

			if (Date.now() - start >= timeout) {
				reject(new Error("Timed out while waiting for test condition."));
				return;
			}

			setTimeout(poll, interval);
		};

		poll();
	});
}
