import { DataFrameType, type Field, FieldType, type LogRowModel, MutableDataFrame } from '@grafana/data';
import { mockTimeRange } from '@grafana/plugin-ui/test';
import { setTemplateSrv } from '@grafana/runtime';
import { type ExploreFieldLinkModel, getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { type GetFieldLinksFn } from 'app/plugins/panel/logs/types';

import { getAllFields, createLogLineLinks, type FieldDef, getDataframeFields } from './logParser';
import { createLogRow } from './mocks/logRow';

describe('logParser', () => {
  describe('getAllFields', () => {
    it('should filter out fields with data links that have a nullish value', () => {
      const createScenario = (value: unknown) =>
        createLogRow({
          entryFieldIndex: 1,
          rowIndex: 0,
          dataFrame: {
            refId: 'A',
            fields: [
              testTimeField,
              testLineField,
              {
                name: 'link',
                type: FieldType.string,
                config: {
                  links: [
                    {
                      title: 'link1',
                      url: 'https://example.com',
                    },
                  ],
                },
                values: [value],
              },
            ],
            length: 1,
          },
        });

      expect(getAllFields(createScenario(null))).toHaveLength(0);
      expect(getAllFields(createScenario(undefined))).toHaveLength(0);
      expect(getAllFields(createScenario(''))).toHaveLength(1);
      expect(getAllFields(createScenario('test'))).toHaveLength(1);
      // technically this is a field-type-string, but i will add more
      // falsy-values, just to be sure
      expect(getAllFields(createScenario(false))).toHaveLength(1);
      expect(getAllFields(createScenario(NaN))).toHaveLength(1);
      expect(getAllFields(createScenario(0))).toHaveLength(1);
      expect(getAllFields(createScenario(-0))).toHaveLength(1);
    });

    it('should filter out field with labels in frame', () => {
      const logRow = createLogRow({
        entryFieldIndex: 1,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [
            testTimeField,
            testLineField,
            testStringField,
            {
              name: 'labels',
              type: FieldType.other,
              config: {},
              values: [{ place: 'luna', source: 'data' }],
            },
          ],
        }),
      });

      const fields = getAllFields(logRow);
      expect(fields.length).toBe(1);
      expect(fields.find((field) => field.keys[0] === 'labels')).toBe(undefined);
    });

    it('should not filter out field with labels name in not-old-loki-style frame', () => {
      const logRow = createLogRow({
        entryFieldIndex: 1,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [
            testTimeField,
            testLineField,
            testStringField,
            {
              name: 'labels',
              type: FieldType.string,
              config: {},
              values: [{ place: 'luna', source: 'data' }],
            },
          ],
        }),
      });
      const fields = getAllFields(logRow);
      expect(fields.length).toBe(2);
      expect(fields.find((field) => field.keys[0] === 'labels')).not.toBe(undefined);
    });

    it('should not filter out field with labels name and other type and datalinks', () => {
      const logRow = createLogRow({
        entryFieldIndex: 1,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [
            testTimeField,
            testLineField,
            testStringField,
            {
              name: 'labels',
              type: FieldType.other,
              config: {
                links: [
                  {
                    title: 'test1',
                    url: 'url1',
                  },
                ],
              },
              values: [{ place: 'luna', source: 'data' }],
            },
          ],
        }),
      });
      const fields = getAllFields(logRow);
      expect(fields.length).toBe(2);
      expect(fields.find((field) => field.keys[0] === 'labels')).not.toBe(undefined);
    });

    it('should filter out field with id name', () => {
      const logRow = createLogRow({
        entryFieldIndex: 1,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [
            testTimeField,
            testLineField,
            testStringField,
            {
              name: 'id',
              type: FieldType.string,
              config: {},
              values: ['1659620138401000000_8b1f7688_'],
            },
          ],
        }),
      });

      const fields = getAllFields(logRow);
      expect(fields.length).toBe(1);
      expect(fields.find((field) => field.keys[0] === 'id')).toBe(undefined);
    });

    it('should filter out field with config hidden field', () => {
      const testField = { ...testStringField };
      testField.config = {
        custom: {
          hidden: true,
        },
      };
      const logRow = createLogRow({
        entryFieldIndex: 10,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [testTimeField, testLineField, { ...testField }],
        }),
      });

      const fields = getAllFields(logRow);
      expect(fields.length).toBe(0);
      expect(fields.find((field) => field.keys[0] === testField.name)).toBe(undefined);
    });

    it('should filter out field with null values', () => {
      const logRow = createLogRow({
        entryFieldIndex: 10,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [testTimeField, testLineField, { ...testFieldWithNullValue }],
        }),
      });

      const fields = getAllFields(logRow);
      expect(fields.length).toBe(0);
      expect(fields.find((field) => field.keys[0] === testFieldWithNullValue.name)).toBe(undefined);
    });

    it('should not filter out field with string values', () => {
      const logRow = createLogRow({
        entryFieldIndex: 1,
        dataFrame: new MutableDataFrame({
          refId: 'A',
          fields: [testTimeField, testLineField, { ...testStringField }],
        }),
      });

      const fields = getAllFields(logRow);
      expect(fields.length).toBe(1);
      expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined);
    });

    describe('dataplane frames', () => {
      const makeLogRow = (fields: Field[], entryFieldIndex: number): LogRowModel =>
        createLogRow({
          entryFieldIndex,
          rowIndex: 0,
          dataFrame: {
            refId: 'A',
            fields,
            length: fields[0]?.values.length,
            meta: {
              type: DataFrameType.LogLines,
            },
          },
        });

      const expectHasField = (defs: FieldDef[], name: string): void => {
        expect(defs.find((field) => field.keys[0] === name)).not.toBe(undefined);
      };

      it('should filter out fields with data links that have a nullish value', () => {
        const createScenario = (value: unknown) =>
          makeLogRow(
            [
              testTimeField,
              testLineField,
              {
                name: 'link',
                type: FieldType.string,
                config: {
                  links: [
                    {
                      title: 'link1',
                      url: 'https://example.com',
                    },
                  ],
                },
                values: [value],
              },
            ],
            1
          );

        expect(getAllFields(createScenario(null))).toHaveLength(0);
        expect(getAllFields(createScenario(undefined))).toHaveLength(0);
        expect(getAllFields(createScenario(''))).toHaveLength(1);
        expect(getAllFields(createScenario('test'))).toHaveLength(1);
        // technically this is a field-type-string, but i will add more
        // falsy-values, just to be sure
        expect(getAllFields(createScenario(false))).toHaveLength(1);
        expect(getAllFields(createScenario(NaN))).toHaveLength(1);
        expect(getAllFields(createScenario(0))).toHaveLength(1);
        expect(getAllFields(createScenario(-0))).toHaveLength(1);
      });

      it('should filter out system-fields without data-links, but should keep severity', () => {
        const row = makeLogRow(
          [
            testTimeField,
            testLineField,
            {
              config: {},
              name: 'id',
              type: FieldType.string,
              values: ['id1'],
            },
            {
              config: {},
              name: 'labels',
              type: FieldType.other,
              values: [{ a: 1, b: 2 }],
            },
            {
              config: {},
              name: 'severity',
              type: FieldType.string,
              values: ['info'],
            },
            testStringField,
          ],
          1
        );

        const output = getAllFields(row);

        expect(output).toHaveLength(2);
        expectHasField(output, 'test_field_string');
        expectHasField(output, 'severity');
      });

      it('should keep system fields with data-links', () => {
        const links = [
          {
            title: 'link1',
            url: 'https://example.com',
          },
        ];

        const row = makeLogRow(
          [
            {
              ...testTimeField,
              config: { links },
            },
            {
              ...testLineField,
              config: { links },
            },
            {
              config: { links },
              name: 'id',
              type: FieldType.string,
              values: ['id1'],
            },
            {
              config: { links },
              name: 'labels',
              type: FieldType.other,
              values: [{ a: 1, b: 2 }],
            },
            {
              config: { links },
              name: 'severity',
              type: FieldType.string,
              values: ['info'],
            },
          ],
          1
        );

        const output = getAllFields(row);

        expect(output).toHaveLength(5);
        expectHasField(output, 'timestamp');
        expectHasField(output, 'body');
        expectHasField(output, 'id');
        expectHasField(output, 'labels');
        expectHasField(output, 'severity');
      });

      it('should filter out config-hidden fields', () => {
        const row = makeLogRow(
          [
            testTimeField,
            testLineField,
            {
              ...testStringField,
              config: {
                custom: {
                  hidden: true,
                },
              },
            },
          ],
          1
        );

        const output = getAllFields(row);

        expect(output).toHaveLength(0);
      });

      it('should filter out fields with null values', () => {
        const row = makeLogRow(
          [
            testTimeField,
            testLineField,
            {
              // null-value
              config: {},
              type: FieldType.string,
              name: 'test1',
              values: [null],
            },
            {
              // null-value and data-link
              config: {
                links: [
                  {
                    title: 'link1',
                    url: 'https://example.com',
                  },
                ],
              },
              type: FieldType.string,
              name: 'test2',
              values: [null],
            },
            {
              // normal value
              config: {},
              type: FieldType.string,
              name: 'test3',
              values: ['testvalue'],
            },
          ],
          1
        );

        const output = getAllFields(row);

        expect(output).toHaveLength(1);
        expectHasField(output, 'test3');
      });
    });
  });

  describe('createLogLineLinks', () => {
    it('should change FieldDef to have keys of variable keys', () => {
      const variableLink: ExploreFieldLinkModel = {
        href: 'test',
        onClick: () => {},
        origin: {
          config: { links: [] },
          name: 'Line',
          type: FieldType.string,
          values: ['a', 'b'],
        },
        title: 'test',
        target: '_self',
        variables: [
          { variableName: 'path', value: 'test', match: '${path}', found: true },
          { variableName: 'msg', value: 'test msg', match: '${msg}', found: true },
        ],
      };

      const fieldWithVarLink: FieldDef = {
        fieldIndex: 2,
        keys: ['Line'],
        values: ['level=info msg="test msg" status_code=200 url=http://test'],
        links: [variableLink],
      };

      const fields = createLogLineLinks([fieldWithVarLink]);
      expect(fields.length).toBe(1);
      expect(fields[0].keys.length).toBe(2);
      expect(fields[0].keys[0]).toBe('path');
      expect(fields[0].values[0]).toBe('test');
      expect(fields[0].keys[1]).toBe('msg');
      expect(fields[0].values[1]).toBe('test msg');
    });

    it('should return empty array if no variables', () => {
      const variableLink: ExploreFieldLinkModel = {
        href: 'test',
        onClick: () => {},
        origin: {
          config: { links: [] },
          name: 'Line',
          type: FieldType.string,
          values: ['a', 'b'],
        },
        title: 'test',
        target: '_self',
        variables: [],
      };

      const fieldWithVarLink: FieldDef = {
        fieldIndex: 2,
        keys: ['Line'],
        values: ['level=info msg="test msg" status_code=200 url=http://test'],
        links: [variableLink],
      };

      const fields = createLogLineLinks([fieldWithVarLink]);
      expect(fields.length).toBe(0);
    });
  });

  describe('getDataframeFields', () => {
    beforeEach(() => {
      setTemplateSrv(new TemplateSrv());
    });

    it('should add row labels as variables for links', () => {
      const row = createLogRow({
        labels: { service_name: 'checkout', service_namespace: 'prod' },
        dataFrame: {
          refId: 'A',
          fields: [
            testTimeField,
            testLineField,
            {
              name: 'link',
              type: FieldType.string,
              config: {
                links: [
                  {
                    title: 'link1',
                    url: 'https://service.com/${__labels.tags["service_namespace"]}/${__labels.tags["service_name"]}',
                  },
                ],
              },
              values: ['some'],
            },
          ],
          length: 1,
        },
      });

      const getFieldLinks: GetFieldLinksFn = (field, rowIndex, dataFrame, vars) => {
        return getFieldLinksForExplore({
          field,
          rowIndex,
          range: mockTimeRange(),
          dataFrame: dataFrame,
          vars,
        });
      };

      const fields = getDataframeFields(row, getFieldLinks);
      expect(fields).toHaveLength(1);
      expect(fields[0].links).toHaveLength(1);
      expect(fields[0].links).toMatchObject([
        {
          href: 'https://service.com/prod/checkout',
          variables: [
            {
              fieldPath: 'tags["service_namespace"]',
              format: undefined,
              found: true,
              match: '${__labels.tags["service_namespace"]}',
              value: 'prod',
              variableName: '__labels',
            },
          ],
        },
      ]);
    });
  });
});

const testTimeField = {
  name: 'timestamp',
  type: FieldType.time,
  config: {},
  values: [1],
};

const testLineField = {
  name: 'body',
  type: FieldType.string,
  config: {},
  values: ['line1'],
};

const testStringField = {
  name: 'test_field_string',
  type: FieldType.string,
  config: {},
  values: ['abc'],
};

const testFieldWithNullValue = {
  name: 'test_field_null',
  type: FieldType.string,
  config: {},
  values: [null],
};
