/**
 * adr-022-scaffold-profiles.test.mjs — ADR-022 scaffold profiles test suite.
 *
 * Purpose: verify behaviour contracts for the profile system: PROFILES shape,
 * disabled state round-trip, profile detection heuristics, drift skipping for
 * disabled/out-of-profile files, and the migrate algorithm.
 *
 * Consumer: CI (npm run test:unit).
 */
import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, test } from "vitest";
import {
	detectRepoProfile,
	ensureAgenticDocsScaffold,
} from "../agentic-docs-scaffold.js";
import {
	parseScaffoldMigrateArgs,
	runScaffoldMigrate,
} from "../commands-scaffold-migrate.js";
import {
	PROFILE_NAMES,
	PROFILES,
	SCAFFOLD_FILES,
} from "../scaffold-constants.js";
import { detectScaffoldDrift } from "../scaffold-drift.js";
import {
	bodyHash,
	extractMarker,
	parseMarker,
	readScaffoldManifest,
	stampScaffoldFile,
} from "../scaffold-versioning.js";

const tmpRoots = [];
afterEach(() => {
	for (const dir of tmpRoots.splice(0)) {
		rmSync(dir, { recursive: true, force: true });
	}
});

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-adr022-"));
	tmpRoots.push(root);
	return root;
}

function writePendingFile(root, relPath, content = `# ${relPath}\n`) {
	const target = join(root, relPath);
	mkdirSync(dirname(target), { recursive: true });
	writeFileSync(target, content, "utf-8");
	stampScaffoldFile(target, relPath, "1.0.0", "pending");
	return target;
}

// ─── PROFILES shape ──────────────────────────────────────────────────────────

describe("PROFILES shape", () => {
	test("PROFILE_NAMES_contains_five_built_in_profiles", () => {
		assert.deepEqual(
			PROFILE_NAMES.sort(),
			["app", "docs", "infra", "library", "minimal"].sort(),
		);
	});

	test("app_profile_contains_all_scaffold_files", () => {
		const allPaths = new Set(SCAFFOLD_FILES.map((f) => f.path));
		for (const p of PROFILES.app) {
			assert.ok(allPaths.has(p), `app profile includes unknown path: ${p}`);
		}
		// app must be the superset — every file belongs to at least app.
		for (const path of allPaths) {
			assert.ok(
				PROFILES.app.has(path),
				`SCAFFOLD_FILES path not in app profile: ${path}`,
			);
		}
	});

	test("minimal_is_strict_subset_of_app", () => {
		for (const p of PROFILES.minimal) {
			assert.ok(PROFILES.app.has(p), `minimal has path not in app: ${p}`);
		}
		// minimal must be strictly smaller.
		assert.ok(PROFILES.minimal.size < PROFILES.app.size);
	});

	test("infra_is_strict_subset_of_app", () => {
		for (const p of PROFILES.infra) {
			assert.ok(PROFILES.app.has(p), `infra has path not in app: ${p}`);
		}
	});

	test("docs_is_strict_subset_of_app", () => {
		for (const p of PROFILES.docs) {
			assert.ok(PROFILES.app.has(p), `docs has path not in app: ${p}`);
		}
	});
});

// ─── disabled state round-trip ───────────────────────────────────────────────

describe("disabled state", () => {
	test("parseMarker_accepts_disabled_state", () => {
		// The disabled state must be parseable — otherwise files stamped disabled
		// before Phase 1 ships would be read as null → untracked → re-rendered.
		const line =
			"<!-- sf-doc: version=1.0.0 template=AGENTS.md state=disabled hash=abc123 -->";
		const marker = parseMarker(line);
		assert.ok(marker, "parseMarker should not return null for state=disabled");
		assert.equal(marker.state, "disabled");
	});

	test("stampScaffoldFile_roundtrip_preserves_disabled_state", () => {
		const root = makeProject();
		const target = join(root, "AGENTS.md");
		writeFileSync(target, "# AGENTS\n", "utf-8");
		stampScaffoldFile(target, "AGENTS.md", "1.0.0", "disabled");

		const { marker } = extractMarker(target);
		assert.ok(marker, "marker should be present after stamping");
		assert.equal(marker.state, "disabled");
	});

	test("detectScaffoldDrift_puts_disabled_files_in_disabled_bucket", () => {
		const root = makeProject();
		const target = join(root, "AGENTS.md");
		writeFileSync(target, "# AGENTS\n", "utf-8");
		stampScaffoldFile(target, "AGENTS.md", "1.0.0", "disabled");

		const report = detectScaffoldDrift(root);
		const item = report.items.find((i) => i.path === "AGENTS.md");
		assert.ok(item, "AGENTS.md should appear in drift report");
		assert.equal(item.bucket, "disabled");
		assert.equal(report.countsByBucket.disabled, 1);
	});

	test("detectScaffoldDrift_disabled_bucket_is_not_actionable", () => {
		const root = makeProject();
		const target = join(root, "AGENTS.md");
		writeFileSync(target, "# AGENTS\n", "utf-8");
		stampScaffoldFile(target, "AGENTS.md", "1.0.0", "disabled");

		const report = detectScaffoldDrift(root);
		// Disabled files should not appear in missing/upgradable/editing-drift.
		const actionable = report.items.filter(
			(i) => i.path === "AGENTS.md" && i.bucket !== "disabled",
		);
		assert.equal(actionable.length, 0);
	});
});

// ─── profile-aware drift detection ───────────────────────────────────────────

describe("profile-aware drift", () => {
	test("detectScaffoldDrift_skips_missing_files_not_in_profile", () => {
		const root = makeProject();
		// Use "minimal" profile — only .siftignore and AGENTS.md.
		// A missing file from "app" profile (e.g. ARCHITECTURE.md) should NOT appear as missing.
		const archFile = "ARCHITECTURE.md";
		assert.ok(
			!PROFILES.minimal.has(archFile),
			"ARCHITECTURE.md should not be in minimal",
		);

		const report = detectScaffoldDrift(root, "minimal");
		const item = report.items.find((i) => i.path === archFile);
		// Should not be in missing bucket — it's out of profile.
		if (item) {
			assert.notEqual(item.bucket, "missing");
		}
	});

	test("detectScaffoldDrift_reports_missing_files_in_profile", () => {
		const root = makeProject();
		// AGENTS.md is in every profile — should appear as missing.
		const report = detectScaffoldDrift(root, "minimal");
		const item = report.items.find((i) => i.path === "AGENTS.md");
		assert.ok(item, "AGENTS.md should be in drift report for minimal profile");
		assert.equal(item.bucket, "missing");
	});
});

// ─── profile auto-detection ───────────────────────────────────────────────────

describe("detectRepoProfile", () => {
	test("detects_nix_repo_as_infra", () => {
		const root = makeProject();
		writeFileSync(join(root, "flake.nix"), "# nix\n", "utf-8");
		assert.equal(detectRepoProfile(root), "infra");
	});

	test("detects_terraform_repo_as_infra", () => {
		const root = makeProject();
		writeFileSync(join(root, "main.tf"), "# terraform\n", "utf-8");
		assert.equal(detectRepoProfile(root), "infra");
	});

	test("detects_node_react_repo_as_app", () => {
		const root = makeProject();
		writeFileSync(
			join(root, "package.json"),
			JSON.stringify({ dependencies: { react: "^18.0.0" } }),
			"utf-8",
		);
		assert.equal(detectRepoProfile(root), "app");
	});

	test("detects_node_non_ui_repo_as_library", () => {
		const root = makeProject();
		writeFileSync(
			join(root, "package.json"),
			JSON.stringify({ dependencies: { express: "^4.0.0" } }),
			"utf-8",
		);
		assert.equal(detectRepoProfile(root), "library");
	});

	test("detects_go_repo_as_library", () => {
		const root = makeProject();
		writeFileSync(join(root, "go.mod"), "module example.com\n", "utf-8");
		assert.equal(detectRepoProfile(root), "library");
	});

	test("detects_no_source_with_docs_dir_as_docs", () => {
		const root = makeProject();
		mkdirSync(join(root, "docs"));
		assert.equal(detectRepoProfile(root), "docs");
	});

	test("falls_back_to_app_for_unknown_shape", () => {
		const root = makeProject();
		assert.equal(detectRepoProfile(root), "app");
	});
});

// ─── ensureAgenticDocsScaffold — profile-aware ────────────────────────────────

describe("ensureAgenticDocsScaffold profile-aware", () => {
	test("writes_profile_to_manifest_on_first_run_when_nix_detected", () => {
		const root = makeProject();
		writeFileSync(join(root, "flake.nix"), "# nix\n", "utf-8");

		ensureAgenticDocsScaffold(root);

		const manifest = readScaffoldManifest(root);
		assert.equal(manifest.profile, "infra");
	});

	test("does_not_write_out_of_profile_files", () => {
		const root = makeProject();
		writeFileSync(join(root, "flake.nix"), "# nix\n", "utf-8");

		ensureAgenticDocsScaffold(root);

		// FRONTEND.md is in "app" but not in "infra" — must not be written.
		assert.ok(
			!PROFILES.infra.has("docs/FRONTEND.md"),
			"precondition: FRONTEND.md not in infra",
		);
		assert.equal(existsSync(join(root, "docs/FRONTEND.md")), false);
	});
});

// ─── migrate algorithm ───────────────────────────────────────────────────────

describe("scaffold migrate", () => {
	test("parseScaffoldMigrateArgs_parses_profile_prune_dry_run", () => {
		const opts = parseScaffoldMigrateArgs("--profile=infra --prune --dry-run");
		assert.equal(opts.profile, "infra");
		assert.equal(opts.prune, true);
		assert.equal(opts.dryRun, true);
	});

	test("runScaffoldMigrate_disables_pending_clean_hash_files_outside_target_profile", () => {
		const root = makeProject();
		// Find a file that is in app but not in infra.
		const appOnly = SCAFFOLD_FILES.find(
			(f) => PROFILES.app.has(f.path) && !PROFILES.infra.has(f.path),
		);
		assert.ok(appOnly, "precondition: at least one file in app not in infra");

		writePendingFile(root, appOnly.path);

		const result = runScaffoldMigrate(root, "infra");
		assert.ok(
			result.disabled.includes(appOnly.path),
			`expected ${appOnly.path} to be disabled`,
		);
	});

	test("runScaffoldMigrate_does_not_disable_editing_or_completed_files", () => {
		const root = makeProject();
		const appOnly = SCAFFOLD_FILES.find(
			(f) => PROFILES.app.has(f.path) && !PROFILES.infra.has(f.path),
		);
		assert.ok(appOnly, "precondition: at least one file in app not in infra");

		const target = join(root, appOnly.path);
		mkdirSync(dirname(target), { recursive: true });
		writeFileSync(target, `# ${appOnly.path}\n`, "utf-8");
		stampScaffoldFile(target, appOnly.path, "1.0.0", "editing");

		const result = runScaffoldMigrate(root, "infra");
		assert.ok(!result.disabled.includes(appOnly.path));
		assert.ok(result.warnings.some((w) => w.path === appOnly.path));
	});

	test("runScaffoldMigrate_does_not_disable_hash_diverged_pending_files", () => {
		const root = makeProject();
		const appOnly = SCAFFOLD_FILES.find(
			(f) => PROFILES.app.has(f.path) && !PROFILES.infra.has(f.path),
		);
		assert.ok(appOnly, "precondition");

		// Write file, stamp pending, then modify content so hash diverges.
		const target = join(root, appOnly.path);
		mkdirSync(dirname(target), { recursive: true });
		writeFileSync(target, "original content\n", "utf-8");
		stampScaffoldFile(target, appOnly.path, "1.0.0", "pending");
		// Now modify content (simulating user edit without changing the marker).
		const line1 = readFileSync(target, "utf-8").split("\n")[0];
		writeFileSync(target, `${line1}\nuser edited content\n`, "utf-8");

		const result = runScaffoldMigrate(root, "infra");
		assert.ok(!result.disabled.includes(appOnly.path));
		assert.ok(result.warnings.some((w) => w.path === appOnly.path));
	});

	test("runScaffoldMigrate_reenables_disabled_file_in_target_profile_with_clean_hash", () => {
		const root = makeProject();
		// Find a file in infra profile.
		const infraPath = [...PROFILES.infra][0];
		assert.ok(infraPath, "precondition: infra has at least one file");

		// Stamp it as disabled.
		const target = join(root, infraPath);
		mkdirSync(dirname(target), { recursive: true });
		writeFileSync(target, `# ${infraPath}\n`, "utf-8");
		stampScaffoldFile(target, infraPath, "1.0.0", "disabled");

		const result = runScaffoldMigrate(root, "infra");
		assert.ok(
			result.reEnabled.includes(infraPath),
			`expected ${infraPath} to be re-enabled`,
		);
	});

	test("runScaffoldMigrate_dry_run_does_not_modify_files", () => {
		const root = makeProject();
		const appOnly = SCAFFOLD_FILES.find(
			(f) => PROFILES.app.has(f.path) && !PROFILES.infra.has(f.path),
		);
		assert.ok(appOnly, "precondition");
		writePendingFile(root, appOnly.path);

		const result = runScaffoldMigrate(root, "infra", { dryRun: true });
		assert.ok(result.disabled.includes(appOnly.path));

		// File should still be pending on disk.
		const { marker } = extractMarker(join(root, appOnly.path));
		assert.equal(marker?.state, "pending");
	});

	test("runScaffoldMigrate_prune_deletes_pending_clean_hash_files", () => {
		const root = makeProject();
		const appOnly = SCAFFOLD_FILES.find(
			(f) => PROFILES.app.has(f.path) && !PROFILES.infra.has(f.path),
		);
		assert.ok(appOnly, "precondition");
		writePendingFile(root, appOnly.path);

		const result = runScaffoldMigrate(root, "infra", { prune: true });
		assert.ok(result.pruned.includes(appOnly.path));
		assert.equal(existsSync(join(root, appOnly.path)), false);
	});

	test("runScaffoldMigrate_updates_manifest_profile", () => {
		const root = makeProject();
		runScaffoldMigrate(root, "infra");
		const manifest = readScaffoldManifest(root);
		assert.equal(manifest.profile, "infra");
	});
});
