/**
 * Tests for scaffold-drift (ADR-021 Phase B).
 *
 * Covers the five drift buckets and the manifestPresent flag.
 */
import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from 'vitest';

import { SCAFFOLD_FILES } from "../agentic-docs-scaffold.ts";
import { detectScaffoldDrift, migrateLegacyScaffold } from "../scaffold-drift.ts";
import {
	SCAFFOLD_MANIFEST_RELPATH,
	stampScaffoldFile,
} from "../scaffold-versioning.ts";

function makeTmp(): string {
	return mkdtempSync(join(tmpdir(), "sf-scaffold-drift-"));
}

/** Pick a scaffold entry that ships a Markdown file (not the .siftignore). */
function pickMarkdownTarget(): { path: string; content: string } {
	const f = SCAFFOLD_FILES.find((s) => s.path === "AGENTS.md");
	if (!f) throw new Error("AGENTS.md must be in SCAFFOLD_FILES for this test");
	return { path: f.path, content: f.content };
}

describe("detectScaffoldDrift", () => {
	let dir: string;
	beforeEach(() => {
		dir = makeTmp();
	});
	afterEach(() => {
		rmSync(dir, { recursive: true, force: true });
	});

	test("missing file → bucket missing", () => {
		const report = detectScaffoldDrift(dir);
		const item = report.items.find((i) => i.path === "AGENTS.md");
		assert.ok(item);
		assert.equal(item?.bucket, "missing");
		assert.ok(report.countsByBucket.missing > 0);
	});

	test("manifestPresent is false when no manifest exists", () => {
		const report = detectScaffoldDrift(dir);
		assert.equal(report.manifestPresent, false);
		// Report still computed
		assert.ok(report.items.length > 0);
	});

	test("manifestPresent is true when manifest has entries", () => {
		mkdirSync(join(dir, ".sf"), { recursive: true });
		writeFileSync(
			join(dir, SCAFFOLD_MANIFEST_RELPATH),
			JSON.stringify({
				schemaVersion: 1,
				applied: [
					{
						path: "AGENTS.md",
						template: "AGENTS.md",
						version: "0.0.0",
						appliedAt: "2026-01-01T00:00:00Z",
						stateAtApply: "pending",
						contentHash: "sha256:abc",
					},
				],
			}),
			"utf-8",
		);
		const report = detectScaffoldDrift(dir);
		assert.equal(report.manifestPresent, true);
	});

	test("file with current marker, body hash matches → not upgradable, not editing-drift", () => {
		const target = pickMarkdownTarget();
		const fp = join(dir, target.path);
		writeFileSync(fp, target.content, "utf-8");
		const SF_VERSION = process.env.SF_VERSION || "0.0.0";
		stampScaffoldFile(fp, target.path, SF_VERSION);

		const report = detectScaffoldDrift(dir);
		const item = report.items.find((i) => i.path === target.path);
		assert.ok(item);
		assert.notEqual(item?.bucket, "upgradable");
		assert.notEqual(item?.bucket, "editing-drift");
		assert.notEqual(item?.bucket, "missing");
		assert.equal(item?.currentVersion, SF_VERSION);
	});

	test("file with older-version marker, body hash matches → upgradable", () => {
		const target = pickMarkdownTarget();
		const fp = join(dir, target.path);
		writeFileSync(fp, target.content, "utf-8");
		// Stamp with a deliberately-old version. This works regardless of what
		// process.env.SF_VERSION is, as long as SF_VERSION > 0.0.1 — which is
		// also true when SF_VERSION is unset (defaults to "0.0.0" → not <
		// "0.0.1"). To make this robust, force a higher SF_VERSION for the
		// scope of this test.
		const prev = process.env.SF_VERSION;
		process.env.SF_VERSION = "9.9.9";
		try {
			stampScaffoldFile(fp, target.path, "0.0.1");
			const report = detectScaffoldDrift(dir);
			const item = report.items.find((i) => i.path === target.path);
			assert.ok(item);
			assert.equal(item?.bucket, "upgradable");
			assert.equal(item?.currentVersion, "0.0.1");
			assert.equal(item?.shipVersion, "9.9.9");
		} finally {
			if (prev === undefined) delete process.env.SF_VERSION;
			else process.env.SF_VERSION = prev;
		}
	});

	test("file with marker, body hash drifted → editing-drift", () => {
		const target = pickMarkdownTarget();
		const fp = join(dir, target.path);
		writeFileSync(fp, target.content, "utf-8");
		stampScaffoldFile(fp, target.path, "1.0.0");

		// Now mutate the body without restamping (simulate user/agent edit)
		const stamped = readFileSync(fp, "utf-8");
		const lines = stamped.split("\n");
		// Append a new line to the body without touching the marker
		const mutated = lines[0] + "\n" + lines.slice(1).join("\n") + "\n# user edit\n";
		writeFileSync(fp, mutated, "utf-8");

		const report = detectScaffoldDrift(dir);
		const item = report.items.find((i) => i.path === target.path);
		assert.ok(item);
		assert.equal(item?.bucket, "editing-drift");
		assert.equal(item?.hashDrifted, true);
	});

	test("file with marker state=completed → bucket customized", () => {
		const target = pickMarkdownTarget();
		const fp = join(dir, target.path);
		writeFileSync(fp, target.content, "utf-8");
		stampScaffoldFile(fp, target.path, "1.0.0", "completed");

		const report = detectScaffoldDrift(dir);
		const item = report.items.find((i) => i.path === target.path);
		assert.ok(item);
		assert.equal(item?.bucket, "customized");
	});

	test("file without marker → bucket untracked", () => {
		const target = pickMarkdownTarget();
		const fp = join(dir, target.path);
		writeFileSync(fp, "# Custom Content\n\nuser-edited\n", "utf-8");

		const report = detectScaffoldDrift(dir);
		const item = report.items.find((i) => i.path === target.path);
		assert.ok(item);
		assert.equal(item?.bucket, "untracked");
		assert.equal(item?.currentVersion, undefined);
	});

	test("countsByBucket sums to items.length", () => {
		const report = detectScaffoldDrift(dir);
		const sum = Object.values(report.countsByBucket).reduce((a, b) => a + b, 0);
		assert.equal(sum, report.items.length);
	});
});

describe("migrateLegacyScaffold", () => {
	test("on empty dir, both migrated and skipped are empty", () => {
		const dir = makeTmp();
		try {
			const result = migrateLegacyScaffold(dir);
			assert.deepEqual(result.migrated, []);
			assert.deepEqual(result.skipped, []);
		} finally {
			rmSync(dir, { recursive: true, force: true });
		}
	});
});
