import { describe, expect, it } from "vitest";
import { truncateHead, truncateLine, truncateTail } from "../../src/harness/utils/truncate.ts";

const encoder = new TextEncoder();

function byteLength(content: string): number {
	return encoder.encode(content).length;
}

function bufferTail(content: string, maxBytes: number): string {
	const bytes = Buffer.from(content, "utf8");
	if (bytes.length <= maxBytes) return content;
	let start = bytes.length - maxBytes;
	while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
	return bytes.subarray(start).toString("utf8");
}

function hasLoneSurrogate(text: string): boolean {
	return [...text].some((character) => {
		if (character.length !== 1) return false;
		const code = character.charCodeAt(0);
		return code >= 0xd800 && code <= 0xdfff;
	});
}

function assertMatchesBufferTail(input: string, maxByteValues?: readonly number[]): void {
	const totalBytes = Buffer.byteLength(input, "utf8");
	const values = maxByteValues ?? Array.from({ length: totalBytes + 5 }, (_, maxBytes) => maxBytes);
	for (const maxBytes of values) {
		const result = truncateTail(input, { maxBytes, maxLines: 10 });
		const expected = bufferTail(input, maxBytes);
		if (result.content !== expected) {
			throw new Error(
				`tail mismatch input=${JSON.stringify(input)} maxBytes=${maxBytes} expected=${JSON.stringify(expected)} actual=${JSON.stringify(result.content)}`,
			);
		}
		const outputBytes = Buffer.byteLength(result.content, "utf8");
		if (outputBytes > maxBytes) {
			throw new Error(
				`tail output exceeded byte limit input=${JSON.stringify(input)} maxBytes=${maxBytes} outputBytes=${outputBytes}`,
			);
		}
	}
}

function sampledByteLimits(input: string): number[] {
	const totalBytes = Buffer.byteLength(input, "utf8");
	const candidates = [
		0,
		1,
		2,
		3,
		4,
		5,
		8,
		Math.floor(totalBytes / 2) - 1,
		Math.floor(totalBytes / 2),
		Math.floor(totalBytes / 2) + 1,
		totalBytes - 8,
		totalBytes - 5,
		totalBytes - 4,
		totalBytes - 3,
		totalBytes - 2,
		totalBytes - 1,
		totalBytes,
		totalBytes + 1,
		totalBytes + 4,
	];
	return [...new Set(candidates.filter((value) => value >= 0))].sort((a, b) => a - b);
}

describe("truncate utilities", () => {
	it("truncates grep lines from the middle with ASCII input", () => {
		const result = truncateLine("abcdefghijklmnopqrstuvwxyz", 10);

		expect(result).toEqual({ text: "abcde... [truncated] ...vwxyz", wasTruncated: true });
	});

	it("truncates grep lines without splitting surrogate pairs", () => {
		const result = truncateLine("a\u{1F642}b\u{1F642}c", 4);

		expect(result.wasTruncated).toBe(true);
		expect(result.text).toBe("a\u{1F642}... [truncated] ...\u{1F642}c");
		expect([...result.text].join("")).toBe(result.text);
		expect(hasLoneSurrogate(result.text)).toBe(false);
	});

	it("counts UTF-8 bytes without Node Buffer", () => {
		const content = "aé🙂\nb";
		const result = truncateHead(content, { maxBytes: 100, maxLines: 10 });

		expect(result.truncated).toBe(false);
		expect(result.totalBytes).toBe(byteLength(content));
		expect(result.outputBytes).toBe(byteLength(content));
		expect(result.totalBytes).toBe(9);
	});

	it("truncates head on UTF-8 byte limits without partial lines", () => {
		const content = "éé\nabc";
		const result = truncateHead(content, { maxBytes: 4, maxLines: 10 });

		expect(result.content).toBe("éé");
		expect(result.truncated).toBe(true);
		expect(result.truncatedBy).toBe("bytes");
		expect(result.outputBytes).toBe(4);
		expect(result.firstLineExceedsLimit).toBe(false);
	});

	it("reports head truncation when the first line exceeds the byte limit", () => {
		const result = truncateHead("éé\nabc", { maxBytes: 3, maxLines: 10 });

		expect(result.content).toBe("");
		expect(result.truncated).toBe(true);
		expect(result.truncatedBy).toBe("bytes");
		expect(result.firstLineExceedsLimit).toBe(true);
	});

	it("truncates tail on UTF-8 boundaries when only a partial last line fits", () => {
		const result = truncateTail("aé🙂b", { maxBytes: 5, maxLines: 10 });

		expect(result.content).toBe("🙂b");
		expect(result.truncated).toBe(true);
		expect(result.truncatedBy).toBe("bytes");
		expect(result.lastLinePartial).toBe(true);
		expect(result.outputBytes).toBe(5);
	});

	it("truncates an oversized single line with a trailing newline", () => {
		const input = `${"X".repeat(300_000)}\n`;
		const result = truncateTail(input, { maxBytes: 1024, maxLines: 100 });

		expect(result.content).toBe("X".repeat(1024));
		expect(result.outputBytes).toBe(1024);
		expect(result.outputLines).toBe(1);
		expect(result.lastLinePartial).toBe(true);
		expect(result.truncatedBy).toBe("bytes");
	});

	it("drops an oversized trailing character when it cannot fit in tail byte limit", () => {
		const result = truncateTail("abc🙂", { maxBytes: 3, maxLines: 10 });

		expect(result.content).toBe("");
		expect(result.truncated).toBe(true);
		expect(result.truncatedBy).toBe("bytes");
		expect(result.lastLinePartial).toBe(true);
		expect(result.outputBytes).toBe(0);
	});

	it("matches Buffer tail truncation semantics for surrogate edge cases", () => {
		const inputs = ["a\ud83d", "\ude42b", "a\ude42b", "\ud83d\ud83d\ude42", "\ud83d\ude42\ude42", "👩‍💻"];
		for (const input of inputs) assertMatchesBufferTail(input);
	});

	it("matches Buffer tail truncation semantics across deterministic fuzz cases", () => {
		const alphabet = [
			"a",
			"\u007f",
			"\u0080",
			"é",
			"\u07ff",
			"\u0800",
			"中",
			"\ud7ff",
			"\ud800",
			"\ud83d",
			"\udc00",
			"\ude42",
			"🙂",
			"\ue000",
			"\uffff",
		];

		function checkExhaustive(prefix: string, depth: number): void {
			assertMatchesBufferTail(prefix, sampledByteLimits(prefix));
			if (depth === 0) return;
			for (const character of alphabet) checkExhaustive(prefix + character, depth - 1);
		}
		checkExhaustive("", 3);

		let seed = 0x12345678;
		function random(): number {
			seed = (seed * 1664525 + 1013904223) >>> 0;
			return seed / 0x100000000;
		}
		for (let i = 0; i < 1_000; i++) {
			let input = "";
			const length = Math.floor(random() * 80);
			for (let j = 0; j < length; j++) input += alphabet[Math.floor(random() * alphabet.length)];
			assertMatchesBufferTail(input, sampledByteLimits(input));
		}
	});
});
