'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const assert = require('node:assert');
const fs = require('node:fs');
const { findPackageJSON } = require('node:module');
const path = require('node:path');
const { describe, it } = require('node:test');


describe('findPackageJSON', () => { // Throws when no arguments are provided
  it('should throw when no arguments are provided', () => {
    assert.throws(
      () => findPackageJSON(),
      { code: 'ERR_MISSING_ARGS' }
    );
  });

  it('should throw when parentLocation is invalid', () => {
    for (const invalid of [null, {}, [], Symbol(), () => {}, true, false, 1, 0]) {
      assert.throws(
        () => findPackageJSON('', invalid),
        { code: 'ERR_INVALID_ARG_TYPE' },
      );
    }
  });

  it('should accept a file URL (string), like from `import.meta.resolve()`', () => {
    const importMetaUrl = `${fixtures.fileURL('whatever.ext')}`;
    const specifierBase = './packages/root-types-field';
    assert.strictEqual(
      findPackageJSON(`${specifierBase}/index.js`, importMetaUrl),
      path.toNamespacedPath(fixtures.path(specifierBase, 'package.json')),
    );
  });

  it('should accept a file URL instance', () => {
    const importMetaUrl = fixtures.fileURL('whatever.ext');
    const specifierBase = './packages/root-types-field';
    assert.strictEqual(
      findPackageJSON(
        new URL(`${specifierBase}/index.js`, importMetaUrl),
        importMetaUrl,
      ),
      path.toNamespacedPath(fixtures.path(specifierBase, 'package.json')),
    );
  });

  it('should be able to crawl up (CJS)', () => {
    const pathToMod = fixtures.path('packages/nested/sub-pkg-cjs/index.js');
    const parentPkg = require(pathToMod);

    const pathToParent = path.toNamespacedPath(fixtures.path('packages/nested/package.json'));
    assert.strictEqual(parentPkg, pathToParent);
  });

  it('should be able to crawl up (ESM)', () => {
    const pathToMod = fixtures.path('packages/nested/sub-pkg-esm/index.js');
    const parentPkg = require(pathToMod).default; // This test is a CJS file

    const pathToParent = path.toNamespacedPath(fixtures.path('packages/nested/package.json'));
    assert.strictEqual(parentPkg, pathToParent);
  });

  it('can require via package.json', () => {
    const pathToMod = fixtures.path('packages/cjs-main-no-index/other.js');
    // `require()` falls back to package.json values like "main" to resolve when there is no index
    const answer = require(pathToMod);

    assert.strictEqual(answer, 43);
  });

  it('should be able to resolve both root and closest package.json', () => {
    tmpdir.refresh();

    fs.writeFileSync(tmpdir.resolve('entry.mjs'), `
      import { findPackageJSON } from 'node:module';
      import fs from 'node:fs';

      const readPJSON = (locus) => JSON.parse(fs.readFileSync(locus, 'utf8'));

      const { secretNumberPkgRoot } = readPJSON(findPackageJSON('pkg', import.meta.url));
      const { secretNumberSubfolder } = readPJSON(findPackageJSON(import.meta.resolve('pkg')));
      const { secretNumberSubfolder2 } = readPJSON(findPackageJSON(import.meta.resolve('pkg2')));
      const { secretNumberPkg2 } = readPJSON(findPackageJSON('pkg2', import.meta.url));

      console.log(secretNumberPkgRoot, secretNumberSubfolder, secretNumberSubfolder2, secretNumberPkg2);
    `);

    const secretNumberPkgRoot = Math.ceil(Math.random() * 999);
    const secretNumberSubfolder = Math.ceil(Math.random() * 999);
    const secretNumberSubfolder2 = Math.ceil(Math.random() * 999);
    const secretNumberPkg2 = Math.ceil(Math.random() * 999);

    fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder'), { recursive: true });
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg/subfolder/index.js'),
      '',
    );
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg/subfolder/package.json'),
      JSON.stringify({
        type: 'module',
        secretNumberSubfolder,
      }),
    );
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg/package.json'),
      JSON.stringify({
        name: 'pkg',
        exports: './subfolder/index.js',
        secretNumberPkgRoot,
      }),
    );

    fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder2'));
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg/subfolder2/package.json'),
      JSON.stringify({
        type: 'module',
        secretNumberSubfolder2,
      }),
    );
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg/subfolder2/index.js'),
      '',
    );

    fs.mkdirSync(tmpdir.resolve('node_modules/pkg2'));
    fs.writeFileSync(
      tmpdir.resolve('node_modules/pkg2/package.json'),
      JSON.stringify({
        name: 'pkg',
        main: tmpdir.resolve('node_modules/pkg/subfolder2/index.js'),
        secretNumberPkg2,
      }),
    );

    common.spawnPromisified(process.execPath, [tmpdir.resolve('entry.mjs')]).then(common.mustCall((result) => {
      console.error(result.stderr);
      console.log(result.stdout);
      assert.deepStrictEqual(result, {
        stdout: `${secretNumberPkgRoot} ${secretNumberSubfolder} ${secretNumberSubfolder2} ${secretNumberPkg2}\n`,
        stderr: '',
        code: 0,
        signal: null,
      });
    }));
  });

  it('should work within a loader', async () => {
    const specifierBase = './packages/root-types-field';
    const target = fixtures.fileURL(specifierBase, 'index.js');
    const foundPjsonPath = path.toNamespacedPath(fixtures.path(specifierBase, 'package.json'));
    const { code, stderr, stdout } = await common.spawnPromisified(process.execPath, [
      '--no-warnings',
      '--loader',
      [
        'data:text/javascript,',
        'import fs from "node:fs";',
        'import module from "node:module";',
        encodeURIComponent(`fs.writeSync(1, module.findPackageJSON(${JSON.stringify(target)}));`),
        'export const resolve = async (s, c, n) => n(s);',
      ].join(''),
      '--eval',
      'import "node:os";', // Can be anything that triggers the resolve hook chain
    ]);

    assert.strictEqual(stderr, '');
    assert.ok(stdout.includes(foundPjsonPath), stdout);
    assert.strictEqual(code, 0);
  });

  it('should work with an async resolve hook registered', async () => {
    const specifierBase = './packages/root-types-field';
    const target = fixtures.fileURL(specifierBase, 'index.js');
    const foundPjsonPath = path.toNamespacedPath(fixtures.path(specifierBase, 'package.json'));
    const { code, stderr, stdout } = await common.spawnPromisified(process.execPath, [
      '--no-warnings',
      '--loader',
      'data:text/javascript,export const resolve = async (s, c, n) => n(s);',
      '--print',
      `require("node:module").findPackageJSON(${JSON.stringify(target)})`,
    ]);

    assert.strictEqual(stderr, '');
    assert.ok(stdout.includes(foundPjsonPath), stdout);
    assert.strictEqual(code, 0);
  });

  it('should work when unrecognised keys are specified within the export', async () => {
    const specifierBase = './packages/unrecognised-export-keys';
    const target = fixtures.fileURL(specifierBase, 'index.js');
    const foundPjsonPath = path.toNamespacedPath(fixtures.path(specifierBase, 'package.json'));

    const { code, stderr, stdout } = await common.spawnPromisified(process.execPath, [
      '--print',
      `require("node:module").findPackageJSON(${JSON.stringify(target)})`,
    ]);

    assert.strictEqual(stderr, '');
    assert.ok(stdout.includes(foundPjsonPath), stdout);
    assert.strictEqual(code, 0);
  });
});
