import {
  makeEventTransaction,
  makeSpan,
  makeTrace,
  makeTransaction,
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeTestUtils';

import {isParentAutogroupedNode, isSiblingAutogroupedNode} from './../traceGuards';
import {TraceTree} from './traceTree';

const start = new Date('2024-02-29T00:00:00Z').getTime() / 1e3;
const traceMetadata = {replay: null, meta: null};

const singleTransactionTrace = makeTrace({
  transactions: [
    makeTransaction({
      start_timestamp: start,
      timestamp: start + 2,
      children: [],
    }),
  ],
  orphan_errors: [],
});

const siblingAutogroupSpans = [
  makeSpan({
    op: 'db',
    description: 'redis',
    start_timestamp: start,
    timestamp: start + 1,
  }),
  makeSpan({
    op: 'db',
    description: 'redis',
    start_timestamp: start,
    timestamp: start + 1,
  }),
  makeSpan({
    op: 'db',
    description: 'redis',
    start_timestamp: start,
    timestamp: start + 1,
  }),
  makeSpan({
    op: 'db',
    description: 'redis',
    start_timestamp: start,
    timestamp: start + 1,
  }),
  makeSpan({
    op: 'db',
    description: 'redis',
    start_timestamp: start,
    timestamp: start + 1,
  }),
];

const parentAutogroupSpans = [
  makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
  makeSpan({op: 'db', description: 'redis', span_id: '0001', parent_span_id: '0000'}),
  makeSpan({op: 'db', description: 'redis', span_id: '0002', parent_span_id: '0001'}),
];

const parentAutogroupSpansWithChilden = [
  makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
  makeSpan({op: 'db', description: 'redis', span_id: '0001', parent_span_id: '0000'}),
  makeSpan({op: 'db', description: 'redis', span_id: '0002', parent_span_id: '0001'}),
  makeSpan({op: 'http', description: 'request', span_id: '0003', parent_span_id: '0002'}),
];

describe('autogrouping', () => {
  describe('parent autogrouping', () => {
    it('groups parent chain with same op', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        parentAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('assigns children to tail node', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        [
          makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
          makeSpan({
            op: 'db',
            description: 'redis',
            span_id: '0001',
            parent_span_id: '0000',
          }),
          makeSpan({
            op: 'db',
            description: 'redis',
            span_id: '0002',
            parent_span_id: '0001',
          }),
          makeSpan({
            op: 'http.request',
            description: 'browser',
            span_id: '0003',
            parent_span_id: '0002',
          }),
        ],
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('autogrouped chain points to tail', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        [
          ...parentAutogroupSpans,
          makeSpan({
            op: 'http',
            description: 'request',
            parent_span_id: parentAutogroupSpans[parentAutogroupSpans.length - 1].span_id,
          }),
        ],
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('expanding parent autogroup renders head to tail chain', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        parentAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);

      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('collapsing parent autogroup removes its children', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        parentAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);

      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });

      expect(tree.build().serialize()).toMatchSnapshot();

      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, false);
        }
      });

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('can expand and collapse', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        [
          makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
          makeSpan({
            op: 'db',
            description: 'redis',
            span_id: '0001',
            parent_span_id: '0000',
          }),
          makeSpan({
            op: 'db',
            description: 'redis',
            span_id: '0002',
            parent_span_id: '0001',
          }),
          makeSpan({
            op: 'http.request',
            description: 'browser',
            span_id: '0003',
            parent_span_id: '0002',
          }),
        ],
        makeEventTransaction()
      );

      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);

      const initial = tree.build().serialize();

      // Expand autogroup part
      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });
      expect(tree.build().serialize()).toMatchSnapshot();

      // Collapse autogroup part
      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, false);
        }
      });

      // Tree should be back to initial state
      expect(tree.build().serialize()).toEqual(initial);
    });

    it('autogroups siblings when they are children of a parent autogroup chain', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        [
          ...parentAutogroupSpans,
          ...[1, 2, 3, 4, 5].map(_i =>
            makeSpan({
              op: 'db',
              description: 'sql',
              start_timestamp: start,
              timestamp: start + 1,
              parent_span_id:
                parentAutogroupSpans[parentAutogroupSpans.length - 1].span_id,
            })
          ),
        ],
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);
      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('removes collapsed parent autogroup', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        parentAutogroupSpansWithChilden,
        makeEventTransaction()
      );

      const snapshot = tree.build().serialize();
      // Add sibling autogroup
      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
      expect(TraceTree.Find(tree.root, c => isParentAutogroupedNode(c))).not.toBeNull();

      // Remove it and assert that the tree is back to the original state
      TraceTree.RemoveDirectChildrenAutogroupNodes(tree.root);

      expect(TraceTree.Find(tree.root, c => isParentAutogroupedNode(c))).toBeNull();
      expect(tree.build().serialize()).toEqual(snapshot);
      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('removes expanded parent autogroup', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        parentAutogroupSpansWithChilden,
        makeEventTransaction()
      );

      const snapshot = tree.build().serialize();
      // Add sibling autogroup
      TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
      TraceTree.ForEachChild(tree.root, c => {
        if (isParentAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });

      expect(tree.build().serialize()).toMatchSnapshot();
      expect(TraceTree.Find(tree.root, c => isParentAutogroupedNode(c))).not.toBeNull();

      // Remove it and assert that the tree is back to the original state
      TraceTree.RemoveDirectChildrenAutogroupNodes(tree.root);
      expect(TraceTree.Find(tree.root, c => isParentAutogroupedNode(c))).toBeNull();
      TraceTree.invalidate(tree.root, true);
      expect(tree.build().serialize()).toEqual(snapshot);
      expect(tree.build().serialize()).toMatchSnapshot();
    });
  });

  describe('sibling autogrouping', () => {
    it('groups spans with the same op and description', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        siblingAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('does not autogroup if count is less 5', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        siblingAutogroupSpans.slice(0, 4),
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('autogroups multiple consecutive groups', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        [
          ...siblingAutogroupSpans,
          ...siblingAutogroupSpans.map(s => ({...s, op: 'mysql'})),
        ],
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('expanding sibling autogroup renders its children', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        siblingAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);

      TraceTree.ForEachChild(tree.root, c => {
        if (isSiblingAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });

      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('collapsing sibling autogroup removes its children', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        siblingAutogroupSpans,
        makeEventTransaction()
      );

      TraceTree.AutogroupSiblingSpanNodes(tree.root);

      TraceTree.ForEachChild(tree.root, c => {
        if (isSiblingAutogroupedNode(c)) {
          tree.expand(c, true);
        }
      });
      expect(tree.build().serialize()).toMatchSnapshot();
      TraceTree.ForEachChild(tree.root, c => {
        if (isSiblingAutogroupedNode(c)) {
          tree.expand(c, false);
        }
      });
      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it('removes sibling autogroup', () => {
      const tree = TraceTree.FromTrace(singleTransactionTrace, traceMetadata);
      TraceTree.FromSpans(
        tree.root.children[0].children[0],
        siblingAutogroupSpans,
        makeEventTransaction()
      );

      const snapshot = tree.build().serialize();

      // Add sibling autogroup
      TraceTree.AutogroupSiblingSpanNodes(tree.root);
      expect(TraceTree.Find(tree.root, c => isSiblingAutogroupedNode(c))).not.toBeNull();

      // Remove it and assert that the tree is back to the original state
      TraceTree.RemoveSiblingAutogroupNodes(tree.root);
      expect(tree.build().serialize()).toEqual(snapshot);
      expect(tree.build().serialize()).toMatchSnapshot();
    });

    it.todo(
      'collects errors, performance issues and profiles from sibling autogroup chain'
    );
  });
});
