/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {readFileSync} from 'fs';
import {join} from 'path';
import * as semver from 'semver';

import {getBranchPushMatcher} from '../../../utils/testing';
import {ActiveReleaseTrains} from '../../versioning/active-release-trains';
import * as npm from '../../versioning/npm-publish';
import {ReleaseTrain} from '../../versioning/release-trains';
import {ReleaseAction} from '../actions';
import {actions} from '../actions/index';
import {changelogPath} from '../constants';

import {getChangelogForVersion, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils';

describe('common release action logic', () => {
  const baseReleaseTrains: ActiveReleaseTrains = {
    releaseCandidate: null,
    next: new ReleaseTrain('master', parse('10.1.0-next.0')),
    latest: new ReleaseTrain('10.0.x', parse('10.0.1')),
  };

  describe('version computation', async () => {
    const testReleaseTrain: ActiveReleaseTrains = {
      releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.3')),
      next: new ReleaseTrain('master', parse('10.2.0-next.0')),
      latest: new ReleaseTrain('10.0.x', parse('10.0.1')),
    };

    it('should not modify release train versions and cause invalid other actions', async () => {
      const {releaseConfig, gitClient} = getTestingMocksForReleaseAction();
      const descriptions: string[] = [];

      for (const actionCtor of actions) {
        if (await actionCtor.isActive(testReleaseTrain)) {
          const action = new actionCtor(testReleaseTrain, gitClient, releaseConfig, testTmpDir);
          descriptions.push(await action.getDescription());
        }
      }

      expect(descriptions).toEqual([
        `Cut a first release-candidate for the feature-freeze branch (v10.1.0-rc.0).`,
        `Cut a new patch release for the "10.0.x" branch (v10.0.2).`,
        `Cut a new next pre-release for the "10.1.x" branch (v10.1.0-next.4).`,
        `Cut a new release for an active LTS branch (0 active).`
      ]);
    });
  });

  describe('build and publishing', () => {
    it('should support a custom NPM registry', async () => {
      const {repo, instance, releaseConfig} =
          setupReleaseActionForTesting(TestAction, baseReleaseTrains);
      const {version, branchName} = baseReleaseTrains.next;
      const tagName = version.format();
      const customRegistryUrl = 'https://custom-npm-registry.google.com';

      repo.expectBranchRequest(branchName, 'STAGING_SHA')
          .expectCommitRequest('STAGING_SHA', `release: cut the v${version} release`)
          .expectTagToBeCreated(tagName, 'STAGING_SHA')
          .expectReleaseToBeCreated(`v${version}`, tagName);

      // Set up a custom NPM registry.
      releaseConfig.publishRegistry = customRegistryUrl;

      await instance.testBuildAndPublish(version, branchName, 'latest');

      expect(npm.runNpmPublish).toHaveBeenCalledTimes(2);
      expect(npm.runNpmPublish)
          .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'latest', customRegistryUrl);
      expect(npm.runNpmPublish)
          .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, 'latest', customRegistryUrl);
    });
  });

  describe('changelog cherry-picking', () => {
    const {version, branchName} = baseReleaseTrains.latest;
    const fakeReleaseNotes = getChangelogForVersion(version.format());
    const forkBranchName = `changelog-cherry-pick-${version}`;

    it('should prepend fetched changelog', async () => {
      const {repo, fork, instance, testTmpDir} =
          setupReleaseActionForTesting(TestAction, baseReleaseTrains);

      // Expect the changelog to be fetched and return a fake changelog to test that
      // it is properly appended. Also expect a pull request to be created in the fork.
      repo.expectChangelogFetch(branchName, fakeReleaseNotes)
          .expectFindForkRequest(fork)
          .expectPullRequestToBeCreated('master', fork, forkBranchName, 200);

      // Simulate that the fork branch name is available.
      fork.expectBranchRequest(forkBranchName, null);

      await instance.testCherryPickWithPullRequest(version, branchName);

      const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
      expect(changelogContent).toEqual(`${fakeReleaseNotes}Existing changelog`);
    });

    it('should respect a custom release note extraction pattern', async () => {
      const {repo, fork, instance, testTmpDir, releaseConfig} =
          setupReleaseActionForTesting(TestAction, baseReleaseTrains);

      // Custom pattern matching changelog output sections grouped through
      // basic level-1 markdown headers (compared to the default anchor pattern).
      releaseConfig.extractReleaseNotesPattern = version =>
          new RegExp(`(# v${version} \\("[^"]+"\\).*?)(?:# v|$)`, 's');

      const customReleaseNotes = `# v${version} ("newton-kepler")\n\nNew Content!`;

      // Expect the changelog to be fetched and return a fake changelog to test that
      // it is properly appended. Also expect a pull request to be created in the fork.
      repo.expectChangelogFetch(branchName, customReleaseNotes)
          .expectFindForkRequest(fork)
          .expectPullRequestToBeCreated('master', fork, forkBranchName, 200);

      // Simulate that the fork branch name is available.
      fork.expectBranchRequest(forkBranchName, null);

      await instance.testCherryPickWithPullRequest(version, branchName);

      const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
      expect(changelogContent).toEqual(`${customReleaseNotes}\n\nExisting changelog`);
    });

    it('should print an error if release notes cannot be extracted', async () => {
      const {repo, fork, instance, testTmpDir, releaseConfig} =
          setupReleaseActionForTesting(TestAction, baseReleaseTrains);

      // Expect the changelog to be fetched and return a fake changelog to test that
      // it is properly appended. Also expect a pull request to be created in the fork.
      repo.expectChangelogFetch(branchName, `non analyzable changelog`)
          .expectFindForkRequest(fork)
          .expectPullRequestToBeCreated('master', fork, forkBranchName, 200);

      // Simulate that the fork branch name is available.
      fork.expectBranchRequest(forkBranchName, null);

      spyOn(console, 'error');

      await instance.testCherryPickWithPullRequest(version, branchName);

      expect(console.error)
          .toHaveBeenCalledWith(
              jasmine.stringMatching(`Could not cherry-pick release notes for v${version}`));
      expect(console.error)
          .toHaveBeenCalledWith(jasmine.stringMatching(
              `Please copy the release notes manually into the "master" branch.`));

      const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8');
      expect(changelogContent).toEqual(`Existing changelog`);
    });

    it('should push changes to a fork for creating a pull request', async () => {
      const {repo, fork, instance, gitClient} =
          setupReleaseActionForTesting(TestAction, baseReleaseTrains);

      // Expect the changelog to be fetched and return a fake changelog to test that
      // it is properly appended. Also expect a pull request to be created in the fork.
      repo.expectChangelogFetch(branchName, fakeReleaseNotes)
          .expectFindForkRequest(fork)
          .expectPullRequestToBeCreated('master', fork, forkBranchName, 200);

      // Simulate that the fork branch name is available.
      fork.expectBranchRequest(forkBranchName, null);

      await instance.testCherryPickWithPullRequest(version, branchName);

      expect(gitClient.pushed.length).toBe(1);
      expect(gitClient.pushed[0]).toEqual(getBranchPushMatcher({
        targetBranch: forkBranchName,
        targetRepo: fork,
        baseBranch: 'master',
        baseRepo: repo,
        expectedCommits: [{
          message: `docs: release notes for the v${version} release`,
          files: ['CHANGELOG.md'],
        }],
      }));
    });
  });
});

/**
 * Test release action that exposes protected units of the base
 * release action class. This allows us to add unit tests.
 */
class TestAction extends ReleaseAction {
  async getDescription() {
    return 'Test action';
  }

  async perform() {
    throw Error('Not implemented.');
  }

  async testBuildAndPublish(newVersion: semver.SemVer, publishBranch: string, distTag: string) {
    await this.buildAndPublish(newVersion, publishBranch, distTag);
  }

  async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) {
    await this.cherryPickChangelogIntoNextBranch(version, branch);
  }
}
