// Flags: --expose-gc --no-warnings --expose-internals
'use strict';

const common = require('../common');
const { isMainThread } = require('worker_threads');

if (!isMainThread) {
  // https://github.com/nodejs/node/issues/22767
  common.skip('This test only works on a main thread');
}

try {
  require('trace_events');
} catch {
  common.skip('missing trace events');
}

const assert = require('assert');
const cp = require('child_process');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const {
  createTracing,
  getEnabledCategories
} = require('trace_events');

function getEnabledCategoriesFromCommandLine() {
  const indexOfCatFlag = process.execArgv.indexOf('--trace-event-categories');
  if (indexOfCatFlag === -1) {
    return undefined;
  }
  return process.execArgv[indexOfCatFlag + 1];
}

const isChild = process.argv[2] === 'child';
const enabledCategories = getEnabledCategoriesFromCommandLine();

assert.strictEqual(getEnabledCategories(), enabledCategories);
for (const i of [1, 'foo', true, false, null, undefined]) {
  assert.throws(() => createTracing(i), {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError'
  });
  assert.throws(() => createTracing({ categories: i }), {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError'
  });
}

assert.throws(
  () => createTracing({ categories: [] }),
  {
    code: 'ERR_TRACE_EVENTS_CATEGORY_REQUIRED',
    name: 'TypeError'
  }
);

const tracing = createTracing({ categories: [ 'node.perf' ] });

assert.strictEqual(tracing.categories, 'node.perf');
assert.strictEqual(tracing.enabled, false);

assert.strictEqual(getEnabledCategories(), enabledCategories);
tracing.enable();
tracing.enable();  // Purposefully enable twice to test calling twice
assert.strictEqual(tracing.enabled, true);

assert.strictEqual(getEnabledCategories(),
                   [
                     ...[enabledCategories].filter((_) => !!_), 'node.perf',
                   ].join(','));

tracing.disable();
assert.strictEqual(tracing.enabled, false);

const tracing2 = createTracing({ categories: [ 'foo' ] });
tracing2.enable();
assert.strictEqual(getEnabledCategories(), 'foo');

tracing2.disable();
tracing2.disable();  // Purposefully disable twice to test calling twice
assert.strictEqual(getEnabledCategories(), enabledCategories);

if (isChild) {
  const { internalBinding } = require('internal/test/binding');

  const {
    trace: {
      TRACE_EVENT_PHASE_NESTABLE_ASYNC_BEGIN: kBeforeEvent,
      TRACE_EVENT_PHASE_NESTABLE_ASYNC_END: kEndEvent,
    }
  } = internalBinding('constants');

  const { trace } = internalBinding('trace_events');

  tracing.enable();

  trace(kBeforeEvent, 'foo', 'test1', 0, 'test');
  setTimeout(() => {
    trace(kEndEvent, 'foo', 'test1');
  }, 1);
} else {
  // Test that enabled tracing references do not get garbage collected
  // until after they are disabled.
  {
    {
      let tracing3 = createTracing({ categories: [ 'abc' ] });
      tracing3.enable();
      assert.strictEqual(getEnabledCategories(), 'abc');
      tracing3 = undefined;
    }
    globalThis.gc();
    assert.strictEqual(getEnabledCategories(), 'abc');
    // Not able to disable the thing after this point, however.
  }

  {
    common.expectWarning(
      'Warning',
      'Possible trace_events memory leak detected. There are more than ' +
      '10 enabled Tracing objects.');
    for (let n = 0; n < 10; n++) {
      const tracing = createTracing({ categories: [ `a${n}` ] });
      tracing.enable();
    }
  }

  testApiInChildProcess(['--trace-event-categories', 'foo'], () => {
    testApiInChildProcess(['--trace-event-categories', 'foo']);
  });
}

function testApiInChildProcess(execArgs, cb) {
  tmpdir.refresh();
  // Save the current directory so we can chdir back to it later
  const parentDir = process.cwd();
  process.chdir(tmpdir.path);

  const expectedBegins = [{ cat: 'foo', name: 'test1' }];
  const expectedEnds = [{ cat: 'foo', name: 'test1' }];

  const proc = cp.fork(__filename,
                       ['child'],
                       {
                         execArgv: [
                           '--expose-gc',
                           '--expose-internals',
                           '--no-warnings',
                           ...execArgs,
                         ]
                       });

  proc.once('exit', common.mustCall(() => {
    const file = tmpdir.resolve('node_trace.1.log');

    assert(fs.existsSync(file));
    fs.readFile(file, common.mustSucceed((data) => {
      const traces = JSON.parse(data.toString()).traceEvents
        .filter((trace) => trace.cat !== '__metadata');

      assert.strictEqual(
        traces.length,
        expectedBegins.length + expectedEnds.length);
      for (const trace of traces) {
        assert.strictEqual(trace.pid, proc.pid);
        switch (trace.ph) {
          case 'b': {
            const expectedBegin = expectedBegins.shift();
            assert.strictEqual(trace.cat, expectedBegin.cat);
            assert.strictEqual(trace.name, expectedBegin.name);
            break;
          }
          case 'e': {
            const expectedEnd = expectedEnds.shift();
            assert.strictEqual(trace.cat, expectedEnd.cat);
            assert.strictEqual(trace.name, expectedEnd.name);
            break;
          }
          default:
            assert.fail('Unexpected trace event phase');
        }
      }
      process.chdir(parentDir);
      cb && process.nextTick(cb);
    }));
  }));
}
