// tool-call-loop-guard — Tests for the tool-call loop detection guard.
//
// Verifies that identical consecutive tool calls are detected and blocked
// after exceeding the threshold, and that the guard resets properly.

import { describe, it } from "vitest";
import assert from "node:assert/strict";
import {
	checkToolCallLoop,
	disableToolCallLoopGuard,
	getToolCallLoopCount,
	resetToolCallLoopGuard,
} from "../bootstrap/tool-call-loop-guard.ts";

describe("tool-call loop guard", () => {
	it("allows first N calls and blocks after threshold", () => {
		resetToolCallLoopGuard();

		for (let i = 1; i <= 4; i++) {
			const result = checkToolCallLoop("web_search", { query: "same query" });
			assert.strictEqual(result.block, false, `Call ${i} should be allowed`);
			assert.strictEqual(
				result.count,
				i,
				`Count should be ${i} after call ${i}`,
			);
		}

		const blocked = checkToolCallLoop("web_search", { query: "same query" });
		assert.strictEqual(blocked.block, true, "5th identical call should be blocked");
		assert.ok(
			blocked.reason!.includes("web_search"),
			"Reason should mention tool name",
		);
		assert.ok(blocked.reason!.includes("5"), "Reason should mention count");
	});

	it("different tool calls reset the streak", () => {
		resetToolCallLoopGuard();

		checkToolCallLoop("web_search", { query: "query A" });
		checkToolCallLoop("web_search", { query: "query A" });
		checkToolCallLoop("web_search", { query: "query A" });
		assert.strictEqual(
			getToolCallLoopCount(),
			3,
			"Count should be 3 after 3 identical calls",
		);

		const different = checkToolCallLoop("bash", { command: "ls" });
		assert.strictEqual(different.block, false, "Different tool call should be allowed");
		assert.strictEqual(
			getToolCallLoopCount(),
			1,
			"Count should reset to 1 after different call",
		);

		checkToolCallLoop("web_search", { query: "query A" });
		checkToolCallLoop("web_search", { query: "query B" });
		assert.strictEqual(
			getToolCallLoopCount(),
			1,
			"Different args should reset count",
		);
	});

	it("reset clears the guard", () => {
		resetToolCallLoopGuard();
		checkToolCallLoop("web_search", { query: "q" });
		checkToolCallLoop("web_search", { query: "q" });
		checkToolCallLoop("web_search", { query: "q" });
		assert.strictEqual(
			getToolCallLoopCount(),
			3,
			"Count should be 3 before reset",
		);

		resetToolCallLoopGuard();
		assert.strictEqual(
			getToolCallLoopCount(),
			0,
			"Count should be 0 after reset",
		);

		const result = checkToolCallLoop("web_search", { query: "q" });
		assert.strictEqual(result.block, false, "Call after reset should be allowed");
		assert.strictEqual(
			getToolCallLoopCount(),
			1,
			"Count should be 1 after first call post-reset",
		);
	});

	it("disable makes guard permissive", () => {
		disableToolCallLoopGuard();

		for (let i = 0; i < 10; i++) {
			const result = checkToolCallLoop("web_search", { query: "same" });
			assert.strictEqual(
				result.block,
				false,
				`Call ${i + 1} should be allowed when disabled`,
			);
		}

		resetToolCallLoopGuard();
		checkToolCallLoop("web_search", { query: "q" });
		assert.strictEqual(
			getToolCallLoopCount(),
			1,
			"Guard should be active again after reset",
		);
	});

	it("arg order does not affect hash", () => {
		resetToolCallLoopGuard();

		checkToolCallLoop("web_search", { query: "test", limit: 5 });
		const result = checkToolCallLoop("web_search", { limit: 5, query: "test" });
		assert.strictEqual(
			result.block,
			false,
			"Same args in different order should count as consecutive",
		);
		assert.strictEqual(
			getToolCallLoopCount(),
			2,
			"Should detect as same call regardless of key order",
		);
	});

	it("nested/array arguments produce distinct hashes", () => {
		resetToolCallLoopGuard();

		for (let i = 1; i <= 5; i++) {
			const result = checkToolCallLoop("ask_user_questions", {
				questions: [{ id: `q${i}`, question: `Question ${i}?` }],
			});
			assert.strictEqual(
				result.block,
				false,
				`Nested call ${i} with unique content should be allowed`,
			);
			assert.strictEqual(
				getToolCallLoopCount(),
				1,
				`Each unique nested call should reset count to 1`,
			);
		}

		resetToolCallLoopGuard();
		const first = checkToolCallLoop("ask_user_questions", {
			questions: [{ id: "same", question: "Same?" }],
		});
		assert.strictEqual(
			first.block,
			false,
			"First ask_user_questions call should be allowed",
		);
		const blocked = checkToolCallLoop("ask_user_questions", {
			questions: [{ id: "same", question: "Same?" }],
		});
		assert.strictEqual(
			blocked.block,
			true,
			"2nd identical ask_user_questions call should be blocked (strict threshold)",
		);

		resetToolCallLoopGuard();
		for (let i = 1; i <= 4; i++) {
			const r = checkToolCallLoop("web_search", {
				questions: [{ id: "same", question: "Same?" }],
			});
			assert.strictEqual(
				r.block,
				false,
				`web_search call ${i} should be allowed (normal threshold)`,
			);
		}
		const blockedNormal = checkToolCallLoop("web_search", {
			questions: [{ id: "same", question: "Same?" }],
		});
		assert.strictEqual(
			blockedNormal.block,
			true,
			"5th identical web_search call should be blocked",
		);
	});

	it("nested object key order is normalized", () => {
		resetToolCallLoopGuard();

		checkToolCallLoop("tool", { outer: { b: 2, a: 1 } });
		checkToolCallLoop("tool", { outer: { a: 1, b: 2 } });
		assert.strictEqual(
			getToolCallLoopCount(),
			2,
			"Same nested args in different key order should match",
		);
	});
});
