import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
import {Frame} from 'sentry/utils/profiling/frame';
import {Profile} from 'sentry/utils/profiling/profile/profile';

// Test utils to keep the tests code dry
export const f = (name: string, key: number, in_app: boolean = true) =>
  new Frame({name, key, is_application: in_app});
export const c = (fr: Frame) => new CallTreeNode(fr, null);
export const firstCallee = (node: CallTreeNode) => node.children[0];
export const nthCallee = (node: CallTreeNode, n: number) => {
  const child = node.children[n];
  if (!child) {
    throw new Error('Child not found');
  }
  return child;
};

export const makeTestingBoilerplate = () => {
  const timings: [Frame['name'], string][] = [];

  const openSpy = jest.fn();
  const closeSpy = jest.fn();

  // We need to wrap the spy fn because they are not allowed to reference external variables
  const open = (node, value) => {
    timings.push([node.frame.name, 'open']);
    openSpy(node, value);
  };
  // We need to wrap the spy fn because they are not allowed to reference external variables
  const close = (node, val) => {
    timings.push([node.frame.name, 'close']);
    closeSpy(node, val);
  };

  return {open, close, timings, openSpy, closeSpy};
};

// Since it's easy to make mistakes or accidentally assign parents to the wrong nodes, this utility fn
// will format the stack samples as a tree string so it's more human friendly.
export const _logExpectedStack = (samples: Profile['samples']): string => {
  const head = `
Samples follow a top-down chronological order\n\n`;

  const tail = `\n
----------------------->
stack top -> stack bottom`;

  const final: string[] = [];

  const visit = (node: CallTreeNode, str: string[]) => {
    str.push(`${node.frame.name}`);

    if (node.parent) {
      visit(node.parent, str);
    }
  };

  for (const stackTop of samples) {
    const str = [];
    visit(stackTop, str);

    final.push(str.join(' -> '));
  }

  return `${head}${final.join('\n')}${tail}`;
};

describe('Profile', () => {
  it('Empty profile duration is not infinity', () => {
    const profile = Profile.Empty;
    expect(profile.duration).toEqual(1000);
    expect(profile.minFrameDuration).toEqual(1000);
  });

  it('forEach - iterates over a single sample', () => {
    const profile = new Profile({
      duration: 1000,
      startedAt: 0,
      endedAt: 1000,
      name: 'profile',
      unit: 'ms',
      threadId: 0,
      type: 'flamechart',
    });

    // Frames
    const f0 = f('f0', 0);
    const f1 = f('f1', 1);

    // Call tree nodes
    const s0 = c(f0);
    const s1 = c(f1);
    s0.parent = s1;

    profile.samples = [s0];

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

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

    expect(openSpy).toHaveBeenCalledTimes(2);
    expect(closeSpy).toHaveBeenCalledTimes(2);
  });

  it('forEach - opens new frames when stack is shared', () => {
    const profile = new Profile({
      duration: 1000,
      startedAt: 0,
      endedAt: 1000,
      name: 'profile',
      unit: 'ms',
      threadId: 0,
      type: 'flamechart',
    });

    // Frames
    const f0 = f('f0', 0);
    const f1 = f('f1', 1);
    const f2 = f('f2', 1);

    // Call tree nodes
    const s0 = c(f0);
    const s1 = c(f1);
    const s2 = c(f2);

    s1.parent = s0;
    s2.parent = s1;

    profile.samples = [s0, s1, s2];

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

    expect(timings).toEqual([
      ['f0', 'open'],
      ['f1', 'open'],
      ['f2', 'open'],
      ['f2', 'close'],
      ['f1', 'close'],
      ['f0', 'close'],
    ]);

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

  it('forEach - closes frames one by one when stack is shared', () => {
    const profile = new Profile({
      duration: 1000,
      startedAt: 0,
      endedAt: 1000,
      name: 'profile',
      unit: 'ms',
      threadId: 0,
      type: 'flamechart',
    });

    // Instantiate frames
    const f0 = f('f0', 0);
    const f1 = f('f1', 1);
    const f2 = f('f2', 2);

    // Instantiate call tree nodes
    const s0 = c(f2);
    const s1 = c(f1);
    const s2 = c(f0);

    profile.samples = [s0, s1, s2];

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

    expect(timings).toEqual([
      ['f2', 'open'],
      ['f2', 'close'],
      ['f1', 'open'],
      ['f1', 'close'],
      ['f0', 'open'],
      ['f0', 'close'],
    ]);

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

  // In JS land, the stack can be idle which is not the case in other runtimes, e.g. in mobile
  //  the program main is always running, so make sure we support "holes" in the samples
  it('forEach - supports an idle stack', () => {
    const profile = new Profile({
      duration: 1000,
      startedAt: 0,
      endedAt: 1000,
      name: 'profile',
      unit: 'ms',
      threadId: 0,
      type: 'flamechart',
    });

    // Instantiate frames
    const f0 = f('f0', 0);

    // Instantiate call tree nodes
    const s0 = c(f0);
    const s1 = c(Frame.Root);

    profile.samples = [s0, s1, s0];

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

    expect(timings).toEqual([
      ['f0', 'open'],
      ['f0', 'close'],
      ['f0', 'open'],
      ['f0', 'close'],
    ]);

    expect(openSpy).toHaveBeenCalledTimes(2);
    expect(closeSpy).toHaveBeenCalledTimes(2);
  });
});
