/**
 * deferred-commit.test.ts — Structural regression for Fix 1.
 *
 * Verifies that `auto-post-unit.ts` implements the deferred-commit pattern:
 *
 *   postUnitPreVerification (execute-task only):
 *     - calls stageOnly() instead of a full commit
 *     - sets s.stagedPendingCommit = true after staging
 *
 *   postUnitPostVerification:
 *     - checks stagedPendingCommit before attempting commitStaged
 *     - clears the flag (sets to false) BEFORE calling commitStaged
 *       (prevents double-commit on retry)
 *     - wraps commitStaged in try/catch; catch uses logWarning (non-fatal)
 *       and does NOT re-throw
 *
 *   Guard:
 *     - the stageOnly / stagedPendingCommit path is only entered for
 *       execute-task units (not for lifecycle-only or other types)
 *
 * Testing strategy:
 *   node:test does NOT enable --experimental-test-module-mocks, so
 *   module-level mocking is unavailable.  We verify structural invariants
 *   by inspecting the compiled source text of auto-post-unit.ts, matching
 *   key patterns that encode the required behavior.  This is the same
 *   approach used in verification-fail-closed.test.ts.
 *
 * Status: tests will FAIL until Fix 1 is implemented.  The TODO(fix-1-deferral)
 * comment in postUnitPreVerification marks the exact insertion point.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, it } from 'vitest';

const src = readFileSync(
	join(import.meta.dirname, "..", "auto-post-unit.ts"),
	"utf-8",
);

// ─── helpers ────────────────────────────────────────────────────────────────

/**
 * Extract the body of a named export function from the source.
 * Returns the text from the function signature up to (but not including)
 * the next top-level `export` keyword or end-of-file.
 */
function extractFunctionBody(name: string): string {
	const marker = `export async function ${name}(`;
	const start = src.indexOf(marker);
	assert.ok(start !== -1, `Function '${name}' not found in auto-post-unit.ts`);

	// Find the next top-level export after this function
	const nextExport = src.indexOf("\nexport ", start + marker.length);
	return nextExport === -1 ? src.slice(start) : src.slice(start, nextExport);
}

// ─── Test 1: stageOnly called before verification for execute-task ────────

describe("Fix 1 — stageOnly called in postUnitPreVerification for execute-task", () => {
	it("postUnitPreVerification contains a call to stageOnly()", () => {
		const body = extractFunctionBody("postUnitPreVerification");
		assert.ok(
			body.includes("stageOnly("),
			"postUnitPreVerification must call stageOnly() as part of the deferred-commit pattern (Fix 1)",
		);
	});

	it("postUnitPreVerification sets stagedPendingCommit = true after stageOnly", () => {
		const body = extractFunctionBody("postUnitPreVerification");

		const stageOnlyIdx = body.indexOf("stageOnly(");
		assert.ok(stageOnlyIdx !== -1, "stageOnly() call must be present");

		// stagedPendingCommit = true must appear after stageOnly in the function
		const afterStageOnly = body.slice(stageOnlyIdx);
		assert.ok(
			afterStageOnly.includes("stagedPendingCommit = true"),
			"postUnitPreVerification must set s.stagedPendingCommit = true after calling stageOnly()",
		);
	});
});

// ─── Test 2: commitStaged called in postUnitPostVerification when flag is set

describe("Fix 1 — commitStaged called in postUnitPostVerification when flag is set", () => {
	it("postUnitPostVerification contains a call to commitStaged(", () => {
		const body = extractFunctionBody("postUnitPostVerification");
		assert.ok(
			body.includes("commitStaged("),
			"postUnitPostVerification must call commitStaged() to finalize the deferred commit (Fix 1)",
		);
	});

	it("postUnitPostVerification checks stagedPendingCommit before calling commitStaged", () => {
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1, "commitStaged() must be present");

		// stagedPendingCommit must appear before commitStaged in the function body
		const beforeCommitStaged = body.slice(0, commitStagedIdx);
		assert.ok(
			beforeCommitStaged.includes("stagedPendingCommit"),
			"postUnitPostVerification must check s.stagedPendingCommit before calling commitStaged()",
		);
	});
});

// ─── Test 3: flag cleared before commitStaged (prevents double-commit on retry)

describe("Fix 1 — stagedPendingCommit cleared before commitStaged", () => {
	it("stagedPendingCommit is set to false before commitStaged is called", () => {
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1, "commitStaged() must be present");

		// Look backwards from the commitStaged call for the flag reset
		const windowBefore = body.slice(0, commitStagedIdx);

		// The flag must be set to false somewhere before the call
		assert.ok(
			windowBefore.includes("stagedPendingCommit = false"),
			"s.stagedPendingCommit must be set to false BEFORE calling commitStaged() to prevent double-commit on retry",
		);
	});

	it("the false-assignment appears closer to commitStaged than commitStaged does to the function start", () => {
		// Additional ordering check: false-assignment is in the near-vicinity
		// of commitStaged, not at the very top of the function accidentally
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1);

		// Find the last occurrence of 'stagedPendingCommit = false' before commitStaged
		const segment = body.slice(0, commitStagedIdx);
		const lastFalseIdx = segment.lastIndexOf("stagedPendingCommit = false");
		assert.ok(
			lastFalseIdx !== -1,
			"stagedPendingCommit = false must appear before commitStaged()",
		);

		// The false-assignment must be within 500 chars of the commitStaged call
		const gap = commitStagedIdx - lastFalseIdx;
		assert.ok(
			gap < 500,
			`stagedPendingCommit = false should be adjacent to the commitStaged() call (gap: ${gap} chars)`,
		);
	});
});

// ─── Test 4: deferred commit failure is non-fatal ────────────────────────

describe("Fix 1 — deferred commit failure is non-fatal", () => {
	it("commitStaged call is inside a try block", () => {
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1);

		// 'try' must appear before commitStaged in a reasonably close window
		const nearby = body.slice(
			Math.max(0, commitStagedIdx - 300),
			commitStagedIdx,
		);
		assert.ok(
			nearby.includes("try"),
			"commitStaged() must be wrapped in a try block so commit failures are non-fatal",
		);
	});

	it("the catch block after commitStaged uses logWarning (not logError)", () => {
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1);

		// Look at the region after commitStaged for the catch block
		const afterCommitStaged = body.slice(
			commitStagedIdx,
			commitStagedIdx + 500,
		);
		assert.ok(
			afterCommitStaged.includes("catch"),
			"there must be a catch block after the commitStaged() call",
		);
		assert.ok(
			afterCommitStaged.includes("logWarning"),
			"the catch block for commitStaged must use logWarning (non-fatal — commit failure should not halt the agent)",
		);
		assert.ok(
			!afterCommitStaged.includes("logError"),
			"the catch block for commitStaged must NOT use logError (commit failure is recoverable)",
		);
	});

	it("the catch block does NOT re-throw", () => {
		const body = extractFunctionBody("postUnitPostVerification");

		const commitStagedIdx = body.indexOf("commitStaged(");
		assert.ok(commitStagedIdx !== -1);

		const catchRegion = body.slice(commitStagedIdx, commitStagedIdx + 500);
		// Find the catch opening within that region
		const catchIdx = catchRegion.indexOf("catch");
		assert.ok(catchIdx !== -1, "catch must be present in region");

		// From catch opening, next 200 chars must not contain 'throw'
		const catchBody = catchRegion.slice(catchIdx, catchIdx + 300);
		assert.ok(
			!catchBody.includes("throw"),
			"the deferred-commit catch block must NOT re-throw — commit failures must be non-fatal",
		);
	});
});

// ─── Test 5: non-execute-task units skip the deferral path ───────────────

describe("Fix 1 — non-execute-task units skip the stageOnly deferral", () => {
	it("the stageOnly / stagedPendingCommit path is guarded by execute-task type check", () => {
		const body = extractFunctionBody("postUnitPreVerification");

		const stageOnlyIdx = body.indexOf("stageOnly(");
		assert.ok(stageOnlyIdx !== -1, "stageOnly() must be present");

		// Within 500 chars before stageOnly there must be a guard on unit type
		const windowBefore = body.slice(
			Math.max(0, stageOnlyIdx - 500),
			stageOnlyIdx,
		);
		const hasExecuteTaskGuard =
			windowBefore.includes('"execute-task"') ||
			windowBefore.includes("'execute-task'") ||
			windowBefore.includes("execute-task");

		assert.ok(
			hasExecuteTaskGuard,
			'the stageOnly() call must be guarded by a check for "execute-task" unit type so lifecycle-only units are not affected',
		);
	});

	it("the TODO(fix-1-deferral) placeholder comment is no longer present", () => {
		// This test validates that the implementation replaced the TODO comment.
		// Until Fix 1 is merged, this assertion will FAIL — that is intentional.
		assert.ok(
			!src.includes("TODO(fix-1-deferral)"),
			"The TODO(fix-1-deferral) comment must be removed once Fix 1 is implemented — it acts as a sentinel that the implementation is still missing",
		);
	});
});
