/**
 * Integration test server helper.
 *
 * Bootstraps an isolated Astro dev server from a minimal fixture,
 * runs setup, seeds test data, and creates auth tokens. Each test
 * suite gets a fresh database and server process.
 *
 * Usage:
 *
 *   const ctx = await createTestServer({ port: 4399 });
 *   // ctx.client  — EmDashClient (devBypass auth)
 *   // ctx.token   — PAT bearer token for CLI tests
 *   // ctx.baseUrl — http://localhost:4399
 *   // ctx.cwd     — working directory of the running server
 *   await ctx.cleanup();
 */

import { execFile, spawn } from "node:child_process";
import { existsSync, mkdtempSync, rmSync, symlinkSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";

import { EmDashClient } from "../../src/client/index.js";

const execAsync = promisify(execFile);

// Test regex patterns
const SESSION_COOKIE_REGEX = /^([^;]+)/;

// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------

const FIXTURE_DIR = resolve(import.meta.dirname, "fixture");
// Borrow node_modules from demos/simple — it has all the deps we need
// and is maintained by pnpm workspace resolution.
const DONOR_NODE_MODULES = resolve(import.meta.dirname, "../../../../demos/simple/node_modules");

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface TestServerOptions {
	port: number;
	/** Server startup timeout in ms (default: 30_000) */
	timeout?: number;
	/** Seed test data after setup (default: true) */
	seed?: boolean;
}

export interface TestServerContext {
	/** Base URL of the running server */
	baseUrl: string;
	/** Working directory containing the fixture */
	cwd: string;
	/** EmDashClient authenticated via dev-bypass session */
	client: EmDashClient;
	/** PAT bearer token with full scopes (for CLI / raw fetch tests) */
	token: string;
	/** Seeded collection slugs */
	collections: string[];
	/** Seeded content IDs keyed by collection */
	contentIds: Record<string, string[]>;
	/** Session cookie string for raw fetch calls needing session auth */
	sessionCookie: string;
	/** Stop the server and remove the temp directory */
	cleanup: () => Promise<void>;
}

// ---------------------------------------------------------------------------
// Node.js version guard
// ---------------------------------------------------------------------------

/**
 * Astro requires Node.js >= 22.12.0. Call from a `beforeAll` to fail the
 * suite immediately when the environment is misconfigured rather than
 * silently skipping.
 */
export function assertNodeVersion(): void {
	const [major, minor] = process.versions.node.split(".").map(Number) as [number, number];
	const ok = major! > 22 || (major === 22 && minor! >= 12);
	if (!ok) {
		throw new Error(
			`Integration tests require Node.js >= 22.12.0 (running ${process.versions.node}). ` +
				`Update your Node version instead of skipping tests.`,
		);
	}
}

// ---------------------------------------------------------------------------
// Build guard
// ---------------------------------------------------------------------------

const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../..");
const CLI_BINARY = resolve(import.meta.dirname, "../../dist/cli/index.mjs");

let buildPromise: Promise<void> | null = null;

/**
 * Ensure the workspace is built before starting integration tests.
 * Runs `pnpm build` once (cached across test suites via module-level promise).
 * Skips if the CLI binary already exists.
 */
export function ensureBuilt(): Promise<void> {
	if (!buildPromise) {
		buildPromise = doBuild();
	}
	return buildPromise;
}

async function doBuild(): Promise<void> {
	if (existsSync(CLI_BINARY)) return;

	console.log("[integration] Built artifacts missing — running pnpm build...");
	await execAsync("pnpm", ["build"], {
		cwd: WORKSPACE_ROOT,
		timeout: 120_000,
	});
	console.log("[integration] Build complete.");
}

// ---------------------------------------------------------------------------
// Server lifecycle
// ---------------------------------------------------------------------------

async function waitForServer(url: string, timeoutMs: number): Promise<void> {
	const start = Date.now();
	while (Date.now() - start < timeoutMs) {
		try {
			const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
			// Any HTTP response (even 500) means the server is up.
			// We only keep waiting on connection errors (caught below).
			if (res.status > 0) return;
		} catch {
			// Server not ready yet — connection refused / timeout
		}
		await new Promise((r) => setTimeout(r, 500));
	}
	throw new Error(`Server at ${url} did not start within ${timeoutMs}ms`);
}

/**
 * Create an Astro dev server for integration testing.
 *
 * Runs the fixture in-place to avoid Astro virtual module resolution
 * issues with symlinked temp dirs. Uses a temp directory only for the
 * database file — source files stay at their real paths.
 */
export async function createTestServer(options: TestServerOptions): Promise<TestServerContext> {
	const { port, timeout = 60_000, seed = true } = options;
	const baseUrl = `http://localhost:${port}`;

	// --- 0. Ensure workspace is built ---
	await ensureBuilt();

	// --- 1. Run fixture in-place, temp dir only for DB ---
	const workDir = FIXTURE_DIR;
	const tempDataDir = mkdtempSync(join(tmpdir(), "emdash-integration-"));
	const dbPath = join(tempDataDir, "test.db");

	// Ensure node_modules symlink exists in the fixture dir.
	// Multiple test suites may race to create this — handle EEXIST gracefully.
	const fixtureNodeModules = join(FIXTURE_DIR, "node_modules");
	let createdSymlink = false;
	if (!existsSync(fixtureNodeModules)) {
		try {
			symlinkSync(DONOR_NODE_MODULES, fixtureNodeModules);
			createdSymlink = true;
		} catch (err: unknown) {
			if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
		}
	}

	// --- 2. Start dev server ---
	const astroBin = join(fixtureNodeModules, ".bin", "astro");
	const server = spawn(astroBin, ["dev", "--port", String(port)], {
		cwd: workDir,
		env: {
			...process.env,
			EMDASH_TEST_DB: `file:${dbPath}`,
		},
		stdio: "pipe",
	});

	// Always capture server output. Forward to stderr when DEBUG is set,
	// and always keep a ring buffer of the last 5 KB for error reporting.
	let serverOutput = "";
	const MAX_OUTPUT = 5000;
	function appendOutput(chunk: string): void {
		if (process.env.DEBUG) process.stderr.write(`[integration:${port}] ${chunk}`);
		serverOutput += chunk;
		if (serverOutput.length > MAX_OUTPUT * 2) {
			serverOutput = serverOutput.slice(-MAX_OUTPUT);
		}
	}
	server.stdout?.on("data", (data: Buffer) => appendOutput(data.toString()));
	server.stderr?.on("data", (data: Buffer) => appendOutput(data.toString()));

	// Track for cleanup
	let stopped = false;

	async function cleanup(): Promise<void> {
		if (stopped) return;
		stopped = true;

		server.kill("SIGTERM");
		await new Promise((r) => setTimeout(r, 1000));

		// Force kill if still alive
		if (!server.killed) {
			server.kill("SIGKILL");
			await new Promise((r) => setTimeout(r, 500));
		}

		// Remove temp data directory
		rmSync(tempDataDir, { recursive: true, force: true });

		// Remove symlink if we created it
		if (createdSymlink && existsSync(fixtureNodeModules)) {
			try {
				unlinkSync(fixtureNodeModules);
			} catch {}
		}
	}

	try {
		// --- 3. Wait for server to be ready ---
		await waitForServer(`${baseUrl}/_emdash/api/setup/dev-bypass`, timeout);

		// --- 4. Run setup + create PAT in one request ---
		// The ?token query param tells the dev-bypass endpoint to also
		// create a PAT with full scopes and return it in the response.
		const setupRes = await fetch(`${baseUrl}/_emdash/api/setup/dev-bypass?token=1`);
		if (!setupRes.ok) {
			const body = await setupRes.text().catch(() => "");
			throw new Error(`Setup bypass failed (${setupRes.status}): ${body}`);
		}
		const setupJson = (await setupRes.json()) as {
			data: { user: { id: string; email: string }; token?: string };
		};
		const setupData = setupJson.data;
		const token = setupData.token;
		if (!token) {
			throw new Error("Setup bypass did not return a PAT token");
		}

		// Extract session cookie for raw fetch calls that need session auth
		const setCookie = setupRes.headers.get("set-cookie");
		let sessionCookie = "";
		if (setCookie) {
			const match = setCookie.match(SESSION_COOKIE_REGEX);
			if (match) sessionCookie = match[1]!;
		}

		// --- 5. Create client authenticated via PAT ---
		const client = new EmDashClient({
			baseUrl,
			token,
		});

		// --- 8. Seed test data ---
		const collections: string[] = [];
		const contentIds: Record<string, string[]> = {};

		if (seed) {
			await seedTestData(client, collections, contentIds);
		}

		return {
			baseUrl,
			cwd: workDir,
			client,
			token,
			collections,
			contentIds,
			sessionCookie,
			cleanup,
		};
	} catch (error) {
		// Include server output in error for CI debugging
		const msg = error instanceof Error ? error.message : String(error);
		await cleanup();
		throw new Error(
			`${msg}\n\nServer output (last ${MAX_OUTPUT} chars):\n${serverOutput.slice(-MAX_OUTPUT)}`,
			{
				cause: error,
			},
		);
	}
}

// ---------------------------------------------------------------------------
// Seed data
// ---------------------------------------------------------------------------

/**
 * Seeds sample content into the test server.
 *
 * Collections and fields are created by the seed file
 * (fixture/.emdash/seed.json) during dev-bypass setup.
 * This function only creates content entries.
 *
 * Content:
 *   - posts: 3 items (2 published, 1 draft)
 *   - pages: 2 items (1 published, 1 draft)
 */
async function seedTestData(
	client: EmDashClient,
	collections: string[],
	contentIds: Record<string, string[]>,
): Promise<void> {
	collections.push("posts");
	collections.push("pages");

	const postIds: string[] = [];

	const post1 = await client.create("posts", {
		data: {
			title: "First Post",
			body: "Hello **world**. This is the first post.",
			excerpt: "The very first post",
		},
		slug: "first-post",
	});
	postIds.push(post1.id);
	await client.publish("posts", post1.id);

	const post2 = await client.create("posts", {
		data: {
			title: "Second Post",
			body: "A second post with a [link](https://example.com).",
			excerpt: "Another post",
		},
		slug: "second-post",
	});
	postIds.push(post2.id);
	await client.publish("posts", post2.id);

	const post3 = await client.create("posts", {
		data: {
			title: "Draft Post",
			body: "This post is still a draft.",
			excerpt: "Not published yet",
		},
		slug: "draft-post",
	});
	postIds.push(post3.id);

	contentIds["posts"] = postIds;

	const pageIds: string[] = [];

	const page1 = await client.create("pages", {
		data: {
			title: "About",
			body: "# About Us\n\nWe are a **test** fixture.",
		},
		slug: "about",
	});
	pageIds.push(page1.id);
	await client.publish("pages", page1.id);

	const page2 = await client.create("pages", {
		data: {
			title: "Contact",
			body: "Get in touch.",
		},
		slug: "contact",
	});
	pageIds.push(page2.id);

	contentIds["pages"] = pageIds;
}
