/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.debug.TraceTest');
goog.setTestOnly();

const StopTraceDetail = goog.requireType('goog.debug.StopTraceDetail');
const Trace = goog.require('goog.debug.Trace');
const googArray = goog.require('goog.array');
const recordFunction = goog.require('goog.testing.recordFunction');
const testSuite = goog.require('goog.testing.testSuite');

/** @type {!Function} */
const recorder = recordFunction();
/** @const {!StopTraceDetail} */
const TRACE_CANCELLED = {
  wasCancelled: true,
};
/** @const {!StopTraceDetail} */
const NORMAL_STOP = {};

/**
 * Checks if the actual log of a fake listener matches the expectations.
 * @param {!Array<!Array<*>>} expected The expected log from the fake listener.
 * @param {!Function} recorder The output of `recordFunction` for logging all
 *     calls to the fake listener.
 * @return {boolean} True if the recorder's log is expected.
 * @suppress {strictMissingProperties,missingReturn} suppression added to enable
 * type checking
 */
function validateRecordedListener(expected, recorder) {
  assertObjectEquals(
      expected,
      googArray.map(recorder.getCalls(), (call) => call.getArguments()));
}

testSuite({
  /**
     @suppress {strictMissingProperties,checkTypes} suppression added to enable
     type checking
   */
  setUp() {
    Trace.initCurrentTrace();
    Trace.removeAllListeners();
    recorder.reset();
  },

  testProperEventReleaseViaResetForComment() {
    Trace.startTracer('foo');
    // The Start event and its id are released due to calling the reset method.
    Trace.clearCurrentTrace();

    // Recycling the last Start event.
    Trace.addComment('abc');
    Trace.startTracer('foo');
    Trace.clearCurrentTrace();

    const t1 = Trace.startTracer('f1');
    const t2 = Trace.startTracer('f2');
    assertNotEquals('The trace ids cannot repeat.', t1, t2);
  },

  testProperEventReleaseViaThresholdForComment() {
    const t3 = Trace.startTracer('foo');
    // The Start event and its id are released due to 1000ms threshold.
    Trace.stopTracer(t3, 1000);

    // Recycling the last Start event.
    Trace.addComment('abc');
    Trace.startTracer('foo');
    Trace.clearCurrentTrace();

    const t1 = Trace.startTracer('f1');
    const t2 = Trace.startTracer('f2');
    assertNotEquals('The trace ids cannot repeat.', t1, t2);
  },

  testProperEventReleaseViaResetForStop() {
    Trace.startTracer('foo');
    Trace.startTracer('foo');
    // The Start events and their ids are released because of reseting.
    Trace.clearCurrentTrace();

    // Recycling the last two Start events.
    const t0 = Trace.startTracer('foo');
    Trace.stopTracer(t0);
    Trace.clearCurrentTrace();

    const t1 = Trace.startTracer('fa');
    Trace.startTracer('fb');
    const t2 = Trace.startTracer('fc');
    // No id is repeated.
    assertNotEquals('The trace ids cannot repeat.', t1, t2);
  },

  testProperEventReleaseViaThresholdForStop() {
    let t1 = Trace.startTracer('f1');
    let t2 = Trace.startTracer('f2');
    // The Start events and their ids are released due to 1000ms threshold.
    Trace.stopTracer(t2, 1000);
    Trace.stopTracer(t1, 1000);

    // Recycling the last two Start events.
    const t0 = Trace.startTracer('foo');
    Trace.stopTracer(t0);
    Trace.clearCurrentTrace();

    t1 = Trace.startTracer('fa');
    Trace.startTracer('fb');
    t2 = Trace.startTracer('fc');
    // No id is repeated.
    assertNotEquals('The trace ids cannot repeat.', t1, t2);
  },

  testTracer() {
    const t = Trace.startTracer('foo');
    let sum = 0;
    for (let i = 0; i < 100000; i++) {
      sum += i;
    }
    Trace.stopTracer(t);
    const trace = Trace.getFormattedTrace();
    const lines = trace.split('\n');
    assertEquals(8, lines.length);
    assertNotNull(lines[0].match(/^\s*\d+\.\d+\s+Start\s+foo$/));
    assertNotNull(lines[1].match(/^\s*\d+\s+\d+\.\d+\s+Done\s+\d+ ms\s+foo$/));
  },

  testListenerTooManyOpenTraces() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
    });
    const expected = [];
    const openTraces = [];
    for (let i = 0; 2 * i <= Trace.MAX_TRACE_SIZE; i++) {
      const t = Trace.startTracer('trace');
      expected.push(['start', t, 'trace']);
      openTraces.push(t);
    }
    // Triggering the giant thread warning to clear open traces.
    const t = Trace.startTracer('last');
    for (let j = 0; 2 * j <= Trace.MAX_TRACE_SIZE; j++) {
      expected.push(['stop', openTraces[j], TRACE_CANCELLED]);
    }
    expected.push(['start', t, 'last']);
    validateRecordedListener(expected, recorder);
  },

  testListenerGiantThread() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
    });
    const t1 = Trace.startTracer('first');
    const expected = [['start', t1, 'first']];
    let t;
    for (let i = 0; 2 * i < Trace.MAX_TRACE_SIZE; i++) {
      t = Trace.startTracer('trace');
      Trace.stopTracer(t);
      expected.push(['start', t, 'trace'], ['stop', t, NORMAL_STOP]);
    }
    // Triggering the giant thread warning.
    const t2 = Trace.startTracer('last');
    expected.push(['start', t2, 'last']);
    // Make sure that the last id of the giant thread is released after
    // clearing.
    assertEquals('The last id is not recycled!', t, t2);
    // Make sure that t1 and t2 are not stopped/cancelled.
    validateRecordedListener(expected, recorder);
  },

  testListenerReset() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
    });
    const t1 = Trace.startTracer('1st');
    const t2 = Trace.startTracer('2nd');
    Trace.stopTracer(t2);
    const t3 = Trace.startTracer('3rd');
    // Forcing t1 and t3 to be cancelled.
    Trace.clearCurrentTrace();

    const expected = [
      ['start', t1, '1st'],
      ['start', t2, '2nd'],
      ['stop', t2, NORMAL_STOP],
      ['start', t3, '3rd'],
      ['stop', t1, TRACE_CANCELLED],
      ['stop', t3, TRACE_CANCELLED],
    ];
    validateRecordedListener(expected, recorder);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testRecord() {
    /** @type {number} */
    const a = 10;
    Trace.addTraceCallbacks(a);
  },

  testListenerStopTracerSilence() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
      comment: goog.partial(recorder, 'comment'),
    });
    const t = Trace.startTracer('first');
    // 1000ms should be enough for silencing the tracer.
    Trace.stopTracer(t, 1000);
    validateRecordedListener(
        [['start', t, 'first'], ['stop', t, NORMAL_STOP]], recorder);
  },

  testListenerStartTracerType() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
      comment: goog.partial(recorder, 'comment'),
    });
    const t = Trace.startTracer('first', 'New Type');
    Trace.stopTracer(t);
    validateRecordedListener(
        [['start', t, '[New Type] first'], ['stop', t, NORMAL_STOP]], recorder);
  },

  testListenerCommentTracerType() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
      comment: goog.partial(recorder, 'comment'),
    });
    Trace.addComment('foo', 'bar');
    validateRecordedListener([['comment', '[bar] foo']], recorder);
  },

  testListenerCommentTracerTime() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
      comment: goog.partial(recorder, 'comment'),
    });
    const timestamp = Date.now() - 10;
    Trace.addComment('first', null, timestamp);
    validateRecordedListener([['comment', 'first', timestamp]], recorder);
  },

  testListenerCommentTracerAlone() {
    Trace.addTraceCallbacks({
      comment: goog.partial(recorder, 'comment'),
    });
    Trace.addComment('foo');
    validateRecordedListener([['comment', 'foo']], recorder);
  },

  testListenerCommentTracerNoLog() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
    });
    Trace.addComment('foo');
    validateRecordedListener([], recorder);
  },

  testListenerStartStopTracerOnly() {
    Trace.addTraceCallbacks({
      start: goog.partial(recorder, 'start'),
      stop: goog.partial(recorder, 'stop'),
    });
    const t = Trace.startTracer('bar');
    Trace.stopTracer(t);
    validateRecordedListener(
        [['start', t, 'bar'], ['stop', t, NORMAL_STOP]], recorder);
  },

  testTwoListeners() {
    const r1 = recordFunction();
    Trace.addTraceCallbacks({
      start: goog.partial(r1, 'start'),
      stop: goog.partial(r1, 'stop'),
    });
    const t0 = Trace.startTracer('first');
    Trace.stopTracer(t0);
    const expected1 = [['start', t0, 'first'], ['stop', t0, NORMAL_STOP]];
    validateRecordedListener(expected1, r1);

    const r2 = recordFunction();
    Trace.addTraceCallbacks({
      start: goog.partial(r2, 'start'),
      stop: goog.partial(r2, 'stop'),
      comment: goog.partial(r2, 'comment'),
    });
    const t1 = Trace.startTracer('second', 'XType');
    const t2 = Trace.startTracer('third');
    Trace.addComment('NoTime');
    const currentTime = Date.now();
    Trace.addComment('WithTime', null, currentTime);
    Trace.addComment('NoTime', 'YType');
    Trace.stopTracer(t2);
    Trace.stopTracer(t1);

    const expected2 = [
      ['start', t1, '[XType] second'],
      ['start', t2, 'third'],
      ['comment', 'NoTime'],
      ['comment', 'WithTime', currentTime],
      ['comment', '[YType] NoTime'],
      ['stop', t2, NORMAL_STOP],
      ['stop', t1, NORMAL_STOP],
    ];
    validateRecordedListener(expected2, r2);

    const expectedNoComment = [
      ['start', t0, 'first'],
      ['stop', t0, NORMAL_STOP],
      ['start', t1, '[XType] second'],
      ['start', t2, 'third'],
      ['stop', t2, NORMAL_STOP],
      ['stop', t1, NORMAL_STOP],
    ];
    validateRecordedListener(expectedNoComment, r1);
  },
});
