'use strict';

// Flags: --expose-internals

const common = require('../common');
const stream = require('stream');
const REPL = require('internal/repl');
const assert = require('assert');
const fs = require('fs');
const { inspect } = require('util');

if (process.env.TERM === 'dumb') {
  common.skip('skipping - dumb terminal');
}

const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

process.throwDeprecation = true;

const defaultHistoryPath = tmpdir.resolve('.node_repl_history');

// Create an input stream specialized for testing an array of actions
class ActionStream extends stream.Stream {
  run(data) {
    const _iter = data[Symbol.iterator]();
    const doAction = () => {
      const next = _iter.next();
      if (next.done) {
        // Close the repl. Note that it must have a clean prompt to do so.
        this.emit('keypress', '', { ctrl: true, name: 'd' });
        return;
      }
      const action = next.value;

      if (typeof action === 'object') {
        this.emit('keypress', '', action);
      } else {
        this.emit('data', `${action}`);
      }
      setImmediate(doAction);
    };
    doAction();
  }
  resume() {}
  pause() {}
}
ActionStream.prototype.readable = true;

// Mock keys
const ENTER = { name: 'enter' };
const UP = { name: 'up' };
const DOWN = { name: 'down' };
const LEFT = { name: 'left' };
const RIGHT = { name: 'right' };
const BACKSPACE = { name: 'backspace' };
const TABULATION = { name: 'tab' };
const WORD_LEFT = { name: 'left', ctrl: true };
const WORD_RIGHT = { name: 'right', ctrl: true };
const GO_TO_END = { name: 'end' };
const SIGINT = { name: 'c', ctrl: true };
const ESCAPE = { name: 'escape', meta: true };

const prompt = '> ';

const tests = [
  {
    env: { NODE_REPL_HISTORY: defaultHistoryPath },
    test: (function*() {
      // Deleting Array iterator should not break history feature.
      //
      // Using a generator function instead of an object to allow the test to
      // keep iterating even when Array.prototype[Symbol.iterator] has been
      // deleted.
      yield 'const ArrayIteratorPrototype =';
      yield '  Object.getPrototypeOf(Array.prototype[Symbol.iterator]());';
      yield ENTER;
      yield 'const {next} = ArrayIteratorPrototype;';
      yield ENTER;
      yield 'const realArrayIterator = Array.prototype[Symbol.iterator];';
      yield ENTER;
      yield 'delete Array.prototype[Symbol.iterator];';
      yield ENTER;
      yield 'delete ArrayIteratorPrototype.next;';
      yield ENTER;
      yield UP;
      yield UP;
      yield DOWN;
      yield DOWN;
      yield 'fu';
      yield 'n';
      yield RIGHT;
      yield BACKSPACE;
      yield LEFT;
      yield LEFT;
      yield 'A';
      yield BACKSPACE;
      yield GO_TO_END;
      yield BACKSPACE;
      yield WORD_LEFT;
      yield WORD_RIGHT;
      yield ESCAPE;
      yield ENTER;
      yield 'require("./';
      yield TABULATION;
      yield SIGINT;
      yield 'import("./';
      yield TABULATION;
      yield SIGINT;
      yield 'Array.proto';
      yield RIGHT;
      yield '.pu';
      yield ENTER;
      yield 'ArrayIteratorPrototype.next = next;';
      yield ENTER;
      yield 'Array.prototype[Symbol.iterator] = realArrayIterator;';
      yield ENTER;
    })(),
    expected: [],
    clean: false
  },
];
const numtests = tests.length;

const runTestWrap = common.mustCall(runTest, numtests);

function cleanupTmpFile() {
  try {
    // Write over the file, clearing any history
    fs.writeFileSync(defaultHistoryPath, '');
  } catch (err) {
    if (err.code === 'ENOENT') return true;
    throw err;
  }
  return true;
}

function runTest() {
  const opts = tests.shift();
  if (!opts) return; // All done

  const { expected, skip } = opts;

  // Test unsupported on platform.
  if (skip) {
    setImmediate(runTestWrap, true);
    return;
  }
  const lastChunks = [];
  let i = 0;

  REPL.createInternalRepl(opts.env, {
    input: new ActionStream(),
    output: new stream.Writable({
      write: common.mustCallAtLeast((chunk, _, next) => {
        const output = chunk.toString();

        if (!opts.showEscapeCodes &&
            (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) {
          return next();
        }

        lastChunks.push(output);

        if (expected.length && !opts.checkTotal) {
          try {
            assert.strictEqual(output, expected[i]);
          } catch (e) {
            console.error(`Failed test # ${numtests - tests.length}`);
            console.error('Last outputs: ' + inspect(lastChunks, {
              breakLength: 5, colors: true
            }));
            throw e;
          }
          // TODO(BridgeAR): Auto close on last chunk!
          i++;
        }

        next();
      }),
    }),
    allowBlockingCompletions: true,
    completer: opts.completer,
    prompt,
    useColors: false,
    preview: opts.preview,
    terminal: true
  }, common.mustCall((err, repl) => {
    if (err) {
      console.error(`Failed test # ${numtests - tests.length}`);
      throw err;
    }

    repl.once('close', common.mustCall(() => {
      if (opts.clean)
        cleanupTmpFile();

      if (opts.checkTotal) {
        assert.deepStrictEqual(lastChunks, expected);
      } else if (expected.length !== i) {
        console.error(tests[numtests - tests.length - 1]);
        throw new Error(`Failed test # ${numtests - tests.length}`);
      }

      setImmediate(runTestWrap, true);
    }));

    if (opts.columns) {
      Object.defineProperty(repl, 'columns', {
        value: opts.columns,
        enumerable: true
      });
    }
    repl.input.run(opts.test);
  }));
}

// run the tests
runTest();
