/**
 * @license
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { resolve } from 'path';
import { existsSync } from 'fs';
import { exec } from 'child-process-promise';
import chalk from 'chalk';
import simpleGit from 'simple-git/promise';
import { TestConfig } from './testConfig';
const root = resolve(__dirname, '../..');
const git = simpleGit(root);

export interface TestTask {
  pkgName: string;
  reason: TestReason;
}

export enum TestReason {
  Changed = 'changed',
  Dependent = 'dependent',
  Global = 'global'
}

export function createTestTask(
  pkgName: string,
  reason = TestReason.Global
): TestTask {
  return {
    pkgName,
    reason
  };
}

/**
 * Changes to these files warrant running all tests.
 */
const fullTestTriggerFiles = [
  // Global dependency changes.
  'yarn.lock',
  // Test/compile/lint configs.
  'config/karma.base.js',
  'config/mocha.browser.opts',
  'config/mocharc.node.js',
  'config/tsconfig.base.json',
  'config/webpack.test.js',
  'config/firestore.rules',
  'config/database.rules.json'
];

/**
 * These files trigger tests in other dirs
 */
const specialPaths = {
  'scripts/emulator-testing/emulators/firestore-emulator.ts': [
    '@firebase/firestore'
  ],
  'scripts/emulator-testing/emulators/database-emulator.ts': [
    '@firebase/database'
  ],
  'scripts/emulator-testing/emulators/emulator.ts': [
    '@firebase/firestore',
    '@firebase/database'
  ],
  'scripts/emulator-testing/firestore-test-runner.ts': ['@firebase/firestore'],
  'scripts/emulator-testing/database-test-runner.ts': ['@firebase/database'],
  'packages/firestore': ['firebase-firestore-integration-test'],
  'packages/messaging': ['firebase-messaging-integration-test']
};

/**
 * Identify modified packages that require tests.
 */
export async function getTestTasks(): Promise<TestTask[]> {
  const packageInfo: any[] = JSON.parse(
    (await exec('npx lerna la --json', { cwd: root })).stdout
  );

  const allPackageNames = packageInfo.map(info => info.name);

  const depGraph: { [key: string]: any } = JSON.parse(
    (await exec('npx lerna ls --graph', { cwd: root })).stdout
  );
  const diff = await git.diff(['--name-only', 'origin/master...HEAD']);
  const changedFiles = diff.split('\n');
  let testTasks: TestTask[] = [];
  for (const filename of changedFiles) {
    // Files that trigger full test suite.
    if (fullTestTriggerFiles.includes(filename)) {
      console.log(
        chalk`{blue Running all tests because ${filename} was modified.}`
      );
      // run tests in all packages
      testTasks = allPackageNames.map(pkgName => createTestTask(pkgName));
      break;
    }
    // Files outside a package dir that should trigger its tests.
    const specialPathKeys = Object.keys(specialPaths) as Array<
      keyof typeof specialPaths
    >;
    const matchingSpecialPaths = specialPathKeys.filter(path =>
      filename.startsWith(path)
    );
    for (const matchingSpecialPath of matchingSpecialPaths) {
      for (const targetPackage of specialPaths[matchingSpecialPath]) {
        if (!testTasks.find(t => t.pkgName === targetPackage)) {
          testTasks.push(createTestTask(targetPackage, TestReason.Dependent));
        }
      }
    }
    // Check for changed files inside package dirs.
    const match = filename.match('^(packages(-exp)?/[a-zA-Z0-9-]+)/.*');
    if (match && match[1]) {
      const pkgJsonPath = resolve(root, match[1], 'package.json');

      // skip projects that don't have package.json
      // It could happen when we rename a package or remove a package from the repo
      if (!existsSync(pkgJsonPath)) {
        continue;
      }

      const changedPkg = require(pkgJsonPath);
      if (changedPkg) {
        const changedPkgName = changedPkg.name;
        const task = testTasks.find(t => t.pkgName === changedPkgName);

        if (task) {
          task.reason = TestReason.Changed;
        } else {
          testTasks.push(createTestTask(changedPkgName, TestReason.Changed));
        }

        // Add packages that depend on it.
        for (const pkgName of Object.keys(depGraph)) {
          if (depGraph[pkgName].includes(changedPkg.name)) {
            const depData = packageInfo.find(item => item.name === pkgName);
            if (depData) {
              const depPkgJson = require(resolve(
                depData.location,
                'package.json'
              ));
              if (depPkgJson) {
                if (!testTasks.find(t => t.pkgName === depPkgJson.name)) {
                  testTasks.push(
                    createTestTask(depPkgJson.name, TestReason.Dependent)
                  );
                }
              }
            }
          }
        }
      }
    }
  }

  if (testTasks.length === 0) {
    console.log(
      chalk`{green No changes detected in any package. No test tasks created }`
    );
  }

  return testTasks;
}

export function filterTasks(
  tasks: TestTask[],
  { onlyIncludePackages, alwaysIncludePackages, ignorePackages }: TestConfig
): TestTask[] {
  let filteredTasks: TestTask[] = [];

  // `ignorePacakges` and `onlyIncludePackages` should not be defined at same time,
  // `ignorePacakges` will be ignored if that happens
  if (onlyIncludePackages) {
    filteredTasks = tasks.filter(t => onlyIncludePackages.includes(t.pkgName));
  } else if (ignorePackages) {
    filteredTasks = tasks.filter(t => !ignorePackages.includes(t.pkgName));
  }

  if (alwaysIncludePackages) {
    for (const pkg of alwaysIncludePackages) {
      if (!filteredTasks.find(t => t.pkgName === pkg)) {
        filteredTasks.push(createTestTask(pkg));
      }
    }
  }

  return filteredTasks;
}
