import { lastValueFrom, of } from 'rxjs';

import {
  type DataQueryRequest,
  type DataSourceInstanceSettings,
  dateTime,
  FieldType,
  type PluginMetaInfo,
  PluginType,
  type ScopedVars,
} from '@grafana/data';
import { type BackendSrv, DataSourceWithBackend, type TemplateSrv } from '@grafana/runtime';

import { JaegerDatasource, type JaegerJsonData } from './datasource';
import mockJson from './mockJsonResponse.json';
import mockSearchResponse from './mockSearchResponse.json';
import mockTraceResponse from './mockTraceResponse.json';
import { type JaegerQuery } from './types';

export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv;

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  getBackendSrv: () => backendSrv,
  getTemplateSrv: () => ({
    replace: (val: string, subs: ScopedVars): string => {
      return subs[val]?.value ?? val;
    },
    containsTemplate: (val: string): boolean => {
      return val.includes('$');
    },
  }),
}));

describe('upload, search and trace query types', () => {
  it('should process valid JSON file uploads', async () => {
    const ds = new JaegerDatasource(defaultSettings);
    ds.uploadedJson = JSON.stringify(mockJson);
    const response = await lastValueFrom(ds.query({ ...defaultQuery, targets: [{ queryType: 'upload', refId: 'A' }] }));
    const field = response.data[0].fields[0];
    expect(field.name).toBe('traceID');
    expect(field.type).toBe(FieldType.string);
    expect(field.values.length).toBe(2);
  });

  it('should reject invalid JSON file uploads', async () => {
    const ds = new JaegerDatasource(defaultSettings);
    ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] });
    const response = await lastValueFrom(
      ds.query({ targets: [{ queryType: 'upload', refId: 'A' }] } as DataQueryRequest<JaegerQuery>)
    );
    expect(response.error?.message).toBe('The JSON file uploaded is not in a valid Jaeger format');
    expect(response.data.length).toBe(0);
  });

  it('should return search results when query type is search', async () => {
    setupQueryMock('search');
    const ds = new JaegerDatasource(defaultSettings);
    const response = await lastValueFrom(
      ds.query({
        ...defaultQuery,
        targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: '/api/services' }],
      })
    );

    expect(response.data[0].meta.preferredVisualisationType).toBe('table');
    expect(response.data[0].fields[0].config.links).toHaveLength(1);
    expect(response.data[0].fields[0].name).toBe('traceID');
  });

  it('should return trace results when query type is trace', async () => {
    setupQueryMock('trace');
    const ds = new JaegerDatasource(defaultSettings);
    const response = await lastValueFrom(
      ds.query({ ...defaultQuery, targets: [{ queryType: undefined, refId: 'a', query: '12345' }] })
    );

    expect(response.data[0].meta.preferredVisualisationType).toBe('trace');
    expect(response.data[0].fields.length).toBe(7);
  });
});

describe('template variable interpolation', () => {
  it('should interpolate template variables in TraceID queries', () => {
    const templateSrv = {
      replace: (value: string, scopedVars: ScopedVars): string => {
        return value.replace(/\$\{(\w+)\}|\$(\w+)/g, (_match, var1, var2) => {
          const key = var1 || var2;
          const scopedVar = scopedVars[key];
          return scopedVar?.value ?? '';
        });
      },
      containsTemplate: (val: string): boolean => {
        return val.includes('$');
      },
    } as TemplateSrv;

    const ds = new JaegerDatasource(defaultSettings, templateSrv);

    const interpolated = ds.interpolateVariablesInQueries(
      [
        {
          refId: 'A',
          queryType: undefined,
          query: '${traceID}',
        } as JaegerQuery,
      ],
      {
        traceID: { value: 'Yyij7GaxbMkkus6NJEcAr6UedJxTCuYl' },
      } as unknown as ScopedVars
    );

    expect(interpolated[0].query).toBe('Yyij7GaxbMkkus6NJEcAr6UedJxTCuYl');
  });
});

describe('node graph functionality', () => {
  it('should include node graph frames when nodeGraph is enabled for trace queries', async () => {
    const settingsWithNodeGraph = {
      ...defaultSettings,
      jsonData: {
        ...defaultSettings.jsonData,
        nodeGraph: { enabled: true },
      },
    };

    const ds = new JaegerDatasource(settingsWithNodeGraph);
    setupQueryMock('trace');

    const response = await lastValueFrom(
      ds.query({
        ...defaultQuery,
        targets: [
          {
            query: '12345',
            refId: '1',
          },
        ],
      })
    );

    expect(response.data.length).toBe(3);
  });

  it('should exclude node graph frames when nodeGraph is disabled for trace queries', async () => {
    const settingsWithoutNodeGraph = {
      ...defaultSettings,
      jsonData: {
        ...defaultSettings.jsonData,
        nodeGraph: { enabled: false },
      },
    };

    const ds = new JaegerDatasource(settingsWithoutNodeGraph);
    setupQueryMock('trace');

    const response = await lastValueFrom(
      ds.query({
        ...defaultQuery,
        targets: [
          {
            query: '12345',
            refId: '1',
          },
        ],
      })
    );

    expect(response.data.length).toBe(1);
  });
});

function setupQueryMock(type: 'trace' | 'search') {
  return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => {
    if (type === 'search') {
      return of(mockSearchResponse);
    } else {
      return of(mockTraceResponse);
    }
  });
}

const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
  uid: '0',
  type: 'tracing',
  name: 'jaeger',
  url: 'http://grafana.com',
  access: 'proxy',
  meta: {
    id: 'jaeger',
    name: 'jaeger',
    type: PluginType.datasource,
    info: {} as PluginMetaInfo,
    module: '',
    baseUrl: '',
  },
  jsonData: {
    nodeGraph: {
      enabled: true,
    },
  },
  readOnly: false,
};

const defaultQuery: DataQueryRequest<JaegerQuery> = {
  requestId: '1',
  interval: '0',
  intervalMs: 10,
  panelId: 0,
  scopedVars: {},
  range: {
    from: dateTime().subtract(1, 'h'),
    to: dateTime(),
    raw: { from: '1h', to: 'now' },
  },
  timezone: 'browser',
  app: 'explore',
  startTime: 0,
  targets: [
    {
      query: '12345',
      refId: '1',
    },
  ],
};
