const path = require('path');
const test = require('ava');
const sh = require('shelljs');
const proxyquire = require('proxyquire');
const _ = require('lodash');
const sinon = require('sinon');
const Log = require('../lib/log');
const Spinner = require('../lib/spinner');
const Config = require('../lib/config');
const runTasks = require('../lib/tasks');
const Plugin = require('../lib/plugin/Plugin');
const { mkTmpDir, gitAdd, getArgs } = require('./util/helpers');
const ShellStub = require('./stub/shell');
const {
  interceptUser: interceptGitLabUser,
  interceptCollaborator: interceptGitLabCollaborator,
  interceptPublish: interceptGitLabPublish,
  interceptAsset: interceptGitLabAsset
} = require('./stub/gitlab');
const {
  interceptAuthentication: interceptGitHubAuthentication,
  interceptCollaborator: interceptGitHubCollaborator,
  interceptCreate: interceptGitHubCreate,
  interceptAsset: interceptGitHubAsset
} = require('./stub/github');

const noop = Promise.resolve();

const sandbox = sinon.createSandbox();

const testConfig = {
  ci: true,
  config: false,
  'disable-metrics': true
};

const log = sandbox.createStubInstance(Log);
const spinner = sandbox.createStubInstance(Spinner);
spinner.show.callsFake(({ enabled = true, task }) => (enabled ? task() : noop));

const getContainer = options => {
  const config = new Config(Object.assign({}, testConfig, options));
  const shell = new ShellStub({ container: { log, config } });
  return {
    log,
    spinner,
    config,
    shell
  };
};

test.serial.beforeEach(t => {
  const bare = mkTmpDir();
  const target = mkTmpDir();
  sh.pushd('-q', bare);
  sh.exec(`git init --bare .`);
  sh.exec(`git clone ${bare} ${target}`);
  sh.pushd('-q', target);
  gitAdd('line', 'file', 'Add file');
  t.context = { bare, target };
});

test.serial.afterEach(() => {
  sandbox.resetHistory();
});

test.serial('should run tasks without throwing errors', async t => {
  sh.mv('.git', 'foo');
  const { name, latestVersion, version } = await runTasks({}, getContainer());
  t.true(log.obtrusive.firstCall.args[0].includes(`release ${name} (${latestVersion}...${version})`));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
});

test.serial('should run tasks without package.json', async t => {
  sh.exec('git tag 1.0.0');
  gitAdd('line', 'file', 'Add file');
  const { name } = await runTasks({}, getContainer({ increment: 'major', git: { commit: false } }));
  t.true(log.obtrusive.firstCall.args[0].includes(`release ${name} (1.0.0...2.0.0)`));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
  t.is(log.warn.callCount, 0);
  {
    const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
    t.is(stdout.trim(), '2.0.0');
  }
});

test.serial('should disable plugins', async t => {
  gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
  sh.exec('git tag 1.2.3');
  gitAdd('line', 'file', 'Add file');
  const container = getContainer({ increment: 'minor', git: false, npm: false });
  const { latestVersion, version } = await runTasks({}, container);
  t.is(latestVersion, '0.0.0');
  t.is(version, '0.1.0');
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
});

test.serial('should run tasks with minimal config and without any warnings/errors', async t => {
  gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
  sh.exec('git tag 1.2.3');
  gitAdd('line', 'file', 'More file');
  await runTasks({}, getContainer({ increment: 'patch' }));
  t.true(log.obtrusive.firstCall.args[0].includes('release my-package (1.2.3...1.2.4)'));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
  const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(stdout.trim(), '1.2.4');
});

test.serial('should use pkg.version', async t => {
  gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
  await runTasks({}, getContainer({ increment: 'minor' }));
  t.true(log.obtrusive.firstCall.args[0].includes('release my-package (1.2.3...1.3.0)'));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
  const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(stdout.trim(), '1.3.0');
});

test.serial('should use pkg.version (in sub dir) w/o tagging repo', async t => {
  gitAdd('{"name":"root-package","version":"1.0.0"}', 'package.json', 'Add package.json');
  sh.exec('git tag 1.0.0');
  sh.mkdir('my-package');
  sh.pushd('-q', 'my-package');
  gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
  const container = getContainer({ increment: 'minor', git: { tag: false } });
  const exec = sinon.spy(container.shell, 'exec');
  await runTasks({}, container);
  t.true(log.obtrusive.firstCall.args[0].endsWith('release my-package (1.2.3...1.3.0)'));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
  const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(stdout.trim(), '1.0.0');
  const npmArgs = getArgs(exec.args, 'npm');
  t.is(npmArgs[4], 'npm version 1.3.0 --no-git-tag-version');
  exec.restore();
});

test.serial('should ignore version in pkg.version and use git tag instead', async t => {
  gitAdd('{"name":"my-package","version":"0.0.0"}', 'package.json', 'Add package.json');
  sh.exec('git tag 1.1.1');
  gitAdd('line', 'file', 'More file');
  await runTasks({}, getContainer({ increment: 'minor', npm: { ignoreVersion: true } }));
  t.true(log.obtrusive.firstCall.args[0].includes('release my-package (1.1.1...1.2.0)'));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);
  const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(stdout.trim(), '1.2.0');
});

test.serial('should release all the things (basic)', async t => {
  const { bare, target } = t.context;
  const project = path.basename(bare);
  const pkgName = path.basename(target);
  const owner = path.basename(path.dirname(bare));
  gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
  sh.exec('git tag 1.0.0');
  const sha = gitAdd('line', 'file', 'More file');

  interceptGitHubAuthentication();
  interceptGitHubCollaborator({ owner, project });
  interceptGitHubCreate({
    owner,
    project,
    body: { tag_name: '1.0.1', name: 'Release 1.0.1', body: `* More file (${sha})`, prerelease: false }
  });

  const container = getContainer({
    github: { release: true, pushRepo: `https://github.com/${owner}/${project}` },
    npm: { name: pkgName }
  });
  const exec = sinon.spy(container.shell, 'exec');

  await runTasks({}, container);

  const npmArgs = getArgs(container.shell.exec.args, 'npm');

  t.deepEqual(npmArgs, [
    'npm ping',
    'npm whoami',
    `npm show ${pkgName}@latest version`,
    `npm access ls-collaborators ${pkgName}`,
    'npm version 1.0.1 --no-git-tag-version',
    'npm publish . --tag latest'
  ]);

  t.true(log.obtrusive.firstCall.args[0].endsWith(`release ${pkgName} (1.0.0...1.0.1)`));
  t.true(log.log.firstCall.args[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
  t.true(log.log.secondCall.args[0].endsWith(`https://github.com/${owner}/${project}/releases/tag/1.0.1`));

  exec.restore();
});

test.serial('should release all the things (pre-release, github, gitlab)', async t => {
  const { bare, target } = t.context;
  const project = path.basename(bare);
  const pkgName = path.basename(target);
  const owner = path.basename(path.dirname(bare));
  const url = `https://gitlab.com/${owner}/${project}`;
  gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
  sh.exec('git tag v1.0.0');
  const sha = gitAdd('line', 'file', 'More file');
  sh.exec('git push --follow-tags');

  interceptGitHubAuthentication();
  interceptGitHubCollaborator({ owner, project });
  interceptGitHubCreate({
    owner,
    project,
    body: {
      tag_name: 'v1.1.0-alpha.0',
      name: 'Release 1.1.0-alpha.0',
      body: `Notes for ${pkgName} [v1.1.0-alpha.0]: ${sha}`,
      prerelease: true
    }
  });
  interceptGitHubAsset({ owner, project, body: 'lineline' });

  interceptGitLabUser({ owner });
  interceptGitLabCollaborator({ owner, project });
  interceptGitLabAsset({ owner, project });
  interceptGitLabPublish({
    owner,
    project,
    body: {
      name: 'Release 1.1.0-alpha.0',
      tag_name: 'v1.1.0-alpha.0',
      description: `Notes for ${pkgName}: ${sha}`,
      assets: {
        links: [
          {
            name: 'file',
            url: `${url}/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file`
          }
        ]
      }
    }
  });

  const container = getContainer({
    increment: 'minor',
    preRelease: 'alpha',
    git: {
      changelog: 'git log --pretty=format:%h ${latestTag}...HEAD',
      commitMessage: 'Release ${version} for ${name} (from ${latestVersion})',
      tagAnnotation: '${repo.owner} ${repo.repository} ${repo.project}'
    },
    github: {
      release: true,
      pushRepo: `https://github.com/${owner}/${project}`,
      releaseNotes: 'echo Notes for ${name} [v${version}]: ${changelog}',
      assets: ['file']
    },
    gitlab: {
      release: true,
      pushRepo: url,
      releaseNotes: 'echo Notes for ${name}: ${changelog}',
      assets: ['file']
    },
    npm: { name: pkgName }
  });

  const exec = sinon.spy(container.shell, 'exec');

  await runTasks({}, container);

  const npmArgs = getArgs(container.shell.exec.args, 'npm');
  t.deepEqual(npmArgs, [
    'npm ping',
    'npm whoami',
    `npm show ${pkgName}@latest version`,
    `npm access ls-collaborators ${pkgName}`,
    'npm version 1.1.0-alpha.0 --no-git-tag-version',
    'npm publish . --tag alpha'
  ]);

  const { stdout: commitMessage } = sh.exec('git log --oneline --format=%B -n 1 HEAD');
  t.is(commitMessage.trim(), `Release 1.1.0-alpha.0 for ${pkgName} (from 1.0.0)`);

  const { stdout: tagName } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(tagName.trim(), 'v1.1.0-alpha.0');

  const { stdout: tagAnnotation } = sh.exec('git for-each-ref refs/tags/v1.1.0-alpha.0 --format="%(contents)"');
  t.is(tagAnnotation.trim(), `${owner} ${owner}/${project} ${project}`);

  t.true(log.obtrusive.firstCall.args[0].endsWith(`release ${pkgName} (1.0.0...1.1.0-alpha.0)`));
  t.true(log.log.firstCall.args[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
  t.true(log.log.secondCall.args[0].endsWith(`https://github.com/${owner}/${project}/releases/tag/v1.1.0-alpha.0`));
  t.true(log.log.thirdCall.args[0].endsWith(`${project}/-/releases`));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);

  exec.restore();
});

test.serial('should publish pre-release without pre-id with different npm.tag', async t => {
  const { target } = t.context;
  const pkgName = path.basename(target);
  gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
  sh.exec('git tag v1.0.0');

  const container = getContainer({ increment: 'major', preRelease: true, npm: { name: pkgName, tag: 'next' } });
  const exec = sinon.spy(container.shell, 'exec');

  await runTasks({}, container);

  const npmArgs = getArgs(container.shell.exec.args, 'npm');
  t.deepEqual(npmArgs, [
    'npm ping',
    'npm whoami',
    `npm show ${pkgName}@latest version`,
    `npm access ls-collaborators ${pkgName}`,
    'npm version 2.0.0-0 --no-git-tag-version',
    'npm publish . --tag next'
  ]);

  const { stdout } = sh.exec('git describe --tags --match=* --abbrev=0');
  t.is(stdout.trim(), 'v2.0.0-0');
  t.true(log.obtrusive.firstCall.args[0].endsWith(`release ${pkgName} (1.0.0...2.0.0-0)`));
  t.true(log.log.firstCall.args[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
  t.regex(log.log.lastCall.args[0], /Done \(in [0-9]+s\.\)/);

  exec.restore();
});

test.serial('should handle private package correctly, bump lockfile', async t => {
  const { target } = t.context;
  const pkgName = path.basename(target);
  gitAdd(`{"name":"${pkgName}","version":"1.0.0","private":true}`, 'package.json', 'Add package.json');
  gitAdd(`{"name":"${pkgName}","version":"1.0.0","private":true}`, 'package-lock.json', 'Add package-lock.json');

  const container = getContainer({ npm: { name: pkgName, private: true } });
  const exec = sinon.spy(container.shell, 'exec');

  await runTasks({}, container);

  const npmArgs = getArgs(container.shell.exec.args, 'npm');
  t.deepEqual(npmArgs, ['npm version 1.0.1 --no-git-tag-version']);
  t.true(log.obtrusive.firstCall.args[0].endsWith(`release ${pkgName} (1.0.0...1.0.1)`));
  t.is(log.warn.lastCall.args[0], 'Skip publish: package is private.');
  t.regex(log.log.firstCall.args[0], /Done \(in [0-9]+s\.\)/);

  exec.restore();
});

test.serial('should initially publish non-private scoped npm package privately', async t => {
  const { target } = t.context;
  const pkgName = path.basename(target);
  gitAdd(`{"name":"@scope/${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');

  const container = getContainer({ npm: { name: pkgName } });

  const exec = sinon.stub(container.shell, 'exec').callThrough();
  exec.withArgs(`npm show @scope/${pkgName}@latest version`).rejects();

  await runTasks({}, container);

  const npmArgs = getArgs(container.shell.exec.args, 'npm');
  t.is(npmArgs[5], 'npm publish . --tag latest');
  exec.restore();
});

test.serial('should use pkg.publishConfig.registry', async t => {
  const { target } = t.context;
  const pkgName = path.basename(target);
  const registry = 'https://my-registry.com';

  gitAdd(
    JSON.stringify({
      name: pkgName,
      version: '1.2.3',
      publishConfig: { registry }
    }),
    'package.json',
    'Add package.json'
  );

  const container = getContainer();

  const exec = sinon.spy(container.shell, 'exec');

  await runTasks({}, container);

  const npmArgs = getArgs(exec.args, 'npm');
  t.is(npmArgs[0], `npm ping --registry ${registry}`);
  t.is(npmArgs[1], `npm whoami --registry ${registry}`);
  t.true(container.log.log.firstCall.args[0].endsWith(`${registry}/package/${pkgName}`));

  exec.restore();
});

test.serial('should propagate errors', async t => {
  const config = {
    hooks: {
      'before:init': 'some-failing-command'
    }
  };
  const container = getContainer(config);

  await t.throwsAsync(runTasks({}, container), { message: /some-failing-command/ });

  t.is(log.error.callCount, 1);
});

{
  class MyPlugin extends Plugin {}
  const statics = { isEnabled: () => true, disablePlugin: () => null };
  const options = { '@global': true, '@noCallThru': true };
  const runTasks = proxyquire('../lib/tasks', {
    'my-plugin': Object.assign(MyPlugin, statics, options)
  });

  test.serial('should run all hooks', async t => {
    gitAdd(`{"name":"hooked","version":"1.0.0"}`, 'package.json', 'Add package.json');
    const hooks = {};
    ['before', 'after'].forEach(prefix => {
      ['version', 'git', 'npm', 'my-plugin'].forEach(ns => {
        ['init', 'beforeBump', 'bump', 'beforeRelease', 'release', 'afterRelease'].forEach(cycle => {
          hooks[`${prefix}:${cycle}`] = `echo ${prefix}:${cycle}`;
          hooks[`${prefix}:${ns}:${cycle}`] = `echo ${prefix}:${ns}:${cycle}`;
        });
      });
    });
    const container = getContainer({ plugins: { 'my-plugin': {} }, hooks });
    const exec = sinon.spy(container.shell, 'execFormattedCommand');

    await runTasks({}, container);

    const commands = _.flatten(exec.args).filter(arg => typeof arg === 'string' && arg.startsWith('echo'));

    t.deepEqual(commands, [
      'echo before:init',
      'echo before:my-plugin:init',
      'echo after:my-plugin:init',
      'echo before:npm:init',
      'echo after:npm:init',
      'echo before:git:init',
      'echo after:git:init',
      'echo before:version:init',
      'echo after:version:init',
      'echo after:init',
      'echo before:beforeBump',
      'echo before:my-plugin:beforeBump',
      'echo after:my-plugin:beforeBump',
      'echo before:npm:beforeBump',
      'echo after:npm:beforeBump',
      'echo before:git:beforeBump',
      'echo after:git:beforeBump',
      'echo before:version:beforeBump',
      'echo after:version:beforeBump',
      'echo after:beforeBump',
      'echo before:bump',
      'echo before:my-plugin:bump',
      'echo after:my-plugin:bump',
      'echo before:npm:bump',
      'echo after:npm:bump',
      'echo before:git:bump',
      'echo after:git:bump',
      'echo before:version:bump',
      'echo after:version:bump',
      'echo after:bump',
      'echo before:beforeRelease',
      'echo before:my-plugin:beforeRelease',
      'echo after:my-plugin:beforeRelease',
      'echo before:npm:beforeRelease',
      'echo after:npm:beforeRelease',
      'echo before:git:beforeRelease',
      'echo after:git:beforeRelease',
      'echo before:version:beforeRelease',
      'echo after:version:beforeRelease',
      'echo after:beforeRelease',
      'echo before:release',
      'echo before:npm:release',
      'echo after:npm:release',
      'echo before:git:release',
      'echo after:git:release',
      'echo before:version:release',
      'echo after:version:release',
      'echo before:my-plugin:release',
      'echo after:my-plugin:release',
      'echo after:release',
      'echo before:afterRelease',
      'echo before:npm:afterRelease',
      'echo after:npm:afterRelease',
      'echo before:git:afterRelease',
      'echo after:git:afterRelease',
      'echo before:version:afterRelease',
      'echo after:version:afterRelease',
      'echo before:my-plugin:afterRelease',
      'echo after:my-plugin:afterRelease',
      'echo after:afterRelease'
    ]);

    exec.restore();
  });
}
