import {waitFor} from 'sentry-test/reactTestingLibrary';

import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
import type {EventTransaction} from 'sentry/types';
import {
  type TraceTree,
  TraceTreeNode,
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
import {searchInTraceTreeTokens} from 'sentry/views/performance/newTraceDetails/traceSearch/traceSearchEvaluator';
import {parseTraceSearch} from 'sentry/views/performance/newTraceDetails/traceSearch/traceTokenConverter';

function makeTransaction(
  overrides: Partial<TraceTree.Transaction> = {}
): TraceTree.Transaction {
  return {
    children: [],
    start_timestamp: 0,
    timestamp: 1,
    transaction: 'transaction',
    'transaction.op': '',
    'transaction.status': '',
    performance_issues: [],
    errors: [],
    ...overrides,
  } as TraceTree.Transaction;
}

function makeSpan(overrides: Partial<RawSpanType> = {}): TraceTree.Span {
  return {
    span_id: '',
    op: '',
    description: '',
    start_timestamp: 0,
    timestamp: 10,
    data: {},
    trace_id: '',
    childTransactions: [],
    event: undefined as unknown as EventTransaction,
    ...overrides,
  };
}

function makeError(overrides: Partial<TraceTree.TraceError> = {}): TraceTree.TraceError {
  return {
    issue_id: 1,
    issue: 'dead issue',
    event_id: 'event_id',
    project_slug: 'project',
    project_id: 1,
    level: 'fatal',
    title: 'dead',
    message: 'dead message',
    span: '1',
    ...overrides,
  };
}

function makePerformanceIssue(
  overrides: Partial<TraceTree.TracePerformanceIssue> = {}
): TraceTree.TracePerformanceIssue {
  return {
    event_id: 'event_id',
    project_slug: 'project',
    message: 'dead message',
    title: 'dead',
    issue_id: 1,
    level: 'fatal',
    project_id: 1,
    culprit: 'culprit',
    start: 0,
    end: 1,
    span: [],
    suspect_spans: [],
    type: 0,
    ...overrides,
  };
}

const makeTree = (list: TraceTree.NodeValue[]): TraceTree => {
  return {
    list: list.map(
      n => new TraceTreeNode(null, n, {project_slug: 'project', event_id: ''})
    ),
  } as unknown as TraceTree;
};

const search = (query: string, tree: TraceTree, cb: any) => {
  searchInTraceTreeTokens(
    tree,
    // @ts-expect-error dont care if this fails
    parseTraceSearch(query),
    null,
    cb
  );
};

describe('TraceSearchEvaluator', () => {
  it('empty string', async () => {
    const list = makeTree([
      makeTransaction({'transaction.op': 'operation'}),
      makeTransaction({'transaction.op': 'other'}),
    ]);

    const cb = jest.fn();
    search('', list, cb);
    await waitFor(() => {
      expect(cb).toHaveBeenCalled();
    });
    expect(cb.mock.calls[0][0][0]).toEqual([]);
    expect(cb.mock.calls[0][0][1].size).toBe(0);
    expect(cb.mock.calls[0][0][2]).toBe(null);
  });
  it.each([
    [''],
    ['invalid_query'],
    ['invalid_query:'],
    ['OR'],
    ['AND'],
    ['('],
    [')'],
    ['()'],
    ['(invalid_query)'],
  ])('invalid grammar %s', async query => {
    const list = makeTree([
      makeTransaction({'transaction.op': 'operation'}),
      makeTransaction({'transaction.op': 'other'}),
    ]);

    const cb = jest.fn();
    search(query, list, cb);
    await waitFor(() => {
      expect(cb).toHaveBeenCalled();
    });
    expect(cb.mock.calls[0][0][0]).toEqual([]);
    expect(cb.mock.calls[0][0][1].size).toBe(0);
    expect(cb.mock.calls[0][0][2]).toBe(null);
  });

  it('AND query', async () => {
    const tree = makeTree([
      makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
      makeTransaction({'transaction.op': 'other'}),
    ]);

    const cb = jest.fn();
    search('transaction.op:operation AND transaction:something', tree, cb);
    await waitFor(() => {
      expect(cb).toHaveBeenCalled();
    });
    expect(cb.mock.calls[0][0][1].size).toBe(1);
    expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
    expect(cb.mock.calls[0][0][2]).toBe(null);
  });

  it('OR query', async () => {
    const tree = makeTree([
      makeTransaction({'transaction.op': 'operation'}),
      makeTransaction({'transaction.op': 'other'}),
    ]);

    const cb = jest.fn();
    search('transaction.op:operation OR transaction.op:other', tree, cb);
    await waitFor(() => {
      expect(cb).toHaveBeenCalled();
    });
    expect(cb.mock.calls[0][0][0]).toEqual([
      {index: 0, value: tree.list[0]},
      {index: 1, value: tree.list[1]},
    ]);
    expect(cb.mock.calls[0][0][1].size).toBe(2);
    expect(cb.mock.calls[0][0][2]).toBe(null);
  });

  it('OR with AND respects precedence', async () => {
    const tree = makeTree([
      makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
      makeTransaction({'transaction.op': 'other', transaction: ''}),
    ]);

    const cb = jest.fn();
    search(
      'transaction.op:operation AND transaction:something OR transaction.op:other',
      tree,
      cb
    );
    await waitFor(() => {
      expect(cb).toHaveBeenCalled();
    });
    expect(cb.mock.calls[0][0][1].size).toBe(2);
    expect(cb.mock.calls[0][0][0]).toEqual([
      {index: 0, value: tree.list[0]},
      {index: 1, value: tree.list[1]},
    ]);
    expect(cb.mock.calls[0][0][2]).toBe(null);
  });

  describe('transaction', () => {
    it('text filter', async () => {
      const tree = makeTree([
        makeTransaction({'transaction.op': 'operation'}),
        makeTransaction({'transaction.op': 'other'}),
      ]);

      const cb = jest.fn();
      search('transaction.op:operation', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('text filter with prefix', async () => {
      const tree = makeTree([makeTransaction({transaction: 'operation'})]);

      const cb = jest.fn();
      search('transaction.transaction:operation', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('transaction.duration (milliseconds)', async () => {
      const tree = makeTree([
        makeTransaction({'transaction.duration': 1000}),
        makeTransaction({'transaction.duration': 500}),
      ]);

      const cb = jest.fn();
      search('transaction.duration:>500ms', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('transaction.duration (seconds)', async () => {
      const tree = makeTree([
        makeTransaction({'transaction.duration': 1000}),
        makeTransaction({'transaction.duration': 500}),
      ]);

      const cb = jest.fn();
      search('transaction.duration:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('transaction.total_time', async () => {
      const tree = makeTree([
        makeTransaction({start_timestamp: 0, timestamp: 1}),
        makeTransaction({start_timestamp: 0, timestamp: 0.5}),
      ]);

      const cb = jest.fn();
      search('transaction.total_time:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    // For consistency between spans and txns, should should be implemented
    // it('transaction.self_time', () => {});
  });

  describe('span', () => {
    it('text filter', async () => {
      const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);

      const cb = jest.fn();
      search('op:db', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('text filter with prefix', async () => {
      const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);

      const cb = jest.fn();
      search('span.op:db', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('span.duration (milliseconds)', async () => {
      const tree = makeTree([
        makeSpan({start_timestamp: 0, timestamp: 1}),
        makeSpan({start_timestamp: 0, timestamp: 0.5}),
      ]);

      const cb = jest.fn();
      search('span.duration:>500ms', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('span.duration (seconds)', async () => {
      const tree = makeTree([
        makeSpan({start_timestamp: 0, timestamp: 1}),
        makeSpan({start_timestamp: 0, timestamp: 0.5}),
      ]);

      const cb = jest.fn();
      search('span.duration:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });

    it('span.total_time', async () => {
      const tree = makeTree([
        makeSpan({start_timestamp: 0, timestamp: 1}),
        makeSpan({start_timestamp: 0, timestamp: 0.5}),
      ]);

      const cb = jest.fn();
      search('span.total_time:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });
    it('span.self_time', async () => {
      const tree = makeTree([
        makeSpan({exclusive_time: 1000}),
        makeSpan({exclusive_time: 500}),
      ]);

      const cb = jest.fn();
      search('span.self_time:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });
    it('span.exclusive_time', async () => {
      const tree = makeTree([
        makeSpan({exclusive_time: 1000}),
        makeSpan({exclusive_time: 500}),
      ]);

      const cb = jest.fn();
      search('span.exclusive_time:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });
    it('exclusive_time', async () => {
      const tree = makeTree([
        makeSpan({exclusive_time: 1000}),
        makeSpan({exclusive_time: 500}),
      ]);

      const cb = jest.fn();
      search('exclusive_time:>0.5s', tree, cb);
      await waitFor(() => expect(cb).toHaveBeenCalled());
      expect(cb.mock.calls[0][0][1].size).toBe(1);
      expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
      expect(cb.mock.calls[0][0][2]).toBe(null);
    });
  });

  describe('synthetic keys', () => {
    describe('has:', () => {
      it.each(['error', 'errors'])('%s (transaction)', async key => {
        const tree = makeTree([
          makeTransaction({
            errors: [makeError()],
          }),
          makeTransaction({errors: []}),
        ]);

        const cb = jest.fn();
        search(`has:${key}`, tree, cb);
        await waitFor(() => expect(cb).toHaveBeenCalled());
        expect(cb.mock.calls[0][0][1].size).toBe(1);
        expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
        expect(cb.mock.calls[0][0][2]).toBe(null);
      });
      it.each(['issue', 'issues'])('%s (error on transaction)', async key => {
        const tree = makeTree([
          makeTransaction({
            errors: [makeError()],
          }),
          makeTransaction({errors: []}),
        ]);

        const cb = jest.fn();
        search(`has:${key}`, tree, cb);
        await waitFor(() => expect(cb).toHaveBeenCalled());
        expect(cb.mock.calls[0][0][1].size).toBe(1);
        expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
        expect(cb.mock.calls[0][0][2]).toBe(null);
      });

      it.each(['issue', 'issues'])('%s (performance issue on transaction)', async key => {
        const tree = makeTree([
          makeTransaction({
            performance_issues: [makePerformanceIssue()],
          }),
          makeTransaction({errors: []}),
        ]);

        const cb = jest.fn();
        search(`has:${key}`, tree, cb);
        await waitFor(() => expect(cb).toHaveBeenCalled());
        expect(cb.mock.calls[0][0][1].size).toBe(1);
        expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
        expect(cb.mock.calls[0][0][2]).toBe(null);
      });
      it.each(['profile', 'profiles'])('%s (profile on transaction)', async key => {
        const tree = makeTree([
          makeTransaction({
            profile_id: 'profile',
          }),
          makeTransaction({errors: []}),
        ]);

        const cb = jest.fn();
        search(`has:${key}`, tree, cb);
        await waitFor(() => expect(cb).toHaveBeenCalled());
        expect(cb.mock.calls[0][0][1].size).toBe(1);
        expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
        expect(cb.mock.calls[0][0][2]).toBe(null);
      });
    });
  });
});
