/**
 * Unit tests for the `webjs dev` Prisma-client preflight (#452).
 *
 * A bare `webjs dev` skips the npm `predev` hook (`prisma generate`), so a
 * Prisma app boots against an ungenerated client and crashes with no hint. The
 * preflight turns that into an actionable warning.
 *
 * These fixtures mirror the REAL `@prisma/client` layout, verified against an
 * actual Prisma 6 install, NOT a fabricated shim:
 *   - `@prisma/client/index.js` is a static re-export shim:
 *       module.exports = { ...require('.prisma/client/default') }
 *     It exists right after `npm install` and never carries the init-error
 *     string itself.
 *   - The GENERATED target `.prisma/client/index.js` (and `default.js`) is what
 *     differs by state: when UNGENERATED it is a stub whose `PrismaClient`
 *     constructor throws "did not initialize yet"; after `prisma generate` it is
 *     the real generated client with no such string.
 * So "ungenerated" is detected by the stub MARKER in the GENERATED target (or by
 * that target being absent), reached through Node resolution so a hoisted
 * monorepo resolves too. (The empirical end-to-end check against a real
 * `npm i @prisma/client` lives in the PR; these reproduce the same shapes.)
 */
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { usesPrisma, prismaClientState, prismaDevHint } from '../../lib/prisma-preflight.js';

/** Build a throwaway app dir from a {relpath: contents} map. */
function makeApp(files) {
  const dir = mkdtempSync(join(tmpdir(), 'webjs-prisma-'));
  for (const [rel, body] of Object.entries(files)) {
    const abs = join(dir, rel);
    mkdirSync(join(abs, '..'), { recursive: true });
    writeFileSync(abs, body);
  }
  return dir;
}

const SCHEMA = 'generator client {\n  provider = "prisma-client-js"\n}\n';
const PKG_WITH_PRISMA = JSON.stringify({ dependencies: { '@prisma/client': '^6.0.0' } });
const PKG_NO_PRISMA = JSON.stringify({ dependencies: { '@webjsdev/core': '*' } });

// The real `@prisma/client` re-export shim. Present in BOTH generated and
// ungenerated states, never carries the init-error string itself.
const CLIENT_SHIM = "module.exports = {\n  ...require('.prisma/client/default'),\n};\n";
// A minimal `@prisma/client/package.json` so Node resolves the package and its
// `.prisma/client` re-export the way the real install does.
const CLIENT_PKG_JSON = JSON.stringify({ name: '@prisma/client', version: '6.0.0', main: 'index.js' });

// The generated target in its two states.
const TARGET_STUB =
  '// generated stub before `prisma generate`\n' +
  'class PrismaClient {\n' +
  '  constructor() { throw new Error(\'@prisma/client did not initialize yet. ' +
  'Please run "prisma generate" and try to import it again.\'); }\n' +
  '}\n' +
  'module.exports = { PrismaClient };\n';
const TARGET_GENERATED =
  '/* !!! This is code generated by Prisma. Do not edit directly. !!! */\n' +
  'class PrismaClient {}\n' +
  'module.exports = { PrismaClient };\n';

/**
 * Lay down a realistic `@prisma/client` install under `nmRoot` (a node_modules
 * parent), with the generated `.prisma/client` target in the given state.
 * @param {string} nmRoot
 * @param {'stub'|'generated'|'none'} targetState
 */
function installClient(nmRoot, targetState) {
  const files = {
    'node_modules/@prisma/client/package.json': CLIENT_PKG_JSON,
    'node_modules/@prisma/client/index.js': CLIENT_SHIM,
    'node_modules/@prisma/client/default.js': CLIENT_SHIM,
  };
  if (targetState !== 'none') {
    const body = targetState === 'stub' ? TARGET_STUB : TARGET_GENERATED;
    files['node_modules/.prisma/client/index.js'] = body;
    files['node_modules/.prisma/client/default.js'] = body;
  }
  for (const [rel, body] of Object.entries(files)) {
    const abs = join(nmRoot, rel);
    mkdirSync(join(abs, '..'), { recursive: true });
    writeFileSync(abs, body);
  }
}

test('usesPrisma: true when a schema is present', () => {
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_NO_PRISMA });
  assert.equal(usesPrisma(dir), true);
  rmSync(dir, { recursive: true, force: true });
});

test('usesPrisma: true when @prisma/client is a dependency (no schema)', () => {
  const dir = makeApp({ 'package.json': PKG_WITH_PRISMA });
  assert.equal(usesPrisma(dir), true);
  rmSync(dir, { recursive: true, force: true });
});

test('usesPrisma: false for a non-Prisma app (the guard)', () => {
  const dir = makeApp({ 'package.json': PKG_NO_PRISMA });
  assert.equal(usesPrisma(dir), false);
  rmSync(dir, { recursive: true, force: true });
});

test('prismaDevHint: fires for the REAL installed-but-ungenerated layout (the #452 crash case)', () => {
  // @prisma/client installed (the re-export shim present), the generated
  // `.prisma/client` target still the init-error stub. This is the exact state
  // after `npm i @prisma/client` without `prisma generate`, and the one the old
  // shim-grep detection silently passed as `ok`.
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_WITH_PRISMA });
  installClient(dir, 'stub');
  assert.equal(prismaClientState(dir).status, 'missing');
  const hint = prismaDevHint(dir);
  assert.ok(hint, 'expected a hint');
  assert.match(hint, /npm run dev/);
  assert.match(hint, /webjs db generate/);
  assert.match(hint, /not generated/);
  rmSync(dir, { recursive: true, force: true });
});

test('prismaDevHint: fires when @prisma/client is declared but not installed at all', () => {
  // Dep in package.json, nothing under node_modules: unresolvable -> missing.
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_WITH_PRISMA });
  assert.equal(prismaClientState(dir).status, 'missing');
  assert.ok(prismaDevHint(dir));
  rmSync(dir, { recursive: true, force: true });
});

test('prismaDevHint: COUNTERFACTUAL - silent for a non-Prisma app', () => {
  // No schema, no @prisma/client. The preflight must NOT warn, or it nags every
  // app. This is the negative case that proves the guard is real.
  const dir = makeApp({ 'package.json': PKG_NO_PRISMA });
  assert.equal(prismaDevHint(dir), null);
  rmSync(dir, { recursive: true, force: true });
});

test('prismaDevHint: COUNTERFACTUAL - a GENERATED real client is silent (proves the marker check fires)', () => {
  // Same install as the crash case but the target is the GENERATED client (no
  // init-error string). If detection regressed to "target exists -> ok" OR
  // back to grepping the shim, the ungenerated case above would go silent; this
  // case proves the difference is the marker in the generated target, not mere
  // presence. Make the client newer than the schema so it is not flagged stale.
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_WITH_PRISMA });
  installClient(dir, 'generated');
  const future = Date.now() / 1000 + 60;
  utimesSync(join(dir, 'node_modules/.prisma/client/index.js'), future, future);
  utimesSync(join(dir, 'node_modules/.prisma/client/default.js'), future, future);
  assert.equal(prismaClientState(dir).status, 'ok');
  assert.equal(prismaDevHint(dir), null);
  rmSync(dir, { recursive: true, force: true });
});

test('prismaDevHint: flags a STALE client (schema newer than the generated client)', () => {
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_WITH_PRISMA });
  installClient(dir, 'generated');
  // Generated client OLD, schema NEW: a stale client.
  const past = Date.now() / 1000 - 3600;
  utimesSync(join(dir, 'node_modules/.prisma/client/index.js'), past, past);
  utimesSync(join(dir, 'node_modules/.prisma/client/default.js'), past, past);
  assert.equal(prismaClientState(dir).status, 'stale');
  const hint = prismaDevHint(dir);
  assert.ok(hint);
  assert.match(hint, /stale/);
  assert.match(hint, /npm run dev/);
  rmSync(dir, { recursive: true, force: true });
});

test('prismaClientState: a HOISTED monorepo (generated client at a parent node_modules) is NOT flagged', () => {
  // app under <root>/apps/web with NO local node_modules; @prisma/client and the
  // generated client live at <root>/node_modules. Resolution must walk up, so a
  // fully-working hoisted app gets no spurious hint (the Finding 2 false-positive).
  const root = mkdtempSync(join(tmpdir(), 'webjs-prisma-hoist-'));
  installClient(root, 'generated');
  const appDir = join(root, 'apps', 'web');
  mkdirSync(join(appDir, 'prisma'), { recursive: true });
  writeFileSync(join(appDir, 'package.json'), PKG_WITH_PRISMA);
  writeFileSync(join(appDir, 'prisma', 'schema.prisma'), SCHEMA);
  // Client newer than the app schema so the only thing under test is resolution.
  const future = Date.now() / 1000 + 60;
  utimesSync(join(root, 'node_modules/.prisma/client/index.js'), future, future);
  utimesSync(join(root, 'node_modules/.prisma/client/default.js'), future, future);

  assert.equal(prismaClientState(appDir).status, 'ok');
  assert.equal(prismaDevHint(appDir), null);
  rmSync(root, { recursive: true, force: true });
});

test('prismaClientState: a HOISTED app whose hoisted client is UNGENERATED still fires', () => {
  // The hoist resolution must not paper over a genuinely ungenerated client:
  // generated target at the parent is the stub -> still missing.
  const root = mkdtempSync(join(tmpdir(), 'webjs-prisma-hoist2-'));
  installClient(root, 'stub');
  const appDir = join(root, 'apps', 'web');
  mkdirSync(join(appDir, 'prisma'), { recursive: true });
  writeFileSync(join(appDir, 'package.json'), PKG_WITH_PRISMA);
  writeFileSync(join(appDir, 'prisma', 'schema.prisma'), SCHEMA);

  assert.equal(prismaClientState(appDir).status, 'missing');
  assert.ok(prismaDevHint(appDir));
  rmSync(root, { recursive: true, force: true });
});

test('prismaClientState: a custom-output client (package resolves, no .prisma/client target) is NOT nagged', () => {
  // A custom generator `output` puts the client somewhere we cannot cheaply
  // resolve. @prisma/client resolves but `.prisma/client` does not. Fall back to
  // `ok` rather than false-positive on a possibly-working app.
  const dir = makeApp({ 'prisma/schema.prisma': SCHEMA, 'package.json': PKG_WITH_PRISMA });
  installClient(dir, 'none');
  assert.equal(prismaClientState(dir).status, 'ok');
  assert.equal(prismaDevHint(dir), null);
  rmSync(dir, { recursive: true, force: true });
});
