import type { HeadingNode } from "@ax/types";
import {
	analyzeHeadings,
	extractUniqueHeadingTypes,
	filterHeadings,
	getHeadColor,
	hasHiddenHeadings,
} from "@ax/components/HeadingsPreviewModal/utils";

const flatNode = (
	tag: HeadingNode["tag"],
	text: string,
	level: HeadingNode["level"],
	children: HeadingNode[] = [],
	isHidden = false,
): HeadingNode => ({ tag, text, level, isHidden, children });

describe("analyzeHeadings", () => {
	it("should return no errors for empty headings", () => {
		expect(analyzeHeadings([])).toEqual([]);
	});

	it("should return no errors when H1 is present and structure is correct", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2)])];
		expect(analyzeHeadings(headings)).toHaveLength(0);
	});

	it("should report missing H1 when not filtering", () => {
		const headings = [flatNode("h2", "Subtitle", 2)];
		const errors = analyzeHeadings(headings, false);
		expect(errors.some((e) => e.message === "No H1 in this page")).toBe(true);
	});

	it("should not report missing H1 when filtering is active", () => {
		const headings = [flatNode("h2", "Subtitle", 2)];
		const errors = analyzeHeadings(headings, true);
		expect(errors.some((e) => e.message === "No H1 in this page")).toBe(false);
	});

	it("should report nesting error when heading levels are skipped", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h3", "Sub-subtitle", 3)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Check heading structure for SEO compliance")).toBe(true);
	});

	it("should include the ID of the incorrectly nested heading", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h3", "Sub-subtitle", 3)];
		const errors = analyzeHeadings(headings);
		const nestingError = errors.find((e) => e.message === "Check heading structure for SEO compliance");
		expect(nestingError?.headingIds.length).toBeGreaterThan(0);
	});

	it("should not report nesting error for sequential heading levels", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h2", "Subtitle", 2)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Check heading structure for SEO compliance")).toBe(false);
	});

	it("should report duplicate heading texts", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h2", "Same text", 2), flatNode("h2", "Same text", 2)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Avoid header repetitions for SEO")).toBe(true);
	});

	it("should not report duplicates when all headings are unique", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h2", "Subtitle A", 2), flatNode("h2", "Subtitle B", 2)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Avoid header repetitions for SEO")).toBe(false);
	});

	it("should report excessive heading length when text exceeds 70 characters", () => {
		const longText = "A".repeat(71);
		const headings = [flatNode("h1", longText, 1)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Some headings are too long")).toBe(true);
	});

	it("should not report excessive length for headings within 70 characters", () => {
		const headings = [flatNode("h1", "A".repeat(70), 1)];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Some headings are too long")).toBe(false);
	});

	it("should detect errors in nested children", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Same", 2), flatNode("h2", "Same", 2)])];
		const errors = analyzeHeadings(headings);
		expect(errors.some((e) => e.message === "Avoid header repetitions for SEO")).toBe(true);
	});
});

describe("filterHeadings", () => {
	const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle A", 2), flatNode("h2", "Subtitle B", 2)])];

	it("should return all headings when filter is 'all'", () => {
		const result = filterHeadings(headings, "all");
		expect(result).toEqual(headings);
	});

	it("should return only h1 headings when filter is 'h1'", () => {
		const result = filterHeadings(headings, "h1");
		expect(result).toHaveLength(1);
		expect(result[0].tag).toBe("h1");
	});

	it("should flatten and return only h2 headings when filter is 'h2'", () => {
		const result = filterHeadings(headings, "h2");
		expect(result).toHaveLength(2);
		expect(result.every((h) => h.tag === "h2")).toBe(true);
		expect(result.every((h) => h.children.length === 0)).toBe(true);
	});

	it("should return empty array when no headings match the filter", () => {
		const result = filterHeadings(headings, "h3");
		expect(result).toHaveLength(0);
	});

	describe("with includeHidden parameter", () => {
		it("should include hidden headings by default", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [flatNode("h2", "Visible", 2), flatNode("h2", "Hidden", 2, [], true)]),
			];
			const result = filterHeadings(headingsWithHidden, "all");
			expect(result).toEqual(headingsWithHidden);
		});

		it("should explicitly include hidden headings when includeHidden is true", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [flatNode("h2", "Visible", 2), flatNode("h2", "Hidden", 2, [], true)]),
			];
			const result = filterHeadings(headingsWithHidden, "all", true);
			expect(result).toEqual(headingsWithHidden);
		});

		it("should filter out hidden headings when includeHidden is false with filter 'all'", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [flatNode("h2", "Visible A", 2), flatNode("h2", "Hidden", 2, [], true), flatNode("h2", "Visible B", 2)]),
			];
			const result = filterHeadings(headingsWithHidden, "all", false);
			expect(result).toHaveLength(1);
			expect(result[0].tag).toBe("h1");
			expect(result[0].children).toHaveLength(2);
			expect(result[0].children.every((h) => !h.isHidden)).toBe(true);
		});

		it("should filter out hidden headings when flattening with specific type", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [
					flatNode("h2", "Visible A", 2),
					flatNode("h2", "Hidden", 2, [], true),
					flatNode("h2", "Visible B", 2),
				]),
			];
			const result = filterHeadings(headingsWithHidden, "h2", false);
			expect(result).toHaveLength(2);
			expect(result.every((h) => h.tag === "h2")).toBe(true);
			expect(result.every((h) => !h.isHidden)).toBe(true);
		});

		it("should return empty array when all matching headings are hidden", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [flatNode("h2", "Hidden A", 2, [], true), flatNode("h2", "Hidden B", 2, [], true)]),
			];
			const result = filterHeadings(headingsWithHidden, "h2", false);
			expect(result).toHaveLength(0);
		});

		it("should maintain hierarchy when filtering hidden with type 'all'", () => {
			const headingsWithHidden = [
				flatNode("h1", "Visible", 1, [
					flatNode("h2", "Visible", 2, [flatNode("h3", "Hidden", 3, [], true)]),
					flatNode("h2", "Hidden", 2, [], true),
				]),
			];
			const result = filterHeadings(headingsWithHidden, "all", false);
			expect(result).toHaveLength(1);
			expect(result[0].tag).toBe("h1");
			expect(result[0].children).toHaveLength(1);
			expect(result[0].children[0].tag).toBe("h2");
			expect(result[0].children[0].children).toHaveLength(0);
		});

		it("should filter hidden headings for specific type that has both visible and hidden", () => {
			const headingsWithHidden = [
				flatNode("h1", "Title", 1, [], true),
				flatNode("h2", "Visible", 2),
				flatNode("h2", "Hidden", 2, [], true),
			];
			const result = filterHeadings(headingsWithHidden, "h2", false);
			expect(result).toHaveLength(1);
			expect(result[0].text).toBe("Visible");
		});
	});
});

describe("extractUniqueHeadingTypes", () => {
	it("should return empty array for empty headings", () => {
		expect(extractUniqueHeadingTypes([])).toEqual([]);
	});

	it("should return unique types sorted alphabetically", () => {
		const headings = [flatNode("h3", "Sub-subtitle", 3), flatNode("h1", "Title", 1)];
		expect(extractUniqueHeadingTypes(headings)).toEqual(["h1", "h3"]);
	});

	it("should include types from nested children", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2, [flatNode("h4", "Deep", 4)])])];
		expect(extractUniqueHeadingTypes(headings)).toEqual(["h1", "h2", "h4"]);
	});

	it("should deduplicate repeated types", () => {
		const headings = [flatNode("h2", "A", 2), flatNode("h2", "B", 2)];
		expect(extractUniqueHeadingTypes(headings)).toEqual(["h2"]);
	});
});

describe("getHeadColor", () => {
	it("should return an rgb color string for a known heading tag", () => {
		expect(getHeadColor("h1")).toBe("rgb(255, 240, 109)");
		expect(getHeadColor("h2")).toBe("rgb(255, 184, 248)");
	});

	it("should return an rgba color string when opacity is provided", () => {
		expect(getHeadColor("h1", 0.5)).toBe("rgba(255, 240, 109, 0.5)");
	});

	it("should return white for an unknown tag", () => {
		expect(getHeadColor("unknown")).toBe("rgb(255, 255, 255)");
	});
});

describe("hasHiddenHeadings", () => {
	it("should return false when there are no hidden headings", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2)])];
		expect(hasHiddenHeadings(headings)).toBe(false);
	});

	it("should return true when there are hidden headings with filter 'all'", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2, [], true)])];
		expect(hasHiddenHeadings(headings, "all")).toBe(true);
	});

	it("should return true when a direct child heading is hidden", () => {
		const headings = [flatNode("h1", "Title", 1, [], true)];
		expect(hasHiddenHeadings(headings)).toBe(true);
	});

	it("should return true when a nested heading is hidden and filter matches", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2, [], true)])];
		expect(hasHiddenHeadings(headings, "h2")).toBe(true);
	});

	it("should return false when hidden headings do not match the filter type", () => {
		const headings = [flatNode("h1", "Title", 1, [flatNode("h2", "Subtitle", 2, [], true)])];
		expect(hasHiddenHeadings(headings, "h1")).toBe(false);
	});

	it("should return true when filter type matches hidden heading", () => {
		const headings = [flatNode("h1", "Title", 1, [], true), flatNode("h2", "Subtitle", 2)];
		expect(hasHiddenHeadings(headings, "h1")).toBe(true);
	});

	it("should return false when no headings match the filter", () => {
		const headings = [flatNode("h1", "Title", 1), flatNode("h2", "Subtitle", 2)];
		expect(hasHiddenHeadings(headings, "h3")).toBe(false);
	});

	it("should return true when multiple levels have hidden headings of the same type", () => {
		const headings = [
			flatNode("h2", "Subtitle A", 2, [], true),
			flatNode("h2", "Subtitle B", 2, [flatNode("h3", "Nested", 3, [], true)]),
		];
		expect(hasHiddenHeadings(headings, "h2")).toBe(true);
	});

	it("should handle deeply nested hidden headings", () => {
		const headings = [
			flatNode("h1", "Title", 1, [
				flatNode("h2", "Subtitle", 2, [flatNode("h3", "Deep", 3, [flatNode("h4", "Deeper", 4, [], true)])]),
			]),
		];
		expect(hasHiddenHeadings(headings, "h4")).toBe(true);
	});

	it("should return false for empty headings array", () => {
		expect(hasHiddenHeadings([], "all")).toBe(false);
	});
});
