import { type collectorTypes } from '@opentelemetry/exporter-collector';

import { type Field, PluginType, type DataSourceInstanceSettings, type PluginMetaInfo } from '@grafana/data';

import {
  transformToOTLP,
  transformFromOTLP,
  createTableFrameFromTraceQlQuery,
  createTableFrameFromTraceQlQueryAsSpans,
} from './resultTransformer';
import {
  badOTLPResponse,
  otlpDataFrameToResponse,
  otlpDataFrameFromResponse,
  otlpResponse,
  traceQlResponse,
} from './test/testResponse';
import { type TraceSearchMetadata } from './types';

const defaultSettings: DataSourceInstanceSettings = {
  uid: '0',
  type: 'tracing',
  name: 'tempo',
  access: 'proxy',
  meta: {
    id: 'tempo',
    name: 'tempo',
    type: PluginType.datasource,
    info: {} as PluginMetaInfo,
    module: '',
    baseUrl: '',
  },
  readOnly: false,
  jsonData: {},
};

describe('transformToOTLP()', () => {
  test('transforms dataframe to OTLP format', () => {
    const otlp = transformToOTLP(otlpDataFrameToResponse);
    expect(otlp).toMatchObject(otlpResponse);
  });
});

describe('transformFromOTLP()', () => {
  test('transforms OTLP format to dataFrame', () => {
    const res = transformFromOTLP(
      otlpResponse.batches as unknown as collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
      false
    );
    expect(res.data[0]).toMatchObject({
      ...otlpDataFrameFromResponse,
      creator: expect.any(Function),
    });
  });

  test('extracts service.namespace from resource attributes into serviceNamespace column', () => {
    const batchesWithNamespace = [
      {
        ...otlpResponse.batches[0],
        resource: {
          attributes: [
            { key: 'service.name', value: { stringValue: 'cart-service' } },
            { key: 'service.namespace', value: { stringValue: 'production' } },
            { key: 'host.name', value: { stringValue: 'host1' } },
          ],
        },
      },
    ];
    const res = transformFromOTLP(
      batchesWithNamespace as unknown as collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
      false
    );
    expect(res.data).toHaveLength(1);
    const frame = res.data[0];
    const serviceNamespaceField = frame.fields.find((f: Field) => f.name === 'serviceNamespace');
    expect(serviceNamespaceField).toBeDefined();
    expect(serviceNamespaceField!.values[0]).toBe('production');
    const serviceNameField = frame.fields.find((f: Field) => f.name === 'serviceName');
    expect(serviceNameField!.values[0]).toBe('cart-service');
  });

  test('coalesces service.namespace.name when service.namespace is not present', () => {
    const batchesWithAltNamespace = [
      {
        ...otlpResponse.batches[0],
        resource: {
          attributes: [
            { key: 'service.name', value: { stringValue: 'api' } },
            { key: 'service.namespace.name', value: { stringValue: 'staging' } },
          ],
        },
      },
    ];
    const res = transformFromOTLP(
      batchesWithAltNamespace as unknown as collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
      false
    );
    expect(res.data).toHaveLength(1);
    const serviceNamespaceField = res.data[0].fields.find((f: Field) => f.name === 'serviceNamespace');
    expect(serviceNamespaceField).toBeDefined();
    expect(serviceNamespaceField!.values[0]).toBe('staging');
  });

  test('leaves serviceNamespace undefined when no namespace attribute is present', () => {
    const res = transformFromOTLP(
      otlpResponse.batches as unknown as collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
      false
    );
    const serviceNamespaceField = res.data[0].fields.find((f: Field) => f.name === 'serviceNamespace');
    expect(serviceNamespaceField).toBeDefined();
    expect(serviceNamespaceField!.values[0]).toBeUndefined();
  });
});

describe('createTableFrameFromTraceQlQuery()', () => {
  test('transforms TraceQL response to DataFrame', () => {
    const frameList = createTableFrameFromTraceQlQuery(traceQlResponse.traces, defaultSettings);
    const frame = frameList[0];
    // Trace ID field
    expect(frame.fields[0].name).toBe('traceID');
    expect(frame.fields[0].values[0]).toBe('b1586c3c8c34d');
    expect(frame.fields[0].config.unit).toBe('string');
    // Start time field
    expect(frame.fields[1].name).toBe('startTime');
    expect(frame.fields[1].type).toBe('time');
    expect(frame.fields[1].values[1]).toBe(1643342166678.0002);
    // Trace service field
    expect(frame.fields[2].name).toBe('traceService');
    expect(frame.fields[2].type).toBe('string');
    expect(frame.fields[2].values[0]).toBe('lb');
    // Trace name field
    expect(frame.fields[3].name).toBe('traceName');
    expect(frame.fields[3].type).toBe('string');
    expect(frame.fields[3].values[0]).toBe('HTTP Client');
    // Duration field
    expect(frame.fields[4].name).toBe('traceDuration');
    expect(frame.fields[4].type).toBe('number');
    expect(frame.fields[4].values[2]).toBe(44);
    expect(frame.fields[4].values[1]).toBe('<1ms');
    // Subframes field
    expect(frame.fields[5].name).toBe('nested');
    expect(frame.fields[5].type).toBe('nestedFrames');
    // Single spanset
    expect(frame.fields[5].values[0][0].fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[5].values[0][0].fields[0].values[0]).toBe('b1586c3c8c34d');
    expect(frame.fields[5].values[0][0].fields[1].name).toBe('spanID');
    expect(frame.fields[5].values[0][0].fields[1].values[0]).toBe('162a4adae63b61f1');
    expect(frame.fields[5].values[0][0].fields[2].name).toBe('time');
    expect(frame.fields[5].values[0][0].fields[2].values[0]).toBe(1666188214303.201);
    expect(frame.fields[5].values[0][0].fields[4].name).toBe('http.method');
    expect(frame.fields[5].values[0][0].fields[4].values[0]).toBe('GET');
    expect(frame.fields[5].values[0][0].fields[5].name).toBe('service.name');
    expect(frame.fields[5].values[0][0].fields[5].values[0]).toBe('db');
    expect(frame.fields[5].values[0][0].fields[6].name).toBe('duration');
    expect(frame.fields[5].values[0][0].fields[6].values[0]).toBe(545000);
    // Multiple spansets - set 0
    expect(frame.fields[5].values[1][0].fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[5].values[1][0].fields[0].values[0]).toBe('9161e77388f3e');
    expect(frame.fields[5].values[1][0].fields[1].name).toBe('spanID');
    expect(frame.fields[5].values[1][0].fields[1].values[0]).toBe('3b9a5c222d3ddd8f');
    expect(frame.fields[5].values[1][0].fields[2].name).toBe('time');
    expect(frame.fields[5].values[1][0].fields[2].values[0]).toBe(1666187875397.7212);
    expect(frame.fields[5].values[1][0].fields[4].name).toBe('by(resource.service.name)');
    expect(frame.fields[5].values[1][0].fields[4].values[0]).toBe('db');
    expect(frame.fields[5].values[1][0].fields[5].name).toBe('http.method');
    expect(frame.fields[5].values[1][0].fields[5].values[0]).toBe('GET');
    expect(frame.fields[5].values[1][0].fields[6].name).toBe('service.name');
    expect(frame.fields[5].values[1][0].fields[6].values[0]).toBe('db');
    expect(frame.fields[5].values[1][0].fields[7].name).toBe('duration');
    expect(frame.fields[5].values[1][0].fields[7].values[0]).toBe(877000);
    // Multiple spansets - set 1
    expect(frame.fields[5].values[1][1].fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[5].values[1][1].fields[0].values[0]).toBe('9161e77388f3e');
    expect(frame.fields[5].values[1][1].fields[1].name).toBe('spanID');
    expect(frame.fields[5].values[1][1].fields[1].values[0]).toBe('894d90db6b5807f');
    expect(frame.fields[5].values[1][1].fields[2].name).toBe('time');
    expect(frame.fields[5].values[1][1].fields[2].values[0]).toBe(1666187875393.293);
    expect(frame.fields[5].values[1][1].fields[4].name).toBe('by(resource.service.name)');
    expect(frame.fields[5].values[1][1].fields[4].values[0]).toBe('app');
    expect(frame.fields[5].values[1][1].fields[5].name).toBe('http.method');
    expect(frame.fields[5].values[1][1].fields[5].values[0]).toBe('GET');
    expect(frame.fields[5].values[1][1].fields[6].name).toBe('service.name');
    expect(frame.fields[5].values[1][1].fields[6].values[0]).toBe('app');
    expect(frame.fields[5].values[1][1].fields[7].name).toBe('duration');
    expect(frame.fields[5].values[1][1].fields[7].values[0]).toBe(11073000);
  });
});

describe('createTableFrameFromTraceQlQueryAsSpans()', () => {
  test('transforms TraceQL legacy response to DataFrame for Spans table type', () => {
    const traces = [
      {
        traceID: '1',
        rootServiceName: 'prometheus',
        rootTraceName: 'POST /api/v1/write',
        startTimeUnixNano: '1702984850354934104',
        durationMs: 1,
        spanSet: {
          spans: [
            {
              spanID: '11',
              startTimeUnixNano: '1702984850354934104',
              durationNanos: '1377608',
            },
          ],
          matched: 1,
          attributes: [{ key: 'attr-key-1', value: { intValue: '123' } }],
        },
      },
      {
        traceID: '2',
        rootServiceName: 'prometheus',
        rootTraceName: 'GET /api/v1/status/config',
        startTimeUnixNano: '1702984840786143459',
        spanSet: {
          spans: [
            {
              spanID: '21',
              startTimeUnixNano: '1702984840786143459',
              durationNanos: '542316',
            },
          ],
          matched: 1,
          attributes: [{ key: 'attr-key-2', value: { stringValue: '456' } }],
        },
      },
    ];
    const frameList = createTableFrameFromTraceQlQueryAsSpans(traces, defaultSettings);
    const frame = frameList[0];

    // Trace ID field
    expect(frame.fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[0].type).toBe('string');
    expect(frame.fields[0].values[0]).toBe('1');
    // Trace service field
    expect(frame.fields[1].name).toBe('traceService');
    expect(frame.fields[1].type).toBe('string');
    expect(frame.fields[1].values[0]).toBe('prometheus');
    // Trace name field
    expect(frame.fields[2].name).toBe('traceName');
    expect(frame.fields[2].type).toBe('string');
    expect(frame.fields[2].values[0]).toBe('POST /api/v1/write');
    // Span ID field
    expect(frame.fields[3].name).toBe('spanID');
    expect(frame.fields[3].type).toBe('string');
    expect(frame.fields[3].values[0]).toBe('11');
    // Time field
    expect(frame.fields[4].name).toBe('time');
    expect(frame.fields[4].type).toBe('time');
    expect(frame.fields[4].values[0]).toBe(1702984850354.934);
    // Name field
    expect(frame.fields[5].name).toBe('name');
    expect(frame.fields[5].type).toBe('string');
    expect(frame.fields[5].values[0]).toBe(undefined);
    // Dynamic fields
    expect(frame.fields[6].name).toBe('attr-key-1');
    expect(frame.fields[6].type).toBe('string');
    expect(frame.fields[6].values[0]).toBe('123');
    expect(frame.fields[6].values[1]).toBe(undefined);
    expect(frame.fields[6].values.length).toBe(2);
    expect(frame.fields[7].name).toBe('attr-key-2');
    expect(frame.fields[7].type).toBe('string');
    expect(frame.fields[7].values[0]).toBe(undefined);
    expect(frame.fields[7].values[1]).toBe('456');
    expect(frame.fields[7].values.length).toBe(2);
    // Duration field
    expect(frame.fields[8].name).toBe('duration');
    expect(frame.fields[8].type).toBe('number');
    expect(frame.fields[8].values[0]).toBe(1377608);
    // No more fields
    expect(frame.fields.length).toBe(9);
  });

  test('transforms TraceQL response to DataFrame for Spans table type', () => {
    const traces = [
      {
        traceID: '1',
        rootServiceName: 'prometheus',
        rootTraceName: 'POST /api/v1/write',
        startTimeUnixNano: '1702984850354934104',
        durationMs: 1,
        spanSets: [
          {
            spans: [
              {
                spanID: '11',
                startTimeUnixNano: '1702984850354934104',
                durationNanos: '1377608',
              },
            ],

            matched: 1,
            attributes: [{ key: 'attr-key-1', value: { intValue: '123' } }],
          },
        ],
      },
      {
        traceID: '2',
        rootServiceName: 'prometheus',
        rootTraceName: 'GET /api/v1/status/config',
        startTimeUnixNano: '1702984840786143459',
        spanSets: [
          {
            spans: [
              {
                spanID: '21',
                startTimeUnixNano: '1702984840786143459',
                durationNanos: '542316',
              },
            ],
            matched: 1,
            attributes: [{ key: 'attr-key-2', value: { stringValue: '456' } }],
          },
        ],
      },
    ];
    const frameList = createTableFrameFromTraceQlQueryAsSpans(traces, defaultSettings);
    const frame = frameList[0];

    // Trace ID field
    expect(frame.fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[0].type).toBe('string');
    expect(frame.fields[0].values[0]).toBe('1');
    // Trace service field
    expect(frame.fields[1].name).toBe('traceService');
    expect(frame.fields[1].type).toBe('string');
    expect(frame.fields[1].values[0]).toBe('prometheus');
    // Trace name field
    expect(frame.fields[2].name).toBe('traceName');
    expect(frame.fields[2].type).toBe('string');
    expect(frame.fields[2].values[0]).toBe('POST /api/v1/write');
    // Span ID field
    expect(frame.fields[3].name).toBe('spanID');
    expect(frame.fields[3].type).toBe('string');
    expect(frame.fields[3].values[0]).toBe('11');
    // Time field
    expect(frame.fields[4].name).toBe('time');
    expect(frame.fields[4].type).toBe('time');
    expect(frame.fields[4].values[0]).toBe(1702984850354.934);
    // Name field
    expect(frame.fields[5].name).toBe('name');
    expect(frame.fields[5].type).toBe('string');
    expect(frame.fields[5].values[0]).toBe(undefined);
    // Dynamic fields
    expect(frame.fields[6].name).toBe('attr-key-1');
    expect(frame.fields[6].type).toBe('string');
    expect(frame.fields[6].values[0]).toBe('123');
    expect(frame.fields[6].values[1]).toBe(undefined);
    expect(frame.fields[6].values.length).toBe(2);
    expect(frame.fields[7].name).toBe('attr-key-2');
    expect(frame.fields[7].type).toBe('string');
    expect(frame.fields[7].values[0]).toBe(undefined);
    expect(frame.fields[7].values[1]).toBe('456');
    expect(frame.fields[7].values.length).toBe(2);
    // Duration field
    expect(frame.fields[8].name).toBe('duration');
    expect(frame.fields[8].type).toBe('number');
    expect(frame.fields[8].values[0]).toBe(1377608);
    // No more fields
    expect(frame.fields.length).toBe(9);
  });

  it.each([[undefined], [[]]])('TraceQL response with no data', (traces: TraceSearchMetadata[] | undefined) => {
    const frameList = createTableFrameFromTraceQlQueryAsSpans(traces, defaultSettings);
    const frame = frameList[0];

    // Trace ID field
    expect(frame.fields[0].name).toBe('traceIdHidden');
    expect(frame.fields[0].type).toBe('string');
    expect(frame.fields[0].values).toMatchObject([]);
    // Trace service field
    expect(frame.fields[1].name).toBe('traceService');
    expect(frame.fields[1].type).toBe('string');
    expect(frame.fields[1].values).toMatchObject([]);
    // Trace name field
    expect(frame.fields[2].name).toBe('traceName');
    expect(frame.fields[2].type).toBe('string');
    expect(frame.fields[2].values).toMatchObject([]);
    // Span ID field
    expect(frame.fields[3].name).toBe('spanID');
    expect(frame.fields[3].type).toBe('string');
    expect(frame.fields[3].values).toMatchObject([]);
    // Time field
    expect(frame.fields[4].name).toBe('time');
    expect(frame.fields[4].type).toBe('time');
    expect(frame.fields[4].values).toMatchObject([]);
    // Name field
    expect(frame.fields[5].name).toBe('name');
    expect(frame.fields[5].type).toBe('string');
    expect(frame.fields[5].values).toMatchObject([]);
    // Duration field
    expect(frame.fields[6].name).toBe('duration');
    expect(frame.fields[6].type).toBe('number');
    expect(frame.fields[6].values).toMatchObject([]);
    // No more fields
    expect(frame.fields.length).toBe(7);
  });
});

describe('transformFromOTLP()', () => {
  // Mock the console error so that running the test suite doesnt throw the error
  const origError = console.error;
  const consoleErrorMock = jest.fn();
  afterEach(() => (console.error = origError));
  beforeEach(() => (console.error = consoleErrorMock));

  test('if passed bad data, will surface an error', () => {
    const res = transformFromOTLP(
      badOTLPResponse.batches as unknown as collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
      false
    );

    expect(res.data[0]).toBeFalsy();
    expect(res.error?.message).toBeTruthy();
    // if it does have resources, no error will be thrown
    expect({
      ...res.data[0],
      resources: {
        attributes: [
          { key: 'service.name', value: { stringValue: 'db' } },
          { key: 'job', value: { stringValue: 'tns/db' } },
          { key: 'opencensus.exporterversion', value: { stringValue: 'Jaeger-Go-2.22.1' } },
          { key: 'host.name', value: { stringValue: '63d16772b4a2' } },
          { key: 'ip', value: { stringValue: '0.0.0.0' } },
          { key: 'client-uuid', value: { stringValue: '39fb01637a579639' } },
        ],
      },
    }).not.toBeFalsy();
  });
});
