// Shared scaffolding for the U5 deprecation tests in inspect.test.ts,
// layout.test.ts, and validate.test.ts: those commands all fail fast (via a
// mocked dynamic import) so the tests can assert the shared deprecation
// envelope (stderr notice, JSON `_meta.deprecated`) without needing a real
// project or headless Chrome.
//
// vi.mock factories are hoisted above imports, so each test file keeps its
// own thin `vi.mock("<path>", () => someFactory())` call (mocking a module
// path can't itself be shared across files) but delegates the factory body
// here.
import type { ArgsDef, CommandDef } from "citty";
import { runCommand } from "citty";
import { expect, vi } from "vitest";

const FAKE_PROJECT = {
  dir: "/fake-project",
  name: "fake-project",
  indexPath: "/fake-project/index.html",
};

export function resolveProjectMock() {
  return { resolveProject: vi.fn(() => FAKE_PROJECT) };
}

export function bundleToSingleHtmlFailureMock() {
  return {
    bundleToSingleHtml: vi.fn(async () => {
      throw new Error("bundling failed (test double)");
    }),
  };
}

export function lintProjectFailureMock() {
  return {
    lintProject: vi.fn(async () => {
      throw new Error("lint failed (test double)");
    }),
  };
}

/**
 * citty's `meta` is `Resolvable<CommandMeta>` (object | promise | thunk).
 * These test files always define it as a synchronous object literal, so
 * narrow to that shape instead of asserting it with `as`.
 */
export function metaDescription<T extends ArgsDef = ArgsDef>(command: CommandDef<T>): string {
  const meta = command.meta;
  if (meta && typeof meta === "object" && "description" in meta) {
    return String(meta.description ?? "");
  }
  throw new Error("expected a synchronous meta object");
}

/**
 * Run a command with stdout/stderr writes captured (and process.exit /
 * console.log stubbed so the run stays silent and non-terminating), and
 * return the captured text for the caller to assert on.
 */
export async function runAndCaptureStdio<T extends ArgsDef = ArgsDef>(
  command: CommandDef<T>,
  rawArgs: string[] = ["--json"],
): Promise<{ stderrText: string; stdoutText: string }> {
  const stderrWrites: string[] = [];
  const stdoutWrites: string[] = [];
  vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
    stderrWrites.push(String(chunk));
    return true;
  });
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: unknown) => {
    stdoutWrites.push(String(chunk));
    return true;
  });
  vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
  vi.spyOn(console, "log").mockImplementation(() => {});

  await runCommand(command, { rawArgs });

  return { stderrText: stderrWrites.join(""), stdoutText: stdoutWrites.join("") };
}

/**
 * Run a command with process.exit stubbed and console.log spied, returning
 * the first console.log call that looks like a JSON object (the `--json`
 * failure envelope). Callers assert on definedness/shape themselves, since
 * that differs slightly per call site.
 */
export async function runAndFindJsonLogCall<T extends ArgsDef = ArgsDef>(
  command: CommandDef<T>,
  rawArgs: string[] = ["--json"],
): Promise<unknown[] | undefined> {
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
  vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
  const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});

  await runCommand(command, { rawArgs });

  return logSpy.mock.calls.find(([arg]) => typeof arg === "string" && arg.trim().startsWith("{"));
}

/**
 * Convenience wrapper: parse the JSON envelope found by runAndFindJsonLogCall.
 * `parsed` is intentionally left as JSON.parse's inferred `any` (matching
 * every call site's prior inline `JSON.parse(...)` usage) rather than
 * annotated `unknown`, since callers assert directly into its shape
 * (`.ok`, `._meta.deprecated`) the same way the original inline tests did.
 */
export async function runAndParseJsonEnvelope<T extends ArgsDef = ArgsDef>(
  command: CommandDef<T>,
  rawArgs: string[] = ["--json"],
) {
  const jsonCall = await runAndFindJsonLogCall(command, rawArgs);
  expect(jsonCall).toBeDefined();
  const parsed = JSON.parse(String(jsonCall?.[0]));
  return { jsonCall, parsed };
}
