import { describe, expect, test } from "vitest";
import { processBlocksSync, processTokensSync } from "./marked";

function renderHtml(md: string): string {
	const tokens = processTokensSync(md, []);
	const textToken = tokens.find((token) => token.type === "text");
	if (!textToken || textToken.type !== "text") return "";
	return typeof textToken.html === "string" ? textToken.html : "";
}

// Full pipeline render (block splitting + optional streaming repairs), as used by MarkdownRenderer
function renderBlocksHtml(md: string, streaming: boolean): string {
	return processBlocksSync(md, [], streaming)
		.flatMap((block) => block.tokens)
		.map((token) => (token.type === "text" && typeof token.html === "string" ? token.html : ""))
		.join("");
}

describe("marked basic rendering", () => {
	test("renders bold text", () => {
		const html = renderHtml("**bold**");
		expect(html).toContain("<strong>bold</strong>");
	});

	test("renders links", () => {
		const html = renderHtml("[link](https://example.com)");
		expect(html).toContain('<a href="https://example.com"');
		expect(html).toContain("link</a>");
	});

	test("renders paragraphs", () => {
		const html = renderHtml("hello world");
		expect(html).toContain("<p>hello world</p>");
	});
});

describe("marked image renderer", () => {
	test("renders video extensions as <video>", () => {
		const html = renderHtml("![](https://example.com/clip.mp4)");
		expect(html).toContain("<video controls");
		expect(html).toContain('<source src="https://example.com/clip.mp4">');
	});

	test("renders audio extensions as <audio>", () => {
		const html = renderHtml("![](https://example.com/clip.mp3)");
		expect(html).toContain("<audio controls");
		expect(html).toContain('<source src="https://example.com/clip.mp3">');
	});

	test("renders non-video images as <img>", () => {
		const html = renderHtml("![](https://example.com/pic.png)");
		expect(html).toContain('<img src="https://example.com/pic.png"');
	});

	test("renders video with query params", () => {
		const html = renderHtml("![](https://example.com/clip.mp4?token=abc)");
		expect(html).toContain("<video controls");
		expect(html).toContain("clip.mp4?token=abc");
	});
});

describe("marked html video tag support", () => {
	test("allows raw <video> tags with controls", () => {
		const html = renderHtml('<video controls src="https://example.com/video.mp4"></video>');
		expect(html).toContain("<video");
		expect(html).toContain("controls");
		expect(html).toContain('src="https://example.com/video.mp4"');
	});

	test("allows <video> with nested <source> tags", () => {
		const html = renderHtml(
			'<video controls><source src="https://example.com/video.webm" type="video/webm"></video>'
		);
		expect(html).toContain("<video");
		expect(html).toContain("<source");
		expect(html).toContain('src="https://example.com/video.webm"');
	});

	test("strips disallowed attributes from video tags", () => {
		const html = renderHtml('<video onclick="alert(1)" src="https://example.com/v.mp4"></video>');
		expect(html).toContain("<video");
		expect(html).not.toContain("onclick");
	});

	test("strips javascript: URLs from media sources", () => {
		const html = renderHtml('<video controls src="javascript:alert(1)"></video>');
		expect(html).not.toContain("javascript:");
	});

	test("escapes disallowed html tags", () => {
		const html = renderHtml("<script>alert(1)</script>");
		expect(html).not.toContain("<script>");
		expect(html).toContain("&lt;script&gt;");
	});

	test("allows <audio> tags with controls", () => {
		const html = renderHtml(
			'<audio controls><source src="https://example.com/audio.mp3" type="audio/mpeg"></audio>'
		);
		expect(html).toContain("<audio");
		expect(html).toContain("<source");
		expect(html).toContain('type="audio/mpeg"');
	});
});

describe("streaming incomplete markdown", () => {
	test("incomplete link renders as inert anchor, not a dead clickable link", () => {
		const html = renderBlocksHtml("Check [the docs](https://exam", true);
		expect(html).toContain("<a data-incomplete-link>the docs</a>");
		expect(html).not.toContain("streamdown:incomplete-link");
		expect(html).not.toContain("target=");
	});

	test("incomplete link text renders as inert anchor", () => {
		const html = renderBlocksHtml("Check [the doc", true);
		expect(html).toContain("<a data-incomplete-link>the doc</a>");
	});

	test("complete links keep their href", () => {
		const html = renderBlocksHtml("Check [the docs](https://example.com)", true);
		expect(html).toContain('<a href="https://example.com" target="_blank" rel="noreferrer">');
		expect(html).not.toContain("data-incomplete-link");
	});

	test("incomplete bold renders as bold", () => {
		const html = renderBlocksHtml("Some **important tex", true);
		expect(html).toContain("<strong>important tex</strong>");
	});

	test("incomplete inline code renders as code", () => {
		const html = renderBlocksHtml("Run `npm insta", true);
		expect(html).toContain("<code>npm insta</code>");
	});

	test("partial trailing HTML tag does not flash as raw text", () => {
		const html = renderBlocksHtml("Some text <video contro", true);
		expect(html).toContain("Some text");
		expect(html).not.toContain("video contro");
	});

	test("lone dash while a list streams in does not flash previous text as heading", () => {
		const html = renderBlocksHtml("Shopping list:\n-", true);
		expect(html).not.toContain("<h2>");
		expect(html).toContain("Shopping list:");
	});

	test("single tilde ranges do not flash as strikethrough", () => {
		const html = renderBlocksHtml("Heat to 20~25°C", true);
		expect(html).not.toContain("<del>");
		expect(html).toContain("20~25°C");
	});

	test("comparison operator in list items does not render a blockquote", () => {
		const html = renderBlocksHtml("- > 25: expensive", true);
		expect(html).not.toContain("<blockquote>");
		expect(html).toContain("&gt; 25: expensive");
	});
});

describe("completed messages render unmodified markdown", () => {
	test("a trailing setext heading still renders as a heading", () => {
		// Regression: remend's setext flash guard must not rewrite completed content
		const html = renderBlocksHtml("Title\n-", false);
		expect(html).toContain("<h2>Title</h2>");
	});

	test("the same trailing setext heading is guarded while streaming", () => {
		const html = renderBlocksHtml("Title\n-", true);
		expect(html).not.toContain("<h2>");
	});

	test("incomplete markers in completed content render literally", () => {
		const html = renderBlocksHtml("Some **truncated outpu", false);
		expect(html).toContain("**truncated outpu");
		expect(html).not.toContain("<strong>");
	});

	test("incomplete links in completed content render literally", () => {
		const html = renderBlocksHtml("Check [the docs](https://exam", false);
		expect(html).not.toContain("data-incomplete-link");
		// Bracket syntax stays literal text (the bare URL may still be autolinked by GFM)
		expect(html).toContain("[the docs](");
	});
});

describe("processBlocksSync streaming behavior", () => {
	test("regex character classes in code do not collapse the document into one block", () => {
		const content = [
			"Match whitespace:",
			"",
			"```js",
			"const re = /[^\\s>]+/;",
			"```",
			"",
			"And some trailing explanation.",
		].join("\n");
		const blocks = processBlocksSync(content, []);
		expect(blocks.length).toBeGreaterThan(1);
	});

	test("block ids of completed blocks are stable while streaming and after completion", () => {
		const full = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.";
		// Final render: completed message, no streaming repairs
		const fullIds = processBlocksSync(full, [], false).map((b) => b.id);
		// Mid-stream render: third paragraph still arriving, repairs active
		const partialIds = processBlocksSync(`${full.slice(0, -10)}`, [], true).map((b) => b.id);
		expect(partialIds.slice(0, -1)).toEqual(fullIds.slice(0, partialIds.length - 1));
	});
});
