'use strict';
const assert = require('assert');
const {existsSync, readFileSync, rmdirSync, rmSync = rmdirSync} = require('fs');
const MemFsEditor = require('mem-fs-editor');
const path = require('path');

const helpers = require('.');

const isObject = (object) =>
  typeof object === 'object' && object !== null && object !== undefined;

function convertArgs(args) {
  if (args.length > 1) {
    return [[...args]];
  }

  const arg = args[0];
  return Array.isArray(arg) ? arg : [arg];
}

/**
 * This class provides utilities for testing generated content.
 */

class RunResult {
  constructor(options = {cwd: process.cwd()}) {
    this.env = options.env;
    this.generator = options.generator;
    this.cwd = options.cwd;
    this.oldCwd = options.oldCwd;
    this.memFs = options.memFs;
    this.fs = this.memFs && MemFsEditor.create(this.memFs);
    this.mockedGenerators = options.mockedGenerators || {};
    this.options = options;
    if (this.memFs && !this.cwd) {
      throw new Error('CWD option is required for mem-fs tests');
    }
  }

  /**
   * Create another RunContext reusing the settings.
   * See helpers.create api
   */
  create(GeneratorOrNamespace, settings, envOptions) {
    return helpers.create(
      GeneratorOrNamespace,
      {
        ...this.options.settings,
        cwd: this.cwd,
        oldCwd: this.oldCwd,
        ...settings
      },
      {...this.options.envOptions, memFs: this.memFs, ...envOptions}
    );
  }

  /**
   * Return an object with fs changes.
   * @param {Function} filter - parameter forwarded to mem-fs-editor#dump
   * @returns {Object}
   */
  getSnapshot(filter) {
    return this.fs.dump(this.cwd, filter);
  }

  /**
   * Return an object with filenames with state.
   * @param {Function} filter - parameter forwarded to mem-fs-editor#dump
   * @returns {Object}
   */
  getStateSnapshot(filter) {
    const snapshot = this.getSnapshot(filter);
    Object.values(snapshot).forEach((dump) => {
      delete dump.contents;
    });
    return snapshot;
  }

  /**
   * Prints files names and contents from mem-fs
   * @param {...string} files - Files to print or empty for entire mem-fs
   * @returns {RunResult} this
   */
  dumpFiles(...files) {
    if (files.length === 0) {
      this.memFs.each((file) => {
        console.log(file.path);
        if (file.contents) {
          console.log(file.contents.toString('utf8'));
        }
      });
      return this;
    }

    files.forEach((file) => {
      console.log(this.fs.read(this._fileName(file)));
    });
    return this;
  }

  /**
   * Prints every file from mem-fs
   * @returns {RunResult} this
   */
  dumpFilenames() {
    this.memFs.each((file) => {
      console.log(file.path);
    });
    return this;
  }

  /**
   * Reverts to old cwd.
   * @returns {RunResult} this
   */
  restore() {
    process.chdir(this.oldCwd);
    return this;
  }

  /**
   * Deletes the test directory recursively.
   * @returns {RunResult} this
   */
  cleanup() {
    process.chdir(this.oldCwd);
    rmSync(this.cwd, {recursive: true});
    return this;
  }

  _fileName(filename) {
    if (path.isAbsolute(filename)) {
      return filename;
    }

    return path.join(this.cwd, filename);
  }

  _readFile(filename, json) {
    filename = this._fileName(filename);
    let file;
    if (this.fs) {
      file = this.fs.read(filename, 'utf8');
    } else {
      file = readFileSync(filename, 'utf8');
    }

    return json ? JSON.parse(file) : file;
  }

  _exists(filename) {
    filename = this._fileName(filename);
    if (this.fs) {
      return this.fs.exists(filename);
    }

    return existsSync(filename);
  }

  /**
   * Assert that a file exists
   * @param {String}       path     - path to a file
   * @example
   * result.assertFile('templates/user.hbs');
   *
   * @also
   *
   * Assert that each files in the array exists
   * @param {Array}         paths    - an array of paths to files
   * @example
   * result.assertFile(['templates/user.hbs', 'templates/user/edit.hbs']);
   */
  assertFile() {
    convertArgs(arguments).forEach((file) => {
      const here = this._exists(file);
      assert.ok(here, `${file}, no such file or directory`);
    });
  }

  /**
   * Assert that a file doesn't exist
   * @param {String}       file     - path to a file
   * @example
   * result.assertNoFile('templates/user.hbs');
   *
   * @also
   *
   * Assert that each of an array of files doesn't exist
   * @param {Array}         pairs    - an array of paths to files
   * @example
   * result.assertNoFile(['templates/user.hbs', 'templates/user/edit.hbs']);
   */
  assertNoFile() {
    convertArgs(arguments).forEach((file) => {
      const here = this._exists(file);
      assert.ok(!here, `${file} exists`);
    });
  }

  /**
   * Assert that a file's content matches a regex or string
   * @param {String}       file     - path to a file
   * @param {Regex|String} reg      - regex / string that will be used to search the file
   * @example
   * result.assertFileContent('models/user.js', /App\.User = DS\.Model\.extend/);
   * result.assertFileContent('models/user.js', 'App.User = DS.Model.extend');
   *
   * @also
   *
   * Assert that each file in an array of file-regex pairs matches its corresponding regex
   * @param {Array}         pairs    - an array of arrays, where each subarray is a [String, RegExp] pair
   * @example
   * var arg = [
   *   [ 'models/user.js', /App\.User = DS\.Model\.extend/ ],
   *   [ 'controllers/user.js', /App\.UserController = Ember\.ObjectController\.extend/ ]
   * ]
   * result.assertFileContent(arg);
   */

  assertFileContent() {
    convertArgs(arguments).forEach((pair) => {
      const file = pair[0];
      const regex = pair[1];
      this.assertFile(file);
      const body = this._readFile(file);

      let match = false;
      if (typeof regex === 'string') {
        match = body.includes(regex);
      } else {
        match = regex.test(body);
      }

      assert(match, `${file} did not match '${regex}'. Contained:\n\n${body}`);
    });
  }

  /**
   * Assert that a file's content is the same as the given string
   * @param {String}  file            - path to a file
   * @param {String}  expectedContent - the expected content of the file
   * @example
   * result.assertEqualsFileContent(
   *   'data.js',
   *   'const greeting = "Hello";\nexport default { greeting }'
   * );
   *
   * @also
   *
   * Assert that each file in an array of file-string pairs equals its corresponding string
   * @param {Array}   pairs           - an array of arrays, where each subarray is a [String, String] pair
   * @example
   * result.assertEqualsFileContent([
   *   ['data.js', 'const greeting = "Hello";\nexport default { greeting }'],
   *   ['user.js', 'export default {\n  name: 'Coleman',\n  age: 0\n}']
   * ]);
   */

  assertEqualsFileContent() {
    convertArgs(arguments).forEach((pair) => {
      const file = pair[0];
      const expectedContent = pair[1];
      this.assertFile(file);
      this.assertTextEqual(this._readFile(file), expectedContent);
    });
  }

  /**
   * Assert that a file's content does not match a regex / string
   * @param {String}       file     - path to a file
   * @param {Regex|String} reg      - regex / string that will be used to search the file
   * @example
   * result.assertNoFileContent('models/user.js', /App\.User = DS\.Model\.extend/);
   * result.assertNoFileContent('models/user.js', 'App.User = DS.Model.extend');
   *
   * @also
   *
   * Assert that each file in an array of file-regex pairs does not match its corresponding regex
   * @param {Array}         pairs    - an array of arrays, where each subarray is a [String, RegExp] pair
   * var arg = [
   *   [ 'models/user.js', /App\.User \ DS\.Model\.extend/ ],
   *   [ 'controllers/user.js', /App\.UserController = Ember\.ObjectController\.extend/ ]
   * ]
   * result.assertNoFileContent(arg);
   */

  assertNoFileContent() {
    convertArgs(arguments).forEach((pair) => {
      const file = pair[0];
      const regex = pair[1];
      this.assertFile(file);
      const body = this._readFile(file);

      if (typeof regex === 'string') {
        assert.ok(!body.includes(regex), `${file} matched '${regex}'.`);
        return;
      }

      assert.ok(!regex.test(body), `${file} matched '${regex}'.`);
    });
  }

  /**
   * Assert that two strings are equal after standardization of newlines
   * @param {String} value    - a string
   * @param {String} expected - the expected value of the string
   * @example
   * result.assertTextEqual('I have a yellow cat', 'I have a yellow cat');
   */

  assertTextEqual(value, expected) {
    const eol = (string) => string.replace(/\r\n/g, '\n');

    assert.equal(eol(value), eol(expected));
  }

  /**
   * Assert an object contains the provided keys
   * @param {Object} obj      Object that should match the given pattern
   * @param {Object} content  An object of key/values the object should contains
   */

  assertObjectContent(object, content) {
    Object.keys(content).forEach((key) => {
      if (isObject(content[key])) {
        this.assertObjectContent(object[key], content[key]);
        return;
      }

      assert.equal(object[key], content[key]);
    });
  }

  /**
   * Assert an object does not contain the provided keys
   * @param {Object} obj Object that should not match the given pattern
   * @param {Object} content An object of key/values the object should not contain
   */

  assertNoObjectContent(object, content) {
    Object.keys(content).forEach((key) => {
      if (isObject(content[key])) {
        this.assertNoObjectContent(object[key], content[key]);
        return;
      }

      assert.notEqual(object[key], content[key]);
    });
  }

  /**
   * Assert a JSON file contains the provided keys
   * @param {String} filename
   * @param {Object} content An object of key/values the file should contains
   */

  assertJsonFileContent(filename, content) {
    this.assertObjectContent(this._readFile(filename, true), content);
  }

  /**
   * Assert a JSON file does not contain the provided keys
   * @param {String} filename
   * @param {Object} content An object of key/values the file should not contain
   */

  assertNoJsonFileContent(filename, content) {
    this.assertNoObjectContent(this._readFile(filename, true), content);
  }
}

module.exports = RunResult;