// @flow
(require('../../lib/git'): any).rebaseRepoMainline = jest.fn();

import {table} from 'table';
import {
  _clearCustomCacheDir as clearCustomCacheDir,
  _setCustomCacheDir as setCustomCacheDir,
} from '../../lib/cacheRepoUtils';

import {copyDir, mkdirp} from '../../lib/fileUtils';

import {
  add as gitAdd,
  commit as gitCommit,
  init as gitInit,
  setLocalConfig as gitConfig,
} from '../../lib/git';

import {fs, path} from '../../lib/node';

import {testProject} from '../../lib/TEST_UTILS';

import {run as install} from '../install';
import {run} from '../outdated';

const BASE_FIXTURE_ROOT = path.join(__dirname, '__outdated-fixtures__');

async function touchFile(filePath: string) {
  await fs.close(await fs.open(filePath, 'w'));
}

async function writePkgJson(filePath: string, pkgJson: {...}) {
  await fs.writeJson(filePath, pkgJson);
}

describe('outdated (command)', () => {
  describe('end-to-end tests', () => {
    const FIXTURE_ROOT = path.join(BASE_FIXTURE_ROOT, 'end-to-end');

    const FIXTURE_FAKE_CACHE_REPO_DIR = path.join(
      FIXTURE_ROOT,
      'fakeCacheRepo',
    );

    const origConsoleLog = console.log;
    const origConsoleError = console.error;

    beforeEach(() => {
      (console: any).log = jest.fn();
      (console: any).error = jest.fn();
    });

    afterEach(() => {
      (console: any).log = origConsoleLog;
      (console: any).error = origConsoleError;
    });

    async function fakeProjectEnv(
      runTest: (flowProjectDir: string) => Promise<void>,
    ) {
      return await testProject(async ROOT_DIR => {
        const FAKE_CACHE_DIR = path.join(ROOT_DIR, 'fakeCache');
        const FAKE_CACHE_REPO_DIR = path.join(FAKE_CACHE_DIR, 'repo');
        const FLOWPROJ_DIR = path.join(ROOT_DIR, 'flowProj');
        const FLOWTYPED_DIR = path.join(FLOWPROJ_DIR, 'flow-typed', 'npm');
        const FLOWTYPED_ENV_DIR = path.join(
          FLOWPROJ_DIR,
          'flow-typed',
          'environments',
        );

        await Promise.all([
          mkdirp(FAKE_CACHE_REPO_DIR),
          mkdirp(FLOWTYPED_DIR),
          mkdirp(FLOWTYPED_ENV_DIR),
        ]);

        await copyDir(FIXTURE_FAKE_CACHE_REPO_DIR, FAKE_CACHE_REPO_DIR);

        await gitInit(FAKE_CACHE_REPO_DIR),
          await Promise.all([
            gitConfig(FAKE_CACHE_REPO_DIR, 'user.name', 'Test Author'),
            gitConfig(FAKE_CACHE_REPO_DIR, 'user.email', 'test@flow-typed.org'),
          ]);
        await gitAdd(FAKE_CACHE_REPO_DIR, 'definitions');
        await gitCommit(FAKE_CACHE_REPO_DIR, 'FIRST');

        setCustomCacheDir(FAKE_CACHE_DIR);

        // $FlowExpectedError[method-unbinding]
        const origCWD = process.cwd;
        (process: any).cwd = () => FLOWPROJ_DIR;
        try {
          await runTest(FLOWPROJ_DIR);
        } finally {
          (process: any).cwd = origCWD;
          clearCustomCacheDir();
        }
      });
    }

    it('reports stub as outdated', () => {
      const fooStub = `// flow-typed signature: 57774713bd9f8b7a3059edf76e66a6e0
// flow-typed version: <<STUB>>/foo_v1.2.3/flow_v0.162.0

/**
 * This is an autogenerated libdef stub for:
 *
 *   'foo'
 *
 * Fill this stub out by replacing all the \`any\` types.
 *
 * Once filled out, we encourage you to share your work with the
 * community by sending a pull request to:
 * https://github.com/flowtype/flow-typed
 */

declare module 'foo' {
  declare module.exports: any;
}
`;

      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
            dependencies: {
              foo: '1.2.3',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
          touchFile(
            path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'),
          ),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'),
            fooStub,
          ),
        ]);

        await run({});

        expect(
          await Promise.all([
            fs.exists(
              path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'),
            ),
          ]),
        ).toEqual([true]);

        expect(console.log).toHaveBeenCalledWith(
          table([
            ['Name', 'Details'],
            [
              'foo',
              'A new libdef has been published to the registry replacing your stub install it with `flow-typed install foo`',
            ],
          ]),
        );
      });
    });

    it('reports outdated libdef as needing updates', () => {
      const fooLibdef = `// flow-typed signature: 9caf6a2fac36ca585677b99fd5bf5036
// flow-typed version: abc/foo_v1.x.x/flow_v0.162.0

declare module 'foo' {}`;

      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
            dependencies: {
              foo: '1.2.3',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
          touchFile(
            path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'),
          ),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'),
            fooLibdef,
          ),
        ]);

        await run({});

        expect(
          await Promise.all([
            fs.exists(
              path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'),
            ),
          ]),
        ).toEqual([true]);

        expect(console.log).toHaveBeenCalledWith(
          table([
            ['Name', 'Details'],
            [
              'foo',
              'This libdef does not match what we found in the registry, update it with `flow-typed update`',
            ],
          ]),
        );
      });
    });

    it('reports a recently installed libdef as update to date', () => {
      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
            dependencies: {
              foo: '1.2.3',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
        ]);

        await install({
          overwrite: false,
          verbose: false,
          skip: false,
          skipFlowRestart: true,
          explicitLibDefs: [],
        });

        (console: any).log.mockClear();

        await run({});

        expect(
          await Promise.all([
            fs.exists(
              path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'),
            ),
          ]),
        ).toEqual([true]);

        expect(console.log).toHaveBeenCalledWith(
          'All your lib defs are up to date!',
        );
      });
    });

    it('reports outdated env definitions as needing updates', () => {
      const fooLibdef = `// flow-typed signature: fa26c13e83581eea415de59d5f03e416
// flow-typed version: /jsx/flow_>=v0.83.x

declare module 'foo' {}`;

      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed', 'environments', 'jsx.js'),
            fooLibdef,
          ),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed.config.json'),
            '{ "env": ["jsx"] }',
          ),
        ]);

        await run({});

        expect(
          await Promise.all([
            fs.exists(
              path.join(FLOWPROJ_DIR, 'flow-typed', 'environments', 'jsx.js'),
            ),
          ]),
        ).toEqual([true]);

        expect(console.log).toHaveBeenCalledWith(
          table([
            ['Name', 'Details'],
            [
              'jsx',
              'This env definition does not match what we found in the registry, update it with `flow-typed update`',
            ],
          ]),
        );
      });
    });

    it('reports outdated env definitions which do not exist in the registry', () => {
      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed.config.json'),
            '{ "env": ["random"] }',
          ),
        ]);

        await run({});

        expect(console.log).toHaveBeenCalledWith(
          table([
            ['Name', 'Details'],
            [
              'random',
              'This env definition does not exist in the registry or there is no compatible definition for your version of flow',
            ],
          ]),
        );
      });
    });

    it('reports outdated env definition when it exists flow-typed.config.json and registry but has not been installed', () => {
      return fakeProjectEnv(async FLOWPROJ_DIR => {
        // Create some dependencies
        await Promise.all([
          touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')),
          writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
            name: 'test',
            devDependencies: {
              'flow-bin': '^0.162.0',
            },
          }),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')),
          mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')),
          fs.writeFile(
            path.join(FLOWPROJ_DIR, 'flow-typed.config.json'),
            '{ "env": ["jsx"] }',
          ),
        ]);

        await run({});

        expect(console.log).toHaveBeenCalledWith(
          table([
            ['Name', 'Details'],
            [
              'jsx',
              'This env def has not yet been installed try running `flow-typed install`',
            ],
          ]),
        );
      });
    });
  });
});
