/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {tmpdir} from 'os';
import * as path from 'path';
import {makeProjectConfig} from '@jest/test-utils';
import type {Config} from '@jest/types';
import type Resolver from 'jest-resolve';
import {buildSnapshotResolver} from 'jest-snapshot';
import {DependencyResolver} from '../index';

const maxWorkers = 1;
let dependencyResolver: DependencyResolver;
let runtimeContextResolver: Resolver;
let config: Config.ProjectConfig;
const cases: Record<string, (path: string) => boolean> = {
  fancyCondition: path => path.length > 10,
  testRegex: path => /.test.js$/.test(path),
};
const filter = (path: string) =>
  Object.keys(cases).every(key => cases[key](path));

beforeEach(async () => {
  const Runtime = (require('jest-runtime') as typeof import('jest-runtime'))
    .default;
  config = makeProjectConfig({
    cacheDirectory: path.resolve(tmpdir(), 'jest-resolve-dependencies-test'),
    moduleDirectories: ['node_modules'],
    moduleNameMapper: [['^\\$asdf/(.*)$', '<rootDir>/$1']],
    /**
     * `rootDir` must be an absolute path
     * @See https://github.com/jestjs/jest/blob/76632c6ec3f56708ec5781158972a295d0cc9332/packages/jest-haste-map/src/lib/fast_path.ts#L19-L25
     */
    rootDir: process.cwd(),
    roots: ['./packages/jest-resolve-dependencies'],
  });
  const runtimeContext = await Runtime.createContext(config, {
    maxWorkers,
    watchman: false,
  });

  runtimeContextResolver = runtimeContext.resolver;
  dependencyResolver = new DependencyResolver(
    runtimeContext.resolver,
    runtimeContext.hasteFS,
    await buildSnapshotResolver(config),
  );
});

test('resolves no dependencies for non-existent path', () => {
  const resolved = dependencyResolver.resolve('/non/existent/path');
  expect(resolved).toHaveLength(0);
});

test('resolves dependencies for existing path', () => {
  const resolved = dependencyResolver.resolve(
    path.resolve(__dirname, '__fixtures__', 'file.js'),
  );
  expect(resolved).toEqual([
    expect.stringContaining('jest-resolve-dependencies'),
    expect.stringContaining('jest-regex-util'),
  ]);
});

test('includes the mocks of dependencies as dependencies', () => {
  const resolved = dependencyResolver.resolve(
    path.resolve(__dirname, '__fixtures__/hasMocked/file.test.js'),
  );

  expect(resolved).toEqual([
    expect.stringContaining(path.join('hasMocked', 'file.js')),
    expect.stringContaining(path.join('hasMocked', '__mocks__', 'file.js')),
    expect.stringContaining(path.join('__mocks__', 'fake-node-module.js')),
  ]);
});

test('resolves dependencies for scoped packages', () => {
  const resolved = dependencyResolver.resolve(
    path.resolve(__dirname, '__fixtures__', 'scoped.js'),
  );
  expect(resolved).toEqual([
    expect.stringContaining(path.join('@myorg', 'pkg')),
  ]);
});

test('resolves no inverse dependencies for empty paths set', () => {
  const paths = new Set<string>();
  const resolved = dependencyResolver.resolveInverse(paths, filter);
  expect(resolved).toHaveLength(0);
});

test('resolves no inverse dependencies for set of non-existent paths', () => {
  const paths = new Set(['/non/existent/path', '/another/one']);
  const resolved = dependencyResolver.resolveInverse(paths, filter);
  expect(resolved).toHaveLength(0);
});

test('resolves inverse dependencies for existing path', () => {
  const paths = new Set([path.resolve(__dirname, '__fixtures__/file.js')]);
  const resolved = dependencyResolver.resolveInverse(paths, filter);
  expect(resolved).toEqual([
    expect.stringContaining(
      path.join('__tests__', '__fixtures__', 'file.test.js'),
    ),
  ]);
});

test('resolves inverse dependencies of mock', () => {
  const paths = new Set([
    path.resolve(__dirname, '__fixtures__/hasMocked/__mocks__/file.js'),
  ]);
  const resolved = dependencyResolver.resolveInverse(paths, filter);

  expect(resolved).toEqual([
    expect.stringContaining(
      path.join('__tests__/__fixtures__/hasMocked/file.test.js'),
    ),
  ]);
});

test('resolves inverse dependencies from available snapshot', () => {
  const paths = new Set([
    path.resolve(__dirname, '__fixtures__/file.js'),
    path.resolve(__dirname, '__fixtures__/__snapshots__/related.test.js.snap'),
  ]);
  const resolved = dependencyResolver.resolveInverse(paths, filter);
  expect(resolved).toEqual(
    expect.arrayContaining([
      expect.stringContaining(
        path.join('__tests__', '__fixtures__', 'file.test.js'),
      ),
      expect.stringContaining(
        path.join('__tests__', '__fixtures__', 'related.test.js'),
      ),
    ]),
  );
});

test('resolves dependencies correctly when dependency resolution fails', () => {
  jest.spyOn(runtimeContextResolver, 'resolveModule').mockImplementation(() => {
    throw new Error('resolveModule has failed');
  });
  jest.spyOn(runtimeContextResolver, 'getMockModule').mockImplementation(() => {
    throw new Error('getMockModule has failed');
  });

  const resolved = dependencyResolver.resolve(
    path.resolve(__dirname, '__fixtures__', 'file.test.js'),
  );

  expect(resolved).toEqual([]);
});

test('resolves dependencies correctly when mock dependency resolution fails', () => {
  jest.spyOn(runtimeContextResolver, 'getMockModule').mockImplementation(() => {
    throw new Error('getMockModule has failed');
  });

  const resolved = dependencyResolver.resolve(
    path.resolve(__dirname, '__fixtures__', 'file.test.js'),
  );

  expect(resolved).toEqual([
    expect.stringContaining(path.join('__tests__', '__fixtures__', 'file.js')),
  ]);
});
