import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from 'vitest';
import { parseTaskPlanFile } from "../files.ts";
import { parsePlan } from "../parsers.ts";
import {
	closeDatabase,
	getSlice,
	getSliceTasks,
	getTask,
	insertMilestone,
	insertSlice,
	openDatabase,
} from "../sf-db.ts";
import { handlePlanSlice, type PlanSliceParams } from "../tools/plan-slice.ts";

function makeTmpBase(): string {
	const base = mkdtempSync(join(tmpdir(), "sf-plan-slice-"));
	mkdirSync(join(base, ".sf", "milestones", "M001", "slices", "S02", "tasks"), {
		recursive: true,
	});
	return base;
}

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

function seedParentSlice(): void {
	insertMilestone({ id: "M001", title: "Milestone", status: "active" });
	insertSlice({
		id: "S02",
		milestoneId: "M001",
		title: "Planning slice",
		status: "pending",
		demo: "Rendered plans exist.",
	});
}

function validParams(): PlanSliceParams {
	return {
		milestoneId: "M001",
		sliceId: "S02",
		goal: "Persist slice planning through the DB.",
		successCriteria:
			"- Slice plan renders from DB\n- Task plan files are regenerated",
		proofLevel: "integration",
		integrationClosure:
			"Planning handlers now write DB rows and render plan artifacts.",
		observabilityImpact:
			"- Validation failures return structured errors\n- Cache invalidation is proven by parse-visible state updates",
		adversarialReview: {
			partner:
				"The slice is tightly scoped and the task split aligns with the actual write/render boundary.",
			combatant:
				"The biggest risk is letting ceremony state exist in the prompt but disappear from persisted slice state.",
			architect:
				"The plan must round-trip through DB rows, markdown renders, and recovery flows without losing its moderation context.",
		},
		planningMeeting: {
			trigger:
				"The slice changes planning-system behavior and needs an explicit route before execution.",
			pm: "Keep the slice focused on persistence and render boundaries, not broader planner redesign.",
			userAdvocate:
				"Users need the planning context to survive the DB-backed render path.",
			customerPanel:
				"Operators and reviewers both need enough meeting context to audit the plan.",
			business: "Reliable planning persistence reduces wasted execution work.",
			researcher:
				"The surrounding workflow already reads ceremony sections from rendered plan artifacts and DB-backed state.",
			deliveryLead:
				"Sequence the fix through the handler, renderer, and tests without widening scope.",
			partner:
				"The current task split is enough if the review state is stored and rendered faithfully.",
			combatant:
				"Without persistence proof, the meeting is just commentary and the system can drift.",
			architect:
				"Persist both the adversarial review and the meeting route in the same transactional planning write.",
			moderator:
				"Weighted synthesis says this slice is ready to plan and execute once the ceremony state is preserved.",
			recommendedRoute: "planning",
			confidenceSummary:
				"High confidence after proving DB + markdown persistence of the ceremony fields.",
		},
		tasks: [
			{
				taskId: "T01",
				title: "Write slice handler",
				description: "Implement the slice planning handler.",
				estimate: "45m",
				files: ["src/resources/extensions/sf/tools/plan-slice.ts"],
				verify:
					"node --test src/resources/extensions/sf/tests/plan-slice.test.ts",
				inputs: ["src/resources/extensions/sf/tools/plan-milestone.ts"],
				expectedOutput: ["src/resources/extensions/sf/tools/plan-slice.ts"],
				observabilityImpact:
					"Tests exercise cache invalidation and render failure paths.",
			},
			{
				taskId: "T02",
				title: "Write task handler",
				description: "Implement the task planning handler.",
				estimate: "30m",
				files: ["src/resources/extensions/sf/tools/plan-task.ts"],
				verify:
					"node --test src/resources/extensions/sf/tests/plan-task.test.ts",
				inputs: ["src/resources/extensions/sf/tools/plan-task.ts"],
				expectedOutput: ["src/resources/extensions/sf/tests/plan-task.test.ts"],
				observabilityImpact: "Task-plan renders remain parse-compatible.",
			},
		],
	};
}

test("handlePlanSlice writes slice/task planning state and renders plan artifacts", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();

		const result = await handlePlanSlice(validParams(), base);
		assert.ok(
			!("error" in result),
			`unexpected error: ${"error" in result ? result.error : ""}`,
		);

		const slice = getSlice("M001", "S02");
		assert.ok(slice);
		assert.equal(slice?.goal, "Persist slice planning through the DB.");
		assert.equal(slice?.proof_level, "integration");
		assert.match(slice?.adversarial_partner ?? "", /tightly scoped/);
		assert.equal(slice?.planning_meeting?.recommendedRoute, "planning");
		assert.match(
			slice?.planning_meeting?.confidenceSummary ?? "",
			/High confidence/,
		);

		const tasks = getSliceTasks("M001", "S02");
		assert.equal(tasks.length, 2);
		assert.equal(tasks[0]?.title, "Write slice handler");
		assert.equal(
			tasks[0]?.description,
			"Implement the slice planning handler.",
		);
		assert.equal(tasks[1]?.estimate, "30m");

		const planPath = join(
			base,
			".sf",
			"milestones",
			"M001",
			"slices",
			"S02",
			"S02-PLAN.md",
		);
		assert.ok(existsSync(planPath), "slice plan should be rendered to disk");
		const parsedPlan = parsePlan(readFileSync(planPath, "utf-8"));
		assert.equal(parsedPlan.goal, "Persist slice planning through the DB.");
		assert.equal(parsedPlan.tasks.length, 2);
		assert.equal(parsedPlan.tasks[0]?.id, "T01");
		const renderedPlan = readFileSync(planPath, "utf-8");
		assert.match(renderedPlan, /## Adversarial Review/);
		assert.match(renderedPlan, /## Planning Meeting/);
		assert.match(renderedPlan, /### User Advocate/);
		assert.match(renderedPlan, /### Customer Panel/);
		assert.match(renderedPlan, /### Delivery Lead/);
		assert.match(renderedPlan, /### Recommended Route/);
		assert.match(renderedPlan, /planning/);

		const taskPlanPath = join(
			base,
			".sf",
			"milestones",
			"M001",
			"slices",
			"S02",
			"tasks",
			"T01-PLAN.md",
		);
		assert.ok(existsSync(taskPlanPath), "task plan should be rendered to disk");
		const taskPlan = parseTaskPlanFile(readFileSync(taskPlanPath, "utf-8"));
		assert.deepEqual(taskPlan.frontmatter.skills_used, []);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice persists a routed-back planning meeting without pretending the slice is execution-ready", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const baseParams = validParams();
		const baseMeeting = baseParams.planningMeeting!;

		const result = await handlePlanSlice(
			{
				...baseParams,
				planningMeeting: {
					trigger: baseMeeting.trigger,
					pm: baseMeeting.pm,
					researcher: baseMeeting.researcher,
					partner: baseMeeting.partner,
					combatant: baseMeeting.combatant,
					architect: baseMeeting.architect,
					moderator:
						"Keep researching before execution; the premise is still too weak.",
					recommendedRoute: "researching",
					confidenceSummary: "Low confidence until the premise is narrowed.",
				},
			},
			base,
		);
		assert.ok(
			!("error" in result),
			`unexpected error: ${"error" in result ? result.error : ""}`,
		);

		const slice = getSlice("M001", "S02");
		assert.equal(slice?.planning_meeting?.recommendedRoute, "researching");

		const renderedPlan = readFileSync(
			join(base, ".sf", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
			"utf-8",
		);
		assert.match(renderedPlan, /## Planning Meeting/);
		assert.match(renderedPlan, /researching/);
		assert.match(renderedPlan, /Low confidence/);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice rejects invalid payloads", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const result = await handlePlanSlice({ ...validParams(), tasks: [] }, base);
		assert.ok("error" in result);
		assert.match(
			result.error,
			/validation failed: tasks must be a non-empty array/,
		);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice normalizes escaped newlines before rendering", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const result = await handlePlanSlice(
			{
				...validParams(),
				successCriteria: "First criterion\\nSecond criterion",
			},
			base,
		);
		assert.ok(
			!("error" in result),
			`unexpected error: ${"error" in result ? result.error : ""}`,
		);

		const renderedPlan = readFileSync(
			join(base, ".sf", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
			"utf-8",
		);
		assert.match(renderedPlan, /- First criterion\n- Second criterion/);
		assert.doesNotMatch(renderedPlan, /\\n/);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice rejects leaked JSON fields inside markdown text fields", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const result = await handlePlanSlice(
			{
				...validParams(),
				proofLevel:
					'Contract test passes.", "integrationClosure": "This belongs in another field"',
			},
			base,
		);

		assert.ok("error" in result);
		assert.match(
			result.error,
			/validation failed: proofLevel appears to contain leaked JSON fields/,
		);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice rejects missing, null, and empty planningMeeting with explicit guidance", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const { planningMeeting: _meeting, ...missingMeetingParams } =
			validParams();
		const cases: Array<[string, PlanSliceParams]> = [
			["missing", missingMeetingParams as unknown as PlanSliceParams],
			[
				"null",
				{
					...validParams(),
					planningMeeting: null,
				} as unknown as PlanSliceParams,
			],
			[
				"empty",
				{ ...validParams(), planningMeeting: {} } as unknown as PlanSliceParams,
			],
		];

		for (const [label, params] of cases) {
			const result = await handlePlanSlice(params, base);
			assert.ok(
				"error" in result,
				`${label} planningMeeting should fail validation`,
			);
			assert.equal(
				result.error,
				"planningMeeting must be a populated object — write at least 2-3 perspectives. Skipping is not allowed.",
			);
		}
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice rejects missing parent slice", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		insertMilestone({ id: "M001", title: "Milestone", status: "active" });
		const result = await handlePlanSlice(validParams(), base);
		assert.ok("error" in result);
		assert.match(result.error, /missing parent slice: M001\/S02/);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice surfaces render failures without changing parse-visible task-plan state for the failing task", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		const failingTaskPlanPath = join(
			base,
			".sf",
			"milestones",
			"M001",
			"slices",
			"S02",
			"tasks",
			"T01-PLAN.md",
		);
		writeFileSync(
			failingTaskPlanPath,
			"---\nestimated_steps: 1\nestimated_files: 1\nskills_used: []\n---\n\n# T01: Cached task\n",
			"utf-8",
		);
		rmSync(failingTaskPlanPath, { force: true });
		mkdirSync(failingTaskPlanPath, { recursive: true });

		const result = await handlePlanSlice(validParams(), base);
		assert.ok("error" in result);
		assert.match(result.error, /render failed:/);

		assert.ok(
			existsSync(failingTaskPlanPath),
			"failing task plan path should remain the blocking directory",
		);
		assert.equal(
			getTask("M001", "S02", "T01")?.description,
			"Implement the slice planning handler.",
		);
	} finally {
		cleanup(base);
	}
});

test("handlePlanSlice reruns idempotently and refreshes parse-visible state", async () => {
	const base = makeTmpBase();
	openDatabase(join(base, ".sf", "sf.db"));

	try {
		seedParentSlice();
		writeFileSync(
			join(base, ".sf", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
			"# S02: Cached\n\n**Goal:** old value\n\n## Tasks\n\n- [ ] **T01: Cached task**\n",
			"utf-8",
		);

		const first = await handlePlanSlice(validParams(), base);
		assert.ok(!("error" in first));

		const second = await handlePlanSlice(
			{
				...validParams(),
				goal: "Updated goal from rerun.",
				tasks: [
					{
						...validParams().tasks[0],
						description: "Updated slice handler description.",
					},
					validParams().tasks[1],
				],
			},
			base,
		);
		assert.ok(!("error" in second));

		const parsedAfter = parsePlan(
			readFileSync(
				join(base, ".sf", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
				"utf-8",
			),
		);
		assert.equal(parsedAfter.goal, "Updated goal from rerun.");
		const task = getTask("M001", "S02", "T01");
		assert.equal(task?.description, "Updated slice handler description.");
	} finally {
		cleanup(base);
	}
});
