/**
 * Path-traversal ID validation tests for handleValidateMilestone.
 *
 * Verifies that unsafe milestoneId values are rejected at the entry point
 * before any path construction or DB write occurs.
 * See: validation-safe-id-path-segments self-feedback entry.
 */

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from 'vitest';
import { clearParseCache } from "../files.ts";
import { clearPathCache } from "../paths.ts";
import { closeDatabase, openDatabase } from "../sf-db.ts";
import { handleValidateMilestone } from "../tools/validate-milestone.ts";
import type { ValidateMilestoneParams } from "../tools/validate-milestone.ts";

function makeTmpBase(): string {
	const base = join(tmpdir(), `sf-val-safeid-${randomUUID()}`);
	mkdirSync(join(base, ".sf", "milestones"), { recursive: true });
	return base;
}

function cleanup(base: string): void {
	clearPathCache();
	clearParseCache();
	closeDatabase();
	try {
		rmSync(base, { recursive: true, force: true });
	} catch {
		/* swallow */
	}
}

function openTestDb(base: string): void {
	const dbPath = join(base, ".sf", "sf.db");
	assert.equal(openDatabase(dbPath), true, "test DB should open");
}

const baseParams: Omit<ValidateMilestoneParams, "milestoneId"> = {
	verdict: "pass",
	remediationRound: 1,
	successCriteriaChecklist: "- [x] done",
	sliceDeliveryAudit: "all slices delivered",
	crossSliceIntegration: "integrated",
	requirementCoverage: "100%",
	verdictRationale: "all criteria met",
};

test("handleValidateMilestone rejects path traversal milestoneId: ../outside", async () => {
	const base = makeTmpBase();
	openTestDb(base);
	try {
		const result = await handleValidateMilestone(
			{ ...baseParams, milestoneId: "../outside" },
			base,
		);
		assert.ok("error" in result, "should return an error for traversal ID");
		assert.ok(
			typeof (result as { error: string }).error === "string",
			"error should be a string",
		);
		if ("details" in result) {
			const details = (
				result as { error: string; details: { error: string; field: string; reason: string } }
			).details;
			assert.equal(details.error, "unsafe_id");
			assert.equal(details.field, "milestone_id");
		}
	} finally {
		cleanup(base);
	}
});

test("handleValidateMilestone rejects path traversal milestoneId: M001/../etc/passwd", async () => {
	const base = makeTmpBase();
	openTestDb(base);
	try {
		const result = await handleValidateMilestone(
			{ ...baseParams, milestoneId: "M001/../etc/passwd" },
			base,
		);
		assert.ok("error" in result, "should return an error for traversal ID");
		if ("details" in result) {
			const details = (
				result as { error: string; details: { error: string; field: string; reason: string } }
			).details;
			assert.equal(details.error, "unsafe_id");
		}
	} finally {
		cleanup(base);
	}
});

test("handleValidateMilestone rejects milestoneId with path separator: a/b", async () => {
	const base = makeTmpBase();
	openTestDb(base);
	try {
		const result = await handleValidateMilestone(
			{ ...baseParams, milestoneId: "a/b" },
			base,
		);
		assert.ok("error" in result, "should return an error for ID with separator");
		if ("details" in result) {
			const details = (
				result as { error: string; details: { error: string; field: string; reason: string } }
			).details;
			assert.equal(details.error, "unsafe_id");
			assert.equal(details.field, "milestone_id");
		}
	} finally {
		cleanup(base);
	}
});

test("handleValidateMilestone rejects milestoneId with embedded newline", async () => {
	const base = makeTmpBase();
	openTestDb(base);
	try {
		const result = await handleValidateMilestone(
			{ ...baseParams, milestoneId: "M001\nbad: true" },
			base,
		);
		assert.ok(
			"error" in result,
			"should return an error for ID with control char",
		);
		if ("details" in result) {
			const details = (
				result as { error: string; details: { error: string; field: string; reason: string } }
			).details;
			assert.equal(details.error, "unsafe_id");
			assert.equal(details.field, "milestone_id");
			assert.ok(
				details.reason.includes("control char") || details.reason.includes("newline"),
				"reason should mention control char or newline",
			);
		}
	} finally {
		cleanup(base);
	}
});

test("handleValidateMilestone does not reject a safe milestoneId as unsafe_id (smoke)", async () => {
	const base = makeTmpBase();
	openTestDb(base);
	try {
		// A safe ID should pass the safe-id check; later DB/disk errors are expected
		// in this minimal test setup and do not indicate a safe-id rejection.
		let result:
			| { error: string }
			| { error: string; details: { error: string; field: string; reason: string } }
			| { milestoneId: string; verdict: string; validationPath: string }
			| undefined;
		try {
			result = await handleValidateMilestone(
				{ ...baseParams, milestoneId: "M001" },
				base,
			);
		} catch {
			// DB/disk errors from the minimal test setup are acceptable here
			return;
		}
		if (result !== undefined && "details" in result) {
			const details = (
				result as { error: string; details: { error: string } }
			).details;
			assert.notEqual(
				details.error,
				"unsafe_id",
				"safe ID should not produce unsafe_id error",
			);
		}
	} finally {
		cleanup(base);
	}
});
