/**
 * Smoke tests for template/demo seed fixtures.
 *
 * Validates that all seed files are well-formed, can be applied
 * to a fresh database, and that the resulting database passes
 * doctor checks. Does NOT start a dev server — these are fast,
 * programmatic tests that exercise the seed/validate/apply/doctor
 * pipeline directly.
 *
 * Also shells out to the CLI binary for seed --validate and doctor
 * commands to ensure the CLI interface works correctly.
 */

import { execFile } from "node:child_process";
import { existsSync, readFileSync, readdirSync, mkdtempSync, rmSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";

import { describe, it, expect, beforeAll, afterEach } from "vitest";

import { createDatabase } from "../../../src/database/connection.js";
import { runMigrations } from "../../../src/database/migrations/runner.js";
import { applySeed } from "../../../src/seed/apply.js";
import type { SeedFile } from "../../../src/seed/types.js";
import { validateSeed } from "../../../src/seed/validate.js";
import { LocalStorage } from "../../../src/storage/local.js";
import { ensureBuilt } from "../server.js";

const exec = promisify(execFile);

const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../../..");
const CLI_BIN = resolve(import.meta.dirname, "../../../dist/cli/index.mjs");
const VALIDATION_FAILED_RE = /validation failed/i;

// ---------------------------------------------------------------------------
// Discover all templates and demos with seed files
// ---------------------------------------------------------------------------

interface SiteFixture {
	/** Human-readable name for test output */
	name: string;
	/** Absolute path to the template/theme directory */
	dir: string;
	/** Absolute path to the seed file */
	seedPath: string;
	/** Parsed seed file contents */
	seed: SeedFile;
}

function discoverFixtures(): SiteFixture[] {
	const fixtures: SiteFixture[] = [];

	const dirs = [
		{ prefix: "templates", path: resolve(WORKSPACE_ROOT, "templates") },
		{ prefix: "demos", path: resolve(WORKSPACE_ROOT, "demos") },
	];

	for (const { prefix, path: parentDir } of dirs) {
		if (!existsSync(parentDir)) continue;

		for (const entry of readdirSync(parentDir)) {
			const dir = join(parentDir, entry);

			// Check for seed path in package.json first (emdash.seed config)
			let seedPath = join(dir, ".emdash", "seed.json");
			const pkgPath = join(dir, "package.json");

			if (existsSync(pkgPath)) {
				try {
					const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
					if (pkg.emdash?.seed) {
						seedPath = join(dir, pkg.emdash.seed);
					}
				} catch {
					// Ignore parse errors
				}
			}

			if (!existsSync(seedPath)) continue;

			const raw = readFileSync(seedPath, "utf-8");
			const seed = JSON.parse(raw) as SeedFile;

			fixtures.push({
				name: `${prefix}/${entry}`,
				dir,
				seedPath,
				seed,
			});
		}
	}

	return fixtures;
}

const fixtures = discoverFixtures();

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("Seed Fixture Smoke Tests", () => {
	let tempDirs: string[] = [];

	beforeAll(async () => {
		// Ensure CLI binary is built for CLI-based tests
		await ensureBuilt();
	}, 120_000);

	afterEach(() => {
		// Clean up any temp directories created during tests
		for (const dir of tempDirs) {
			rmSync(dir, { recursive: true, force: true });
		}
		tempDirs = [];
	});

	function createTempDir(): string {
		const dir = mkdtempSync(join(tmpdir(), "emdash-smoke-"));
		tempDirs.push(dir);
		return dir;
	}

	// Sanity check: we actually found fixtures to test
	it("discovers at least one template/demo with a seed file", () => {
		expect(fixtures.length).toBeGreaterThanOrEqual(1);
		const names = fixtures.map((f) => f.name);
		// At minimum the blog template should always be present.
		expect(names).toContain("templates/blog");
	});

	// -----------------------------------------------------------------------
	// Per-fixture tests
	// -----------------------------------------------------------------------

	for (const fixture of fixtures) {
		describe(fixture.name, () => {
			// --- Seed file is valid JSON with correct structure ---

			it("has a valid seed.json that parses as JSON", () => {
				expect(fixture.seed).toBeDefined();
				expect(fixture.seed.version).toBe("1");
			});

			// --- Programmatic validation ---

			it("passes programmatic seed validation", () => {
				const result = validateSeed(fixture.seed);
				if (!result.valid) {
					// Include errors in failure message for debuggability
					expect.fail(`Seed validation failed:\n${result.errors.join("\n")}`);
				}
				expect(result.valid).toBe(true);
			});

			// --- CLI --validate ---

			it("passes CLI seed --validate", async () => {
				const { stdout, stderr } = await exec(
					"node",
					[CLI_BIN, "seed", fixture.seedPath, "--validate"],
					{
						cwd: fixture.dir,
						timeout: 15_000,
					},
				);
				// The validate command should succeed (exit 0) — if it throws,
				// the test will fail with the error message
				expect(stdout + stderr).not.toMatch(VALIDATION_FAILED_RE);
			});

			// --- Seed applies to fresh database ---

			it("applies seed to a fresh database without errors", { timeout: 30_000 }, async () => {
				const tempDir = createTempDir();
				const dbPath = join(tempDir, "test.db");
				const uploadsDir = join(tempDir, "uploads");
				mkdirSync(uploadsDir, { recursive: true });

				// Create database and run migrations
				const db = createDatabase({ url: `file:${dbPath}` });

				try {
					const { applied } = await runMigrations(db);
					expect(applied.length).toBeGreaterThan(0);

					// Set up local storage for media resolution
					const storage = new LocalStorage({
						directory: uploadsDir,
						baseUrl: "/_emdash/api/media/file",
					});

					// Apply seed
					const result = await applySeed(db, fixture.seed, {
						includeContent: true,
						onConflict: "skip",
						storage,
						mediaBasePath: join(fixture.dir, ".emdash"),
					});

					// Verify collections were created
					if (fixture.seed.collections && fixture.seed.collections.length > 0) {
						expect(result.collections.created).toBeGreaterThan(0);
					}

					// Verify fields were created
					const totalFields =
						fixture.seed.collections?.reduce((sum, c) => sum + (c.fields?.length ?? 0), 0) ?? 0;
					if (totalFields > 0) {
						expect(result.fields.created).toBeGreaterThan(0);
					}

					// Verify content was created if seed has content
					if (fixture.seed.content) {
						const totalEntries = Object.values(fixture.seed.content).reduce(
							(sum, entries) => sum + (Array.isArray(entries) ? entries.length : 0),
							0,
						);
						if (totalEntries > 0) {
							expect(result.content.created).toBeGreaterThan(0);
						}
					}

					// Verify taxonomy processing completed (some may be pre-seeded by migrations)
					if (fixture.seed.taxonomies && fixture.seed.taxonomies.length > 0) {
						// Taxonomies either created or already existed — just verify no crash
						expect(result.taxonomies.created + result.taxonomies.terms).toBeGreaterThanOrEqual(0);
					}

					// Verify menus if present
					if (fixture.seed.menus && fixture.seed.menus.length > 0) {
						expect(result.menus.created).toBeGreaterThan(0);
					}
				} finally {
					await db.destroy();
				}
			});

			// --- CLI seed apply + doctor ---

			it("passes CLI doctor after seed apply", { timeout: 30_000 }, async () => {
				const tempDir = createTempDir();
				const dbPath = join(tempDir, "test.db");

				// Apply seed via CLI (this also runs migrations)
				await exec("node", [CLI_BIN, "seed", fixture.seedPath, "--database", dbPath], {
					cwd: fixture.dir,
					timeout: 30_000,
				});

				// Run doctor and verify all checks pass
				const { stdout } = await exec("node", [CLI_BIN, "doctor", "--database", dbPath, "--json"], {
					cwd: fixture.dir,
					timeout: 15_000,
				});

				const checks = JSON.parse(stdout) as Array<{
					name: string;
					status: "pass" | "warn" | "fail";
					message: string;
				}>;

				// No failures allowed
				const failures = checks.filter((c) => c.status === "fail");
				if (failures.length > 0) {
					expect.fail(
						`Doctor failures:\n${failures.map((f) => `  ${f.name}: ${f.message}`).join("\n")}`,
					);
				}

				// Database, migrations, and collections should all pass
				const dbCheck = checks.find((c) => c.name === "database");
				expect(dbCheck?.status).toBe("pass");

				const migrationsCheck = checks.find((c) => c.name === "migrations");
				expect(migrationsCheck?.status).toBe("pass");

				const collectionsCheck = checks.find((c) => c.name === "collections");
				expect(collectionsCheck?.status).toBe("pass");
			});

			// --- Idempotent re-apply ---

			it(
				"can re-apply seed with on-conflict=skip without errors",
				{ timeout: 30_000 },
				async () => {
					const tempDir = createTempDir();
					const dbPath = join(tempDir, "test.db");
					const uploadsDir = join(tempDir, "uploads");
					mkdirSync(uploadsDir, { recursive: true });

					const db = createDatabase({ url: `file:${dbPath}` });

					try {
						await runMigrations(db);

						const storage = new LocalStorage({
							directory: uploadsDir,
							baseUrl: "/_emdash/api/media/file",
						});

						const seedOpts = {
							includeContent: true,
							onConflict: "skip" as const,
							storage,
							seedDir: join(fixture.dir, ".emdash"),
						};

						// First apply
						await applySeed(db, fixture.seed, seedOpts);

						// Second apply — should not throw
						const result2 = await applySeed(db, fixture.seed, seedOpts);

						// Everything should be skipped on second apply
						expect(result2.collections.created).toBe(0);
					} finally {
						await db.destroy();
					}
				},
			);

			// --- package.json has emdash.seed pointing to seed file ---

			it("has package.json with emdash.seed pointing to the seed file", () => {
				const pkgPath = join(fixture.dir, "package.json");
				if (!existsSync(pkgPath)) return; // blank template has no seed, already filtered
				const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));

				// Either emdash.seed is set, or we rely on the .emdash/seed.json convention
				const seedRef = pkg.emdash?.seed;
				if (seedRef) {
					const resolvedSeedPath = resolve(fixture.dir, seedRef);
					expect(existsSync(resolvedSeedPath)).toBe(true);
				} else {
					// Convention: .emdash/seed.json exists (which it does since we're iterating fixtures)
					expect(existsSync(fixture.seedPath)).toBe(true);
				}
			});
		});
	}

	// -----------------------------------------------------------------------
	// Cross-cutting: all templates/demos have required files
	// -----------------------------------------------------------------------

	describe("Required files", () => {
		const roots = [
			{ prefix: "templates", dir: resolve(WORKSPACE_ROOT, "templates") },
			{ prefix: "demos", dir: resolve(WORKSPACE_ROOT, "demos") },
		].filter((root) => existsSync(root.dir));

		const allDirs = roots
			.flatMap((root) =>
				readdirSync(root.dir).map((entry) => ({
					name: `${root.prefix}/${entry}`,
					dir: join(root.dir, entry),
				})),
			)
			.filter((d) => existsSync(join(d.dir, "package.json")));

		for (const { name, dir } of allDirs) {
			describe(name, () => {
				it("has astro.config.mjs", () => {
					expect(existsSync(join(dir, "astro.config.mjs"))).toBe(true);
				});

				it("has tsconfig.json", () => {
					expect(existsSync(join(dir, "tsconfig.json"))).toBe(true);
				});

				it("has live.config.ts with emdashLoader", () => {
					const liveConfig = join(dir, "src", "live.config.ts");
					expect(existsSync(liveConfig)).toBe(true);

					const content = readFileSync(liveConfig, "utf-8");
					expect(content).toContain("emdashLoader");
					expect(content).toContain("defineLiveCollection");
				});

				it("has typecheck script in package.json", () => {
					const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
					expect(pkg.scripts?.typecheck || pkg.scripts?.check).toBeDefined();
				});

				it("uses workspace:* for emdash dependency", () => {
					const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
					expect(pkg.dependencies?.emdash).toBe("workspace:*");
				});

				it("uses catalog: for astro dependency", () => {
					const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
					const astroVersion = pkg.dependencies?.astro;
					expect(astroVersion).toBe("catalog:");
				});
			});
		}
	});
});
