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";

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

function dispatchPointerEvent(target, type, options = {}) {
	const event = new Event(type, {
		bubbles: true,
		cancelable: true,
		composed: true,
	});

	const values = {
		button: 0,
		isPrimary: true,
		pointerId: 1,
		clientX: 0,
		clientY: 0,
		...options,
	};

	for (const [key, value] of Object.entries(values)) {
		Object.defineProperty(event, key, {
			configurable: true,
			value,
		});
	}

	target.dispatchEvent(event);
	return event;
}

function mockScrollableElement(element, { clientWidth, scrollWidth, scrollLeft = 0 }) {
	let currentScrollLeft = scrollLeft;

	Object.defineProperty(element, "clientWidth", {
		configurable: true,
		value: clientWidth,
	});
	Object.defineProperty(element, "scrollWidth", {
		configurable: true,
		value: scrollWidth,
	});
	Object.defineProperty(element, "scrollLeft", {
		configurable: true,
		get: () => currentScrollLeft,
		set: (value) => {
			currentScrollLeft = value;
		},
	});
}

describe("Datatable drag scroll", function () {
	before(async function () {
		await initJSDOM();
		await import("element-internals-polyfill").catch(() => {});
		await import("../../../../source/components/datatable/datatable.mjs");
	});

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

	it("drags horizontally on non-interactive cells and suppresses the follow-up click", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "drag-scroll-table";
		datatable.innerHTML = `
			<template id="drag-scroll-table-row">
				<div data-monster-head="Name">Alpha</div>
				<div data-monster-head="Value">Beta</div>
			</template>
		`;
		datatable.setOption("data", [{}, {}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const scroll = datatable.shadowRoot.querySelector(
			"[data-monster-role=table-scroll]",
		);
		const cell = datatable.shadowRoot.querySelector(
			"[data-monster-role=datatable] > div",
		);

		expect(scroll).to.exist;
		expect(cell).to.exist;

		mockScrollableElement(scroll, {
			clientWidth: 200,
			scrollWidth: 600,
		});

		scroll.scrollLeft = 120;

		let clickCount = 0;
		cell.addEventListener("click", () => {
			clickCount += 1;
		});

		dispatchPointerEvent(cell, "pointerdown", {
			clientX: 100,
			clientY: 20,
		});
		dispatchPointerEvent(window, "pointermove", {
			clientX: 40,
			clientY: 24,
		});
		dispatchPointerEvent(window, "pointerup", {
			clientX: 40,
			clientY: 24,
		});

		expect(scroll.scrollLeft).to.equal(180);
		expect(scroll.classList.contains("is-dragging")).to.equal(false);

		cell.dispatchEvent(
			new window.MouseEvent("click", {
				bubbles: true,
				cancelable: true,
				composed: true,
			}),
		);

		expect(clickCount).to.equal(0);
	});

	it("does not break clicks on embedded controls", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "drag-scroll-button-table";
		datatable.innerHTML = `
			<template id="drag-scroll-button-table-row">
				<div data-monster-head="Name">Alpha</div>
				<div data-monster-head="Action"><button type="button">Open</button></div>
			</template>
		`;
		datatable.setOption("data", [{}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const scroll = datatable.shadowRoot.querySelector(
			"[data-monster-role=table-scroll]",
		);
		const button = datatable.shadowRoot.querySelector("button");

		expect(scroll).to.exist;
		expect(button).to.exist;

		mockScrollableElement(scroll, {
			clientWidth: 200,
			scrollWidth: 600,
		});

		scroll.scrollLeft = 120;

		let clickCount = 0;
		button.addEventListener("click", () => {
			clickCount += 1;
		});

		dispatchPointerEvent(button, "pointerdown", {
			clientX: 100,
			clientY: 20,
		});
		dispatchPointerEvent(window, "pointermove", {
			clientX: 30,
			clientY: 22,
		});
		dispatchPointerEvent(window, "pointerup", {
			clientX: 30,
			clientY: 22,
		});

		button.dispatchEvent(
			new window.MouseEvent("click", {
				bubbles: true,
				cancelable: true,
				composed: true,
			}),
		);

		expect(scroll.scrollLeft).to.equal(120);
		expect(clickCount).to.equal(1);
	});

	it("does not start drag scroll from header interactions", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "drag-scroll-header-table";
		datatable.innerHTML = `
			<template id="drag-scroll-header-table-row">
				<div data-monster-head="Name">Alpha</div>
				<div data-monster-head="Value">Beta</div>
			</template>
		`;
		datatable.setOption("data", [{}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const scroll = datatable.shadowRoot.querySelector(
			"[data-monster-role=table-scroll]",
		);
		const headerCell = datatable.shadowRoot.querySelector(
			"[data-monster-role=datatable-headers] > div",
		);

		expect(scroll).to.exist;
		expect(headerCell).to.exist;

		mockScrollableElement(scroll, {
			clientWidth: 200,
			scrollWidth: 600,
		});

		scroll.scrollLeft = 120;

		dispatchPointerEvent(headerCell, "pointerdown", {
			clientX: 100,
			clientY: 20,
		});
		dispatchPointerEvent(window, "pointermove", {
			clientX: 40,
			clientY: 24,
		});
		dispatchPointerEvent(window, "pointerup", {
			clientX: 40,
			clientY: 24,
		});

		expect(scroll.scrollLeft).to.equal(120);
		expect(scroll.classList.contains("is-dragging")).to.equal(false);
	});

	it("keeps double click copy for a single cell", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "copy-cell-table";
		datatable.innerHTML = `
			<template id="copy-cell-table-row">
				<div data-monster-head="Name">Alpha</div>
				<div data-monster-head="Value">Beta</div>
			</template>
		`;
		datatable.setOption("data", [{}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const copied = [];
		Object.defineProperty(window.navigator, "clipboard", {
			configurable: true,
			value: {
				writeText: async (text) => {
					copied.push(text);
				},
			},
		});

		const cell = datatable.shadowRoot.querySelector(
			"[data-monster-role=datatable] > div",
		);
		cell.dispatchEvent(
			new window.MouseEvent("dblclick", {
				bubbles: true,
				cancelable: true,
				composed: true,
			}),
		);

		await new Promise((resolve) => setTimeout(resolve, 0));
		expect(copied).deep.equal(["Alpha"]);
	});

	it("copies the whole row on shift click", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "copy-row-table";
		datatable.innerHTML = `
			<template id="copy-row-table-row">
				<div data-monster-head="Name">Alpha</div>
				<div data-monster-head="Value">Beta</div>
			</template>
		`;
		datatable.setOption("data", [{}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const copied = [];
		Object.defineProperty(window.navigator, "clipboard", {
			configurable: true,
			value: {
				writeText: async (text) => {
					copied.push(text);
				},
			},
		});

		const cell = datatable.shadowRoot.querySelector(
			"[data-monster-role=datatable] > div",
		);
		cell.dispatchEvent(
			new window.MouseEvent("click", {
				bubbles: true,
				cancelable: true,
				composed: true,
				shiftKey: true,
			}),
		);

		await new Promise((resolve) => setTimeout(resolve, 0));
		expect(copied).deep.equal(['"Alpha";"Beta"']);
	});

	it("adds an error attribute when clipboard copy fails", async function () {
		const mocks = document.getElementById("mocks");
		const datatable = document.createElement("monster-datatable");
		datatable.id = "copy-error-table";
		datatable.innerHTML = `
			<template id="copy-error-table-row">
				<div data-monster-head="Name">Alpha</div>
			</template>
		`;
		datatable.setOption("data", [{}]);
		mocks.appendChild(datatable);

		await new Promise((resolve) => setTimeout(resolve, 30));

		const error = new DOMException("Document is not focused.", "NotAllowedError");
		Object.defineProperty(window.navigator, "clipboard", {
			configurable: true,
			value: {
				writeText: async () => {
					throw error;
				},
			},
		});

		let eventDetail = null;
		datatable.addEventListener("monster-datatable-copy-error", (event) => {
			eventDetail = event.detail;
		});

		const cell = datatable.shadowRoot.querySelector(
			"[data-monster-role=datatable] > div",
		);
		cell.dispatchEvent(
			new window.MouseEvent("dblclick", {
				bubbles: true,
				cancelable: true,
				composed: true,
			}),
		);

		await new Promise((resolve) => setTimeout(resolve, 0));

		expect(datatable.getAttribute("data-monster-error")).to.contain(
			"Document is not focused.",
		);
		expect(eventDetail.error).to.equal(error);
		expect(eventDetail.text).to.equal("Alpha");
	});

	it("defers resize observer grid updates to the next animation frame", async function () {
		const OriginalResizeObserver = window.ResizeObserver;
		const originalGlobalResizeObserver = globalThis.ResizeObserver;
		const originalRequestAnimationFrame = window.requestAnimationFrame;
		const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;

		class TrackingResizeObserver extends ResizeObserverMock {
			static instances = [];

			constructor(callback) {
				super(callback);
				TrackingResizeObserver.instances.push(this);
			}
		}

		try {
			window.ResizeObserver = TrackingResizeObserver;
			globalThis.ResizeObserver = TrackingResizeObserver;

			const mocks = document.getElementById("mocks");
			const wrapper = document.createElement("div");
			let wrapperWidth = 500;

			Object.defineProperty(wrapper, "clientWidth", {
				configurable: true,
				get: () => wrapperWidth,
			});

			mocks.appendChild(wrapper);

			const datatable = document.createElement("monster-datatable");
			datatable.id = "resize-scheduled-table";
			datatable.innerHTML = `
				<template id="resize-scheduled-table-row">
					<div data-monster-head="Name">Alpha</div>
					<div data-monster-head="Value">Beta</div>
				</template>
			`;
			datatable.setOption("responsive.breakpoint", 300);
			datatable.setOption("data", [{}]);
			wrapper.appendChild(datatable);

			await new Promise((resolve) => setTimeout(resolve, 30));

			const control = datatable.shadowRoot.querySelector(
				"[data-monster-role=control]",
			);
			expect(control).to.exist;
			expect(control.classList.contains("small")).to.equal(false);
			expect(TrackingResizeObserver.instances).to.have.length.greaterThan(0);
			const tableResizeObserver = TrackingResizeObserver.instances.find(
				(observer) => observer.elements.includes(wrapper),
			);
			expect(tableResizeObserver).to.exist;

			let scheduledCallback = null;
			window.requestAnimationFrame = (callback) => {
				scheduledCallback = callback;
				return 1;
			};
			globalThis.requestAnimationFrame = window.requestAnimationFrame;

			wrapperWidth = 200;
			tableResizeObserver.triggerResize([]);

			expect(scheduledCallback).to.be.a("function");
			expect(control.classList.contains("small")).to.equal(false);

			scheduledCallback();

			expect(control.classList.contains("small")).to.equal(true);
		} finally {
			window.ResizeObserver = OriginalResizeObserver;
			globalThis.ResizeObserver = originalGlobalResizeObserver;
			window.requestAnimationFrame = originalRequestAnimationFrame;
			globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
		}
	});

	it("defers column bar resize updates to the next animation frame", async function () {
		const OriginalResizeObserver = window.ResizeObserver;
		const originalGlobalResizeObserver = globalThis.ResizeObserver;
		const originalRequestAnimationFrame = window.requestAnimationFrame;
		const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;

		class TrackingResizeObserver extends ResizeObserverMock {
			static instances = [];

			constructor(callback) {
				super(callback);
				TrackingResizeObserver.instances.push(this);
			}
		}

		try {
			window.ResizeObserver = TrackingResizeObserver;
			globalThis.ResizeObserver = TrackingResizeObserver;

			const mocks = document.getElementById("mocks");
			const wrapper = document.createElement("div");
			let parentWidth = 400;

			Object.defineProperty(wrapper, "getBoundingClientRect", {
				configurable: true,
				value: () => ({ width: parentWidth }),
			});

			mocks.appendChild(wrapper);

			const columnBar = document.createElement("monster-column-bar");
			columnBar.setOption("columns", [
				{ index: 0, name: "One", visible: true },
				{ index: 1, name: "Two", visible: true },
				{ index: 2, name: "Three", visible: true },
				{ index: 3, name: "Four", visible: true },
				{ index: 4, name: "Five", visible: true },
				{ index: 5, name: "Six", visible: true },
			]);
			wrapper.appendChild(columnBar);

			await new Promise((resolve) => setTimeout(resolve, 30));

			const control = columnBar.shadowRoot.querySelector(
				"[data-monster-role=control]",
			);
			const settingsButton = columnBar.shadowRoot.querySelector(
				"[data-monster-role=settings-button]",
			);
			const dotsContainer = columnBar.shadowRoot.querySelector(
				"[data-monster-role=dots]",
			);
			const dots = Array.from(dotsContainer.querySelectorAll("li"));

			expect(control).to.exist;
			expect(settingsButton).to.exist;
			expect(dots.length).to.equal(6);

			Object.defineProperty(control, "getBoundingClientRect", {
				configurable: true,
				value: () => ({ width: parentWidth }),
			});
			Object.defineProperty(settingsButton, "getBoundingClientRect", {
				configurable: true,
				value: () => ({ width: 20 }),
			});
			dots.forEach((dot) => {
				Object.defineProperty(dot, "getBoundingClientRect", {
					configurable: true,
					value: () => ({ width: 20 }),
				});
			});

			await new Promise((resolve) => setTimeout(resolve, 0));

			expect(
				dotsContainer.querySelector(".dots-overflow-indicator"),
			).to.equal(null);

			const columnBarResizeObserver = TrackingResizeObserver.instances.find(
				(observer) => observer.elements.includes(control),
			);
			expect(columnBarResizeObserver).to.exist;

			let scheduledCallback = null;
			window.requestAnimationFrame = (callback) => {
				scheduledCallback = callback;
				return 1;
			};
			globalThis.requestAnimationFrame = window.requestAnimationFrame;

			parentWidth = 80;
			columnBarResizeObserver.triggerResize([]);

			expect(scheduledCallback).to.be.a("function");
			expect(
				dotsContainer.querySelector(".dots-overflow-indicator"),
			).to.equal(null);

			scheduledCallback();

			expect(
				dotsContainer.querySelector(".dots-overflow-indicator"),
			).to.exist;
		} finally {
			window.ResizeObserver = OriginalResizeObserver;
			globalThis.ResizeObserver = originalGlobalResizeObserver;
			window.requestAnimationFrame = originalRequestAnimationFrame;
			globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
		}
	});

	it("coalesces column bar dot updates and ignores overflow indicator mutations", async function () {
		const OriginalResizeObserver = window.ResizeObserver;
		const originalGlobalResizeObserver = globalThis.ResizeObserver;
		const originalRequestAnimationFrame = window.requestAnimationFrame;
		const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;

		class TrackingResizeObserver extends ResizeObserverMock {
			static instances = [];

			constructor(callback) {
				super(callback);
				TrackingResizeObserver.instances.push(this);
			}
		}

		try {
			window.ResizeObserver = TrackingResizeObserver;
			globalThis.ResizeObserver = TrackingResizeObserver;

			const mocks = document.getElementById("mocks");
			const wrapper = document.createElement("div");
			let parentWidth = 80;

			Object.defineProperty(wrapper, "getBoundingClientRect", {
				configurable: true,
				value: () => ({ width: parentWidth }),
			});

			mocks.appendChild(wrapper);

			const createColumnBar = async () => {
				const columnBar = document.createElement("monster-column-bar");
				columnBar.setOption("columns", [
					{ index: 0, name: "One", visible: true },
					{ index: 1, name: "Two", visible: true },
					{ index: 2, name: "Three", visible: true },
					{ index: 3, name: "Four", visible: true },
					{ index: 4, name: "Five", visible: true },
					{ index: 5, name: "Six", visible: true },
				]);
				wrapper.appendChild(columnBar);

				await new Promise((resolve) => setTimeout(resolve, 30));

				const control = columnBar.shadowRoot.querySelector(
					"[data-monster-role=control]",
				);
				const settingsButton = columnBar.shadowRoot.querySelector(
					"[data-monster-role=settings-button]",
				);
				const dotsContainer = columnBar.shadowRoot.querySelector(
					"[data-monster-role=dots]",
				);
				const dots = Array.from(dotsContainer.querySelectorAll("li"));

				Object.defineProperty(control, "getBoundingClientRect", {
					configurable: true,
					value: () => ({ width: parentWidth }),
				});
				Object.defineProperty(settingsButton, "getBoundingClientRect", {
					configurable: true,
					value: () => ({ width: 20 }),
				});
				dots.forEach((dot) => {
					Object.defineProperty(dot, "getBoundingClientRect", {
						configurable: true,
						value: () => ({ width: 20 }),
					});
				});

				return { columnBar, control, dotsContainer };
			};

			const first = await createColumnBar();
			const second = await createColumnBar();

			const firstResizeObserver = TrackingResizeObserver.instances.find(
				(observer) => observer.elements.includes(first.control),
			);
			const secondResizeObserver = TrackingResizeObserver.instances.find(
				(observer) => observer.elements.includes(second.control),
			);

			expect(firstResizeObserver).to.exist;
			expect(secondResizeObserver).to.exist;

			const scheduledCallbacks = [];
			window.requestAnimationFrame = (callback) => {
				scheduledCallbacks.push(callback);
				return scheduledCallbacks.length;
			};
			globalThis.requestAnimationFrame = window.requestAnimationFrame;

			firstResizeObserver.triggerResize([]);
			secondResizeObserver.triggerResize([]);

			expect(scheduledCallbacks.length).to.equal(1);

			scheduledCallbacks.shift()();

			expect(
				first.dotsContainer.querySelector(".dots-overflow-indicator"),
			).to.exist;
			expect(
				second.dotsContainer.querySelector(".dots-overflow-indicator"),
			).to.exist;

			await new Promise((resolve) => setTimeout(resolve, 0));

			expect(scheduledCallbacks.length).to.equal(0);
		} finally {
			window.ResizeObserver = OriginalResizeObserver;
			globalThis.ResizeObserver = originalGlobalResizeObserver;
			window.requestAnimationFrame = originalRequestAnimationFrame;
			globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
		}
	});
});
