import {
  COLOR,
  DETAIL,
  FILLOPACITY,
  OPACITY,
  SIZE,
  STROKEOPACITY,
  STROKEWIDTH,
  UNIT_CHANNELS,
  X2,
  Y2,
} from '../src/channel.js';
import {isPositionFieldOrDatumDef} from '../src/channeldef.js';
import {defaultConfig} from '../src/config.js';
import {
  channelHasNestedOffsetScale,
  Encoding,
  extractTransformsFromEncoding,
  fieldDefs,
  initEncoding,
  markChannelCompatible,
  pathGroupingFields,
} from '../src/encoding.js';
import * as log from '../src/log/index.js';
import {CIRCLE, POINT, SQUARE, TICK} from '../src/mark.js';
import {internalField} from '../src/util.js';

describe('encoding', () => {
  describe('initEncoding', () => {
    it(
      'should drop color channel if fill is specified and filled = true',
      log.wrap((logger) => {
        const encoding = initEncoding(
          {
            color: {field: 'a', type: 'quantitative'},
            fill: {field: 'b', type: 'quantitative'},
          },
          'bar',
          true,
          defaultConfig,
        );

        expect(encoding).toEqual({
          fill: {field: 'b', type: 'quantitative'},
        });
        expect(logger.warns[0]).toEqual(log.message.droppingColor('encoding', {fill: true}));
      }),
    );

    it(
      'should replace angle channel for arc marks with theta',
      log.wrap((logger) => {
        const encoding = initEncoding(
          {
            color: {field: 'a', type: 'quantitative'},
            angle: {field: 'b', type: 'quantitative'},
          },
          'arc',
          undefined,
          defaultConfig,
        );

        expect(encoding).toEqual({
          color: {field: 'a', type: 'quantitative'},
          theta: {field: 'b', type: 'quantitative'},
        });
        expect(logger.warns[0]).toEqual(log.message.REPLACE_ANGLE_WITH_THETA);
      }),
    );

    it(
      'should drop color channel if stroke is specified and filled is false',
      log.wrap((logger) => {
        const encoding = initEncoding(
          {
            color: {field: 'a', type: 'quantitative'},
            stroke: {field: 'b', type: 'quantitative'},
          },
          'point',
          false,
          defaultConfig,
        );

        expect(encoding).toEqual({
          stroke: {field: 'b', type: 'quantitative'},
        });
        expect(logger.warns[0]).toEqual(log.message.droppingColor('encoding', {stroke: true}));
      }),
    );

    it(
      'drops xOffset if x is continuous',
      log.wrap((logger) => {
        const encoding = initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            xOffset: {field: 'b', type: 'quantitative'},
          },
          'point',
          false,
          defaultConfig,
        );

        expect(encoding).toEqual({
          x: {field: 'a', type: 'quantitative'},
        });
        expect(logger.warns[0]).toEqual(log.message.offsetNestedInsideContinuousPositionScaleDropped('x'));
      }),
    );

    it(
      'drops xOffset if x is binned quantitative',
      log.wrap((logger) => {
        const encoding = initEncoding(
          {
            x: {field: 'a', type: 'quantitative', bin: true},
            xOffset: {field: 'b', type: 'nominal'},
          },
          'point',
          false,
          defaultConfig,
        );

        expect(encoding).toEqual({
          x: {field: 'a', type: 'quantitative', bin: {maxbins: 10}},
        });
        expect(logger.warns[0]).toEqual(log.message.offsetNestedInsideContinuousPositionScaleDropped('x'));
      }),
    );

    it('does not drop xOffset if x is time with timeUnit', () => {
      const encoding = initEncoding(
        {
          x: {field: 'a', type: 'temporal', timeUnit: 'year'},
          xOffset: {field: 'b', type: 'nominal'},
        },
        'point',
        false,
        defaultConfig,
      );

      expect(encoding).toEqual({
        x: {field: 'a', type: 'temporal', timeUnit: {unit: 'year'}},
        xOffset: {field: 'b', type: 'nominal'},
      });
    });
  });

  describe('extractTransformsFromEncoding', () => {
    it('should indlude axis in extracted encoding', () => {
      const encoding = extractTransformsFromEncoding(
        {
          x: {field: 'dose', type: 'ordinal', axis: {labelAngle: 15}},
          y: {field: 'response', type: 'quantitative'},
        },
        defaultConfig,
      ).encoding;

      const x = encoding.x;
      expect(x).toBeDefined();
      if (isPositionFieldOrDatumDef(x)) {
        expect(x.axis).toBeDefined();
        expect(x.axis.labelAngle).toBe(15);
      } else {
        expect(false).toBe(true);
      }
    });
    it('should extract time unit from encoding field definition and add axis format', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {timeUnit: 'yearmonthdatehoursminutes', field: 'a', type: 'temporal'},
            y: {field: 'b', type: 'quantitative'},
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [],
        timeUnits: [{timeUnit: {unit: 'yearmonthdatehoursminutes'}, field: 'a', as: 'yearmonthdatehoursminutes_a'}],
        aggregate: [],
        groupby: ['yearmonthdatehoursminutes_a', 'b'],
        encoding: {
          x: {
            field: 'yearmonthdatehoursminutes_a',
            type: 'temporal',
            title: 'a (year-month-date-hours-minutes)',
          },
          y: {field: 'b', type: 'quantitative'},
        },
      });
    });
    it('should produce format and formatType in axis when there is timeUnit', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {timeUnit: 'year', field: 'b', type: 'ordinal'},
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );

      expect(output.encoding.y).toEqual({
        axis: {
          formatType: 'time',
        },
        field: 'year_b',
        title: 'b (year)',
        type: 'ordinal',
      });
    });
    it('should not produce formatType in axis when there is timeUnit with type temporal', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {timeUnit: 'year', field: 'b', type: 'temporal'},
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );

      expect(output.encoding.y).toEqual({
        field: 'year_b',
        title: 'b (year)',
        type: 'temporal',
      });
    });
    it('should produce format and formatType in legend when there is timeUnit', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {field: 'b', type: 'ordinal'},
            detail: {field: 'c', timeUnit: 'month', type: 'nominal'},
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );

      expect(output.encoding.detail).toEqual({
        legend: {
          formatType: 'time',
        },
        field: 'month_c',
        title: 'c (month)',
        type: 'nominal',
      });
    });
    it('should not produce formatType in legend when there is timeUnit with type temporal', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {field: 'b', type: 'ordinal'},
            detail: {field: 'c', timeUnit: 'month', type: 'temporal'},
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );

      expect(output.encoding.detail).toEqual({
        field: 'month_c',
        title: 'c (month)',
        type: 'temporal',
      });
    });
    it('should produce format and formatType when there is timeUnit in tooltip channel or tooltip channel', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {field: 'b', type: 'ordinal'},
            tooltip: {field: 'c', timeUnit: 'month', type: 'nominal'},
            text: {field: 'c', timeUnit: 'month', type: 'nominal'},
          },
          'text',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output.encoding.tooltip).toEqual({
        formatType: 'time',
        field: 'month_c',
        title: 'c (month)',
        type: 'nominal',
      });
      expect(output.encoding.text).toEqual({
        formatType: 'time',
        field: 'month_c',
        title: 'c (month)',
        type: 'nominal',
      });
    });
    it('should extract aggregates from encoding', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {
              aggregate: 'max',
              field: 'b',
              type: 'quantitative',
            },
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [],
        timeUnits: [],
        aggregate: [{op: 'max', field: 'b', as: 'max_b'}],
        groupby: ['a'],
        encoding: {
          x: {field: 'a', type: 'quantitative'},
          y: {
            field: 'max_b',
            type: 'quantitative',
            title: 'Max of b',
          },
        },
      });
    });
    it('should extract aggregates with exponential operations from encoding', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {
              aggregate: {exponential: 0.3},
              field: 'b',
              type: 'quantitative',
            },
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [],
        timeUnits: [],
        aggregate: [{op: {exponential: 0.3}, field: 'b', as: 'exponential_b'}],
        groupby: ['a'],
        encoding: {
          x: {field: 'a', type: 'quantitative'},
          y: {
            field: 'exponential_b',
            type: 'quantitative',
            title: 'Exponential of b',
          },
        },
      });
    });
    it('should extract aggregates with exponentialb operations from encoding', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {
              aggregate: {exponentialb: 0.3},
              field: 'b',
              type: 'quantitative',
            },
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [],
        timeUnits: [],
        aggregate: [{op: {exponentialb: 0.3}, field: 'b', as: 'exponentialb_b'}],
        groupby: ['a'],
        encoding: {
          x: {field: 'a', type: 'quantitative'},
          y: {
            field: 'exponentialb_b',
            type: 'quantitative',
            title: 'Exponentialb of b',
          },
        },
      });
    });
    it('should extract binning from encoding', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'ordinal', bin: true},
            y: {type: 'quantitative', aggregate: 'count'},
          },
          'bar',
          true,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [{bin: {maxbins: 10}, field: 'a', as: 'bin_maxbins_10_a'}],
        timeUnits: [],
        aggregate: [{op: 'count', as: internalField('count')}],
        groupby: ['bin_maxbins_10_a', 'bin_maxbins_10_a_end', 'bin_maxbins_10_a_range'],
        encoding: {
          x: {field: 'bin_maxbins_10_a', type: 'quantitative', title: 'a (binned)', bin: 'binned'},
          x2: {field: 'bin_maxbins_10_a_end'},
          y: {field: internalField('count'), type: 'quantitative', title: 'Count of Records'},
        },
      });
    });
    it('should preserve auxiliary properties (i.e. axis) in encoding', () => {
      const output = extractTransformsFromEncoding(
        initEncoding(
          {
            x: {field: 'a', type: 'quantitative'},
            y: {
              aggregate: 'mean',
              field: 'b',
              type: 'quantitative',
              title: 'foo',
              axis: {title: 'foo', format: '.2e'},
            },
          },
          'line',
          false,
          defaultConfig,
        ),
        defaultConfig,
      );
      expect(output).toEqual({
        bins: [],
        timeUnits: [],
        aggregate: [{op: 'mean', field: 'b', as: 'mean_b'}],
        groupby: ['a'],
        encoding: {
          x: {field: 'a', type: 'quantitative'},
          y: {
            field: 'mean_b',
            type: 'quantitative',
            title: 'foo',
            axis: {title: 'foo', format: '.2e'},
          },
        },
      });
    });
  });

  describe('markChannelCompatible', () => {
    it('should support x2 for circle, point, square and tick mark with binned data', () => {
      const encoding: Encoding<string> = {
        x: {
          field: 'bin_start',
          bin: 'binned',
          type: 'quantitative',
          axis: {
            tickMinStep: 2,
          },
        },
        x2: {
          field: 'bin_end',
        },
        y: {
          field: 'count',
          type: 'quantitative',
        },
      };
      expect(markChannelCompatible(encoding, X2, CIRCLE)).toBe(true);
      expect(markChannelCompatible(encoding, X2, POINT)).toBe(true);
      expect(markChannelCompatible(encoding, X2, SQUARE)).toBe(true);
      expect(markChannelCompatible(encoding, X2, TICK)).toBe(true);
    });

    it('should support y2 for circle, point, square and tick mark with binned data', () => {
      const encoding: Encoding<string> = {
        y: {
          field: 'bin_start',
          bin: 'binned',
          type: 'quantitative',
          axis: {
            tickMinStep: 2,
          },
        },
        y2: {
          field: 'bin_end',
        },
        x: {
          field: 'count',
          type: 'quantitative',
        },
      };
      expect(markChannelCompatible(encoding, Y2, CIRCLE)).toBe(true);
      expect(markChannelCompatible(encoding, Y2, POINT)).toBe(true);
      expect(markChannelCompatible(encoding, Y2, SQUARE)).toBe(true);
      expect(markChannelCompatible(encoding, Y2, TICK)).toBe(true);
    });

    it('should not support x2 for circle, point, square and tick mark without binned data', () => {
      const encoding: Encoding<string> = {
        x: {
          field: 'bin_start',
          type: 'quantitative',
          axis: {
            tickMinStep: 2,
          },
        },
        x2: {
          field: 'bin_end',
        },
        y: {
          field: 'count',
          type: 'quantitative',
        },
      };
      expect(markChannelCompatible(encoding, X2, CIRCLE)).toBe(false);
      expect(markChannelCompatible(encoding, X2, POINT)).toBe(false);
      expect(markChannelCompatible(encoding, X2, SQUARE)).toBe(false);
      expect(markChannelCompatible(encoding, X2, TICK)).toBe(false);
    });

    it('should not support y2 for circle, point, square and tick mark with binned data', () => {
      const encoding: Encoding<string> = {
        y: {
          field: 'bin_start',
          type: 'quantitative',
          axis: {
            tickMinStep: 2,
          },
        },
        y2: {
          field: 'bin_end',
        },
        x: {
          field: 'count',
          type: 'quantitative',
        },
      };
      expect(markChannelCompatible(encoding, Y2, CIRCLE)).toBe(false);
      expect(markChannelCompatible(encoding, Y2, POINT)).toBe(false);
      expect(markChannelCompatible(encoding, Y2, SQUARE)).toBe(false);
      expect(markChannelCompatible(encoding, Y2, TICK)).toBe(false);
    });
  });

  describe('pathGroupingFields()', () => {
    it('should return fields for unaggregate detail, color, size, opacity fieldDefs.', () => {
      for (const channel of [DETAIL, COLOR, SIZE, OPACITY, FILLOPACITY, STROKEOPACITY, STROKEWIDTH]) {
        expect(pathGroupingFields('line', {[channel]: {field: 'a', type: 'nominal'}})).toEqual(['a']);
      }
    });

    it('should not return a field for size of a trail mark.', () => {
      expect(pathGroupingFields('trail', {size: {field: 'a', type: 'nominal'}})).toEqual([]);
    });

    it('should not return fields for aggregate detail, color, size, opacity fieldDefs.', () => {
      for (const channel of [DETAIL, COLOR, SIZE, OPACITY, FILLOPACITY, STROKEOPACITY, STROKEWIDTH]) {
        expect(pathGroupingFields('line', {[channel]: {aggregate: 'mean', field: 'a', type: 'nominal'}})).toEqual([]);
      }
    });

    it('should return condition detail fields for color, size, shape', () => {
      for (const channel of [COLOR, SIZE, OPACITY, FILLOPACITY, STROKEOPACITY, STROKEWIDTH]) {
        expect(
          pathGroupingFields('line', {
            [channel]: {
              condition: {param: 'sel', field: 'a', type: 'nominal'},
            },
          }),
        ).toEqual(['a']);
      }
    });

    it('should not return errors for all channels', () => {
      for (const channel of UNIT_CHANNELS) {
        expect(() => {
          pathGroupingFields('line', {
            [channel]: {field: 'a', type: 'nominal'},
          });
        }).not.toThrow();
      }
    });

    it('should not include fields from tooltip', () => {
      expect(pathGroupingFields('line', {tooltip: {field: 'a', type: 'nominal'}})).toEqual([]);
    });

    it('should not group line/area/trail by the main channel when using an offset field', () => {
      for (const mark of ['line', 'area', 'trail'] as const) {
        expect(
          pathGroupingFields(mark, {
            x: {field: 'a', type: 'nominal'},
            y: {field: 'c', type: 'nominal'},
            yOffset: {field: 'b', type: 'quantitative'},
          }),
        ).not.toEqual(['c']);
        expect(
          pathGroupingFields(mark, {
            x: {field: 'a', type: 'nominal'},
            y: {field: 'c', type: 'nominal'},
            xOffset: {field: 'b', type: 'quantitative'},
          }),
        ).not.toEqual(['a']);
      }
    });

    it('should group by an explicit detail field when using an offset field', () => {
      for (const mark of ['line', 'area', 'trail'] as const) {
        expect(
          pathGroupingFields(mark, {
            x: {field: 'a', type: 'nominal'},
            y: {field: 'c', type: 'nominal'},
            yOffset: {field: 'b', aggregate: 'sum', type: 'quantitative'},
            detail: {field: 'c', type: 'nominal'},
          }),
        ).toEqual(['c']);
      }
    });
  });

  describe('fieldDefs', () => {
    it('should return field defs', () => {
      expect(
        fieldDefs<string>({
          x: {field: 'foo', type: 'quantitative'},
          color: {
            condition: {
              test: 'datum.val > 12',
              field: 'bar',
              type: 'quantitative',
            },
            value: 'red',
          },
        }),
      ).toEqual([
        {field: 'foo', type: 'quantitative'},
        {field: 'bar', test: 'datum.val > 12', type: 'quantitative'},
      ]);
    });
  });

  describe('channelHasNestedOffsetScale', () => {
    it('returns true for nominal x with xOffset', () => {
      expect(
        channelHasNestedOffsetScale<string>(
          {x: {field: 'a', type: 'nominal'}, xOffset: {field: 'b', type: 'nominal'}},
          'x',
        ),
      ).toBe(true);
    });

    it('returns true for ordinal x with xOffset', () => {
      expect(
        channelHasNestedOffsetScale<string>(
          {x: {field: 'a', type: 'ordinal'}, xOffset: {field: 'b', type: 'nominal'}},
          'x',
        ),
      ).toBe(true);
    });

    it('returns false for binned quantitative x with xOffset (type-only check ignores binning)', () => {
      expect(
        channelHasNestedOffsetScale<string>(
          {x: {field: 'a', type: 'quantitative', bin: true}, xOffset: {field: 'b', type: 'nominal'}},
          'x',
        ),
      ).toBe(false);
    });

    it('returns false for unbinned quantitative x with xOffset', () => {
      expect(
        channelHasNestedOffsetScale<string>(
          {x: {field: 'a', type: 'quantitative'}, xOffset: {field: 'b', type: 'nominal'}},
          'x',
        ),
      ).toBe(false);
    });

    it('returns false when offset channel is missing', () => {
      expect(channelHasNestedOffsetScale<string>({x: {field: 'a', type: 'nominal'}}, 'x')).toBe(false);
    });

    it('returns true for temporal x with timeUnit and xOffset', () => {
      expect(
        channelHasNestedOffsetScale<string>(
          {x: {field: 'a', type: 'temporal', timeUnit: 'year'}, xOffset: {field: 'b', type: 'nominal'}},
          'x',
        ),
      ).toBe(true);
    });
  });
});