import {JSSelfProfile} from 'sentry/utils/profiling/profile/jsSelfProfile';
import {createFrameIndex} from 'sentry/utils/profiling/profile/utils';

import {firstCallee, makeTestingBoilerplate, nthCallee} from './profile.spec';

describe('jsSelfProfile', () => {
  it('imports the base properties', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js', 'vendor.js'],
      frames: [{name: 'ReactDOM.render', line: 1, column: 1, resourceId: 0}],
      samples: [
        {
          timestamp: 0,
          stackId: 0,
        },
        {
          timestamp: 1000,
          stackId: 0,
        },
      ],
      stacks: [
        {
          frameId: 0,
        },
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    expect(profile.duration).toBe(1000);
    expect(profile.startedAt).toBe(0);
    expect(profile.endedAt).toBe(1000);
    expect(profile.callTree.children[0].frame.name).toBe('ReactDOM.render');
    expect(profile.callTree.children[0].frame.resource).toBe('app.js');
  });

  it('tracks discarded samples', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js', 'vendor.js'],
      frames: [{name: 'ReactDOM.render', line: 1, column: 1, resourceId: 0}],
      samples: [
        {
          timestamp: 0,
          stackId: 0,
        },
      ],
      stacks: [
        {
          frameId: 0,
        },
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', [{name: 'f0'}]),
      {type: 'flamechart'}
    );
    expect(profile.stats.discardedSamplesCount).toBe(1);
  });

  it('tracks negative samples', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js', 'vendor.js'],
      frames: [{name: 'ReactDOM.render', line: 1, column: 1, resourceId: 0}],
      samples: [
        {
          timestamp: 0,
          stackId: 0,
        },
        {
          timestamp: -1,
          stackId: 0,
        },
      ],
      stacks: [
        {
          frameId: 0,
        },
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', [{name: 'f0'}]),
      {type: 'flamechart'}
    );
    expect(profile.stats.negativeSamplesCount).toBe(1);
  });

  it('tracks raw weights', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js', 'vendor.js'],
      frames: [{name: 'ReactDOM.render', line: 1, column: 1, resourceId: 0}],
      samples: [
        {
          timestamp: 5,
          stackId: 0,
        },
        {
          timestamp: 10,
          stackId: 0,
        },
        {
          timestamp: 15,
          stackId: 0,
        },
      ],
      stacks: [
        {
          frameId: 0,
        },
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', [{name: 'f0'}]),
      {type: 'flamechart'}
    );
    // For JsSelfProfile, first sample is appended with 0 weight because it
    // contains the stack sample of when startProfile was called
    expect(profile.rawWeights.length).toBe(2);
  });

  it('handles the first stack sample differently', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'main', line: 1, column: 1, resourceId: 0},
        {name: 'new Profiler', line: 1, column: 1, resourceId: 0},
        {name: 'afterProfiler.init', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 1,
          timestamp: 500,
        },
        {
          stackId: 2,
          timestamp: 1500,
        },
      ],
      stacks: [
        {frameId: 0, parentId: undefined},
        {frameId: 1, parentId: 0},
        {frameId: 2, parentId: 0},
      ],
    };

    const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    profile.forEach(open, close);

    expect(timings).toEqual([
      ['main', 'open'],
      ['new Profiler', 'open'],
      ['new Profiler', 'close'],
      ['afterProfiler.init', 'open'],
      ['afterProfiler.init', 'close'],
      ['main', 'close'],
    ]);
    expect(openSpy).toHaveBeenCalledTimes(3);
    expect(closeSpy).toHaveBeenCalledTimes(3);

    const root = firstCallee(profile.callTree);

    if (!root) {
      throw new Error('root is null');
    }

    expect(root.totalWeight).toEqual(1000);
    expect(root.selfWeight).toEqual(0);

    expect(nthCallee(root, 0).selfWeight).toEqual(0);
    expect(nthCallee(root, 0).totalWeight).toEqual(0);

    expect(nthCallee(root, 1).selfWeight).toEqual(1000);
    expect(nthCallee(root, 1).totalWeight).toEqual(1000);
  });

  it('rebuilds the stack', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'f0', line: 1, column: 1, resourceId: 0},
        {name: 'f1', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          timestamp: 1000,
          stackId: 0,
        },
      ],
      stacks: [{frameId: 1, parentId: 1}, {frameId: 0}],
    };

    const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    profile.forEach(open, close);

    expect(timings).toEqual([
      ['f0', 'open'],
      ['f1', 'open'],
      ['f1', 'close'],
      ['f0', 'close'],
    ]);
    expect(openSpy).toHaveBeenCalledTimes(2);
    expect(closeSpy).toHaveBeenCalledTimes(2);

    const root = firstCallee(profile.callTree);

    expect(root.totalWeight).toEqual(1000);
    expect(firstCallee(root).totalWeight).toEqual(1000);

    expect(root.selfWeight).toEqual(0);
    expect(firstCallee(root).selfWeight).toEqual(1000);
  });

  it('marks direct recursion', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [{name: 'f0', line: 1, column: 1, resourceId: 0}],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          stackId: 0,
          timestamp: 0,
        },
      ],
      stacks: [{frameId: 0, parentId: 1}, {frameId: 0}],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    expect(!!firstCallee(firstCallee(profile.callTree)).recursive).toBe(true);
  });

  it('marks indirect recursion', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'f0', line: 1, column: 1, resourceId: 0},
        {name: 'f1', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          stackId: 2,
          timestamp: 100,
        },
      ],
      stacks: [
        {frameId: 0, parentId: undefined},
        {frameId: 1, parentId: 0},
        {frameId: 0, parentId: 1},
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    expect(!!firstCallee(firstCallee(firstCallee(profile.callTree))).recursive).toBe(
      true
    );
  });

  it('tracks minFrameDuration', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'f0', line: 1, column: 1, resourceId: 0},
        {name: 'f1', line: 1, column: 1, resourceId: 0},
        {name: 'f2', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          stackId: 2,
          timestamp: 10,
        },
        {
          stackId: 3,
          timestamp: 100,
        },
      ],
      stacks: [
        {frameId: 0, parentId: undefined},
        {frameId: 1, parentId: 0},
        {frameId: 0, parentId: 1},
        {frameId: 2},
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    expect(profile.minFrameDuration).toBe(10);
  });

  it('appends gc to previous stack', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'f1', line: 1, column: 1, resourceId: 0},
        {name: 'f0', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          timestamp: 10,
          marker: 'gc',
          stackId: 0,
        },
      ],
      stacks: [
        {frameId: 0, parentId: 1},
        {frameId: 1, parentId: undefined},
      ],
    };

    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();

    profile.forEach(open, close);

    expect(openSpy).toHaveBeenCalledTimes(3);
    expect(closeSpy).toHaveBeenCalledTimes(3);

    expect(timings).toEqual([
      ['f0', 'open'],
      ['f1', 'open'],
      ['Garbage Collection', 'open'],
      ['Garbage Collection', 'close'],
      ['f1', 'close'],
      ['f0', 'close'],
    ]);
  });

  it('flamegraph tracks node occurences', () => {
    const trace: JSSelfProfiling.Trace = {
      resources: ['app.js'],
      frames: [
        {name: 'f1', line: 1, column: 1, resourceId: 0},
        {name: 'f0', line: 1, column: 1, resourceId: 0},
      ],
      samples: [
        {
          stackId: 0,
          timestamp: 0,
        },
        {
          timestamp: 10,
          stackId: 1,
        },
        {
          timestamp: 20,
          stackId: 0,
        },
      ],
      stacks: [
        {frameId: 0, parentId: undefined},
        {frameId: 1, parentId: 0},
      ],
    };
    const profile = JSSelfProfile.FromProfile(
      trace,
      createFrameIndex('javascript', trace.frames, trace),
      {type: 'flamechart'}
    );

    expect(profile.callTree.children[0].count).toBe(3);
    expect(profile.callTree.children[0].children[0].count).toBe(1);
  });
});
