/**
 * Tests for read tool offset clamping (#3007).
 *
 * When offset exceeds file length, the read tool should clamp to the
 * last line instead of throwing, preventing downstream JSON parse errors
 * in autonomous mode milestone completion.
 */

import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";

import { createReadTool } from "../../packages/coding-agent/src/core/tools/read.ts";

// ─── Helpers ─────────────────────────────────────────────────────────────────

function makeTmpDir(): { dir: string; cleanup: () => void } {
	const dir = mkdtempSync(join(tmpdir(), "read-tool-test-"));
	return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
}

function writeLines(dir: string, name: string, lineCount: number): string {
	const lines = Array.from(
		{ length: lineCount },
		(_, i) => `Line ${i + 1}: content`,
	);
	const filePath = join(dir, name);
	writeFileSync(filePath, lines.join("\n"));
	return filePath;
}

// ═══════════════════════════════════════════════════════════════════════════
// Offset beyond file bounds — should clamp, not throw (#3007)
// ═══════════════════════════════════════════════════════════════════════════

test("read tool: offset exceeding file length should NOT throw (#3007)", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "small-artifact.md", 13);

	const readTool = createReadTool(dir);

	// offset 30 on a 13-line file — exact reproduction of #3007
	const result = await readTool.execute("test-call", {
		path: "small-artifact.md",
		offset: 30,
	});

	assert.ok(result, "should return a result, not throw");
	assert.ok(result.content, "should have content");
	assert.ok(
		result.content.length > 0,
		"should have at least one content block",
	);

	const text = (result.content[0] as any).text as string;
	assert.ok(typeof text === "string", "first content block should be text");
	// Should include the last line of the file (clamped)
	assert.ok(
		text.includes("Line 13"),
		"should include last line of file after clamping",
	);
});

test("read tool: offset 100 on a 5-line file clamps to last line", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "tiny-file.txt", 5);

	const readTool = createReadTool(dir);
	const result = await readTool.execute("test-call", {
		path: "tiny-file.txt",
		offset: 100,
	});

	const text = (result.content[0] as any).text as string;
	assert.ok(
		text.includes("Line 5"),
		"should include the last line of the file",
	);
});

test("read tool: offset at exact last line works normally", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "exact-offset.txt", 5);

	const readTool = createReadTool(dir);
	// offset 5 on a 5-line file — should return line 5 (valid, no clamping needed)
	const result = await readTool.execute("test-call", {
		path: "exact-offset.txt",
		offset: 5,
	});

	const text = (result.content[0] as any).text as string;
	assert.ok(text.includes("Line 5"), "should include line 5");
});

test("read tool: clamped offset includes notice about adjustment", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "notice-test.md", 10);

	const readTool = createReadTool(dir);
	const result = await readTool.execute("test-call", {
		path: "notice-test.md",
		offset: 50,
	});

	const text = (result.content[0] as any).text as string;
	// Should contain some notice that the offset was adjusted
	assert.ok(
		text.includes("clamped") ||
			text.includes("adjusted") ||
			text.includes("beyond"),
		`should indicate offset was clamped, got: ${text.slice(0, 200)}`,
	);
});

test("read tool: streaming with offset and limit on large file reads only requested lines", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);

	// Create a 1000-line file
	writeLines(dir, "large-file.txt", 1000);

	const readTool = createReadTool(dir);
	const result = await readTool.execute("test-call", {
		path: "large-file.txt",
		offset: 100,
		limit: 10,
	});

	const text = (result.content[0] as any).text as string;

	// Should include lines 100-109
	assert.ok(text.includes("Line 100:"), "should include line 100");
	assert.ok(text.includes("Line 109:"), "should include line 109");

	// Should NOT include line 1 or line 200
	assert.ok(!text.includes("Line 1:"), "should NOT include line 1");
	assert.ok(!text.includes("Line 200:"), "should NOT include line 200");

	// Should include continuation notice
	assert.ok(
		text.includes("more lines") || text.includes("offset="),
		"should include continuation notice",
	);
});

test("read tool: streaming with limit only reads first N lines", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "medium-file.txt", 500);

	const readTool = createReadTool(dir);
	const result = await readTool.execute("test-call", {
		path: "medium-file.txt",
		limit: 5,
	});

	const text = (result.content[0] as any).text as string;

	assert.ok(text.includes("Line 1:"), "should include line 1");
	assert.ok(text.includes("Line 5:"), "should include line 5");
	assert.ok(!text.includes("Line 6:"), "should NOT include line 6");
});

test("read tool: streaming with offset only reads from offset to end", async (_t) => {
	const { dir, cleanup } = makeTmpDir();
	afterEach(cleanup);
	writeLines(dir, "offset-only.txt", 100);

	const readTool = createReadTool(dir);
	const result = await readTool.execute("test-call", {
		path: "offset-only.txt",
		offset: 95,
	});

	const text = (result.content[0] as any).text as string;

	assert.ok(text.includes("Line 95:"), "should include line 95");
	assert.ok(text.includes("Line 100:"), "should include line 100");
	assert.ok(!text.includes("Line 94:"), "should NOT include line 94");
});
