<script module lang="ts">
	import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf";
	import { expect, waitFor } from "storybook/test";
	import { LineOp, type DiffFile } from "$lib/api/diff";
	import { toFileDiffMetadata } from "./fileDiff";
	import PierreDiff from "./PierreDiff.svelte";

	const context = (line: string) => ({ Op: LineOp.Context, Line: `${line}\n` });
	const deletion = (line: string) => ({ Op: LineOp.Delete, Line: `${line}\n` });
	const addition = (line: string) => ({ Op: LineOp.Add, Line: `${line}\n` });

	const files: DiffFile[] = [
		{
			name: { old: "src/colors.ts", new: "src/colors.ts" },
			text_fragments: [
				{
					OldPosition: 1,
					OldLines: 4,
					NewPosition: 1,
					NewLines: 6,
					Lines: [
						context("export const palette = {"),
						deletion('\tbackground: "white",'),
						deletion('\tforeground: "black",'),
						addition('\tbackground: "oklch(0.98 0 0)",'),
						addition('\tforeground: "oklch(0.2 0 0)",'),
						context("};"),
						addition(""),
						addition('export const accent = "oklch(0.6 0.2 250)";')
					]
				},
				{
					OldPosition: 10,
					OldLines: 3,
					NewPosition: 11,
					NewLines: 3,
					Lines: [
						context("export const spacing = 4;"),
						deletion("export const radius = 0;"),
						addition("export const radius = 8;"),
						context('export const shadow = "none";')
					]
				}
			]
		},
		{
			name: { old: "src/old.ts", new: "src/renamed.ts" },
			is_rename: true,
			text_fragments: [
				{
					OldPosition: 1,
					OldLines: 2,
					NewPosition: 1,
					NewLines: 2,
					Lines: [
						deletion('export const name = "old";'),
						addition('export const name = "renamed";'),
						context("export default name;")
					]
				}
			]
		}
	];

	// the hunk starts deep in the file, the leading unmodified region collapses into a
	// top separator
	const offsetFiles: DiffFile[] = [
		{
			name: { old: "src/deep.ts", new: "src/deep.ts" },
			text_fragments: [
				{
					OldPosition: 20,
					OldLines: 3,
					NewPosition: 20,
					NewLines: 3,
					Lines: [
						context("export const spacing = 4;"),
						deletion("export const radius = 0;"),
						addition("export const radius = 8;"),
						context('export const shadow = "none";')
					]
				}
			]
		}
	];

	type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvasElement">;

	const hostsOf = (canvasElement: HTMLElement) => [
		...canvasElement.querySelectorAll("diffs-container")
	];

	// pierre renders into shadow roots, testing library cannot see through
	const shadowText = (canvasElement: HTMLElement) =>
		hostsOf(canvasElement)
			.map((host) => host.shadowRoot?.textContent ?? "")
			.join("\n");

	const rendersPatch = async (canvasElement: HTMLElement) => {
		const hosts = hostsOf(canvasElement);
		await expect(hosts.length).toBe(2);
		await waitFor(() => expect(shadowText(canvasElement)).toContain("export const palette"));
		await waitFor(() => expect(shadowText(canvasElement)).toContain("export const name"));
		return hosts;
	};

	const appviewChrome = async ({ canvasElement }: PlayContext) => {
		const hosts = await rendersPatch(canvasElement);
		for (const host of hosts) {
			expect(host.shadowRoot?.querySelector("[data-diffs-header]")).toBeNull();
			expect(host.shadowRoot?.querySelector("[data-indicators='classic']")).not.toBeNull();
		}
		// the hunks cover old lines 1-5 and 10-12, 4 collapse between
		expect(hosts[0].shadowRoot?.querySelector("[data-separator='line-info']")).not.toBeNull();
		await waitFor(() => expect(shadowText(canvasElement)).toContain("4 unmodified lines"));
	};

	const expectSplitColumns = (present: boolean) => {
		return async ({ canvasElement }: PlayContext) => {
			const hosts = await rendersPatch(canvasElement);
			await waitFor(() => {
				const found = hosts.some(
					(host) =>
						host.shadowRoot?.querySelector("[data-deletions]") &&
						host.shadowRoot?.querySelector("[data-additions]")
				);
				expect(found).toBe(present);
			});
		};
	};

	// the separator bar must span the diff instead of clipping in the 44px
	// gutter. the additions-side copy hides its text, only measure
	// non-zero spans
	const separatorNotClipped = async ({ canvasElement }: PlayContext) => {
		const hosts = await rendersPatch(canvasElement);
		await waitFor(() => {
			const spans = hosts.flatMap((host) => [
				...(host.shadowRoot?.querySelectorAll("[data-unmodified-lines]") ?? [])
			]);
			const visible = spans.filter((span) => span.getBoundingClientRect().width > 0);
			expect(visible.length).toBeGreaterThan(0);
			for (const span of visible) {
				const wrapper = span.closest("[data-separator-wrapper]");
				expect(wrapper).not.toBeNull();
				const wrapperRect = wrapper!.getBoundingClientRect();
				const spanRect = span.getBoundingClientRect();
				expect(wrapperRect.width).toBeGreaterThan(200);
				expect(spanRect.x).toBeGreaterThanOrEqual(wrapperRect.x - 1);
				expect(spanRect.right).toBeLessThanOrEqual(wrapperRect.right + 1);
			}
		});
	};

	const { Story } = defineMeta({
		title: "Repo/PierreDiff",
		component: PierreDiff,
		tags: ["autodocs"],
		args: { files: files.map(toFileDiffMetadata) }
	});
</script>

<Story
	name="Unified"
	play={async (ctx) => {
		await appviewChrome(ctx);
		await expectSplitColumns(false)(ctx);
		await separatorNotClipped(ctx);
	}}
/>

<Story
	name="Split"
	args={{ diffStyle: "split" }}
	play={async (ctx) => {
		await expectSplitColumns(true)(ctx);
		await separatorNotClipped(ctx);
	}}
/>

<Story
	name="Leading collapse"
	args={{ files: offsetFiles.map(toFileDiffMetadata) }}
	play={async ({ canvasElement }: PlayContext) => {
		await waitFor(() => expect(shadowText(canvasElement)).toContain("19 unmodified lines"));
		const host = hostsOf(canvasElement)[0];
		const separator = host.shadowRoot?.querySelector("[data-separator='line-info']");
		expect(separator).not.toBeNull();
		expect(separator?.hasAttribute("data-separator-first")).toBe(true);
	}}
/>
