import {describe, expectTypeOf, test} from 'vitest';
import type {ExpressionInputType, ExpressionSpecification} from '../types.g';

describe('Distance expression', () => {
    describe('Invalid expression', () => {
        test('missing geometry typecheck', () => {
            expectTypeOf<['distance']>().not.toExtend<ExpressionSpecification>();
        });
        test('invalid geometry typecheck', () => {
            expectTypeOf<['distance', {type: 'Nope!'}]>().not.toExtend<ExpressionSpecification>();
        });
        test('expression as geometry typecheck', () => {
            expectTypeOf<
                ['distance', ['literal', {type: 'MultiPoint'; coordinates: [[3, 3], [3, 4]]}]]
            >().not.toExtend<ExpressionSpecification>();
        });
    });

    describe('valid expression', () => {
        test('multi point geometry typecheck', () => {
            expectTypeOf<
                ['distance', {type: 'MultiPoint'; coordinates: [[3, 3], [3, 4]]}]
            >().toExtend<ExpressionSpecification>();
        });
        test('multi line geometry typecheck', () => {
            expectTypeOf<
                ['distance', {type: 'MultiLineString'; coordinates: [[[3, 3], [3, 4]]]}]
            >().toExtend<ExpressionSpecification>();
        });
        test('multi polygon geometry typecheck', () => {
            expectTypeOf<
                [
                    'distance',
                    {
                        type: 'MultiPolygon';
                        coordinates: [[[[3, 3], [3, 4], [4, 4], [4, 3], [3, 3]]]];
                    }
                ]
            >().toExtend<ExpressionSpecification>();
        });
    });
});

describe('"array" expression', () => {
    test('type requires an expression as the input value', () => {
        expectTypeOf<['array', 1, 2, 3]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<['array', [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<['array', 'number', [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<['array', 'number', 3, [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();

        expectTypeOf<['array', ['literal', []]]>().toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['array', 'number', ['literal', [1, 2, 3]]]
        >().toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['array', 'number', typeof Number.MAX_SAFE_INTEGER, ['get', 'arr']]
        >().toExtend<ExpressionSpecification>();
    });
    test('type requires either "string", "number", or "boolean" as the asserted type', () => {
        expectTypeOf<['array', 0, ['literal', []]]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<['array', '0', ['literal', []]]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['array', ['literal', 'number'], ['literal', []]]
        >().not.toExtend<ExpressionSpecification>();

        expectTypeOf<['array', 'string', ['literal', []]]>().toExtend<ExpressionSpecification>();
        expectTypeOf<['array', 'number', ['literal', []]]>().toExtend<ExpressionSpecification>();
        expectTypeOf<['array', 'boolean', ['literal', []]]>().toExtend<ExpressionSpecification>();
    });
    test('type requires a number literal as the asserted length', () => {
        expectTypeOf<
            ['array', 'string', '0', ['literal', []]]
        >().not.toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['array', 'string', ['literal', 0], ['literal', []]]
        >().not.toExtend<ExpressionSpecification>();

        expectTypeOf<['array', 'string', 0, ['literal', []]]>().toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['array', 'string', 2, ['literal', ['one', 'two']]]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"format" expression', () => {
    test('type rejects bare string arrays in the "text-font" style override', () => {
        expectTypeOf<
            ['format', 'foo', {'text-font': ['Helvetica', 'Arial']}]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which scales text', () => {
        expectTypeOf<
            ['format', ['get', 'title'], {'font-scale': 0.8}]
        >().toExtend<ExpressionSpecification>();
    });
    test('type requires either "bottom", "center", or "top" as the vertical alignment', () => {
        expectTypeOf<
            ['format', 'foo', {'vertical-align': 'middle'}]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which aligns a text section vertically', () => {
        expectTypeOf<
            ['format', 'foo', {'vertical-align': 'top'}]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which aligns an image vertically', () => {
        expectTypeOf<
            ['format', ['image', 'bar'], {'vertical-align': 'bottom'}]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which applies multiple style overrides', () => {
        expectTypeOf<
            ['format', 'foo', {'font-scale': 0.8; 'text-color': '#fff'}]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which applies default styles with an empty overrides object', () => {
        expectTypeOf<['format', ['downcase', 'BaR'], {}]>().toExtend<ExpressionSpecification>();
    });
});

describe('"image" expression', () => {
    test('type requires a string as the image name argument', () => {
        expectTypeOf<['image', true]>().not.toExtend<ExpressionSpecification>();
        expectTypeOf<['image', 123]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which returns an image with a string literal as the image name', () => {
        expectTypeOf<['image', 'foo']>().toExtend<ExpressionSpecification>();
    });
    test('type accepts an expression which returns an image with an expression as the image name', () => {
        expectTypeOf<['image', ['concat', 'foo', 'bar']]>().toExtend<ExpressionSpecification>();
    });
});

describe('"typeof" expression', () => {
    test('type requires a value argument', () => {
        expectTypeOf<['typeof']>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a second argument', () => {
        expectTypeOf<['typeof', true, 42]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which returns a string describing the type of the given literal value', () => {
        expectTypeOf<['typeof', true]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which returns a string describing the type of the given expression value', () => {
        expectTypeOf<
            ['typeof', ['concat', 'foo', ['to-string', 0]]]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"feature-state" expression', () => {
    test('type accepts expression which retrieves the feature state with a string literal argument', () => {
        expectTypeOf<['feature-state', 'foo']>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which retrieves the feature state with an expression argument', () => {
        expectTypeOf<['feature-state', ['get', 'feat-prop']]>().toExtend<ExpressionSpecification>();
    });
});

describe('"get" expression', () => {
    test('type requires an expression as the object argument if provided', () => {
        expectTypeOf<['get', 'prop', {prop: 4}]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which retrieves a property value from the given object argument', () => {
        expectTypeOf<['get', 'prop', ['literal', {prop: 4}]]>().toExtend<ExpressionSpecification>();
    });
});

describe('"global-state" expression', () => {
    test('type requires a property argument', () => {
        expectTypeOf<['global-state']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string literal as the property argument', () => {
        expectTypeOf<
            ['global-state', ['concat', 'pr', 'op']]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a second argument', () => {
        expectTypeOf<['global-state', 'foo', 'bar']>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which evaluates a global state property', () => {
        expectTypeOf<['global-state', 'foo']>().toExtend<ExpressionSpecification>();
    });
});

describe('"has" expression', () => {
    test('type requires an expression as the object argument if provided', () => {
        expectTypeOf<['has', 'prop', {prop: 4}]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which checks whether a property exists in the given object argument', () => {
        expectTypeOf<['has', 'prop', ['literal', {prop: 4}]]>().toExtend<ExpressionSpecification>();
    });
});

describe('"at" expression', () => {
    test('type accepts expression which retrieves the item at the specified index in the given array', () => {
        expectTypeOf<['at', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
    });
});

describe('"in" expression', () => {
    test('type requires a needle', () => {
        expectTypeOf<['in']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a haystack', () => {
        expectTypeOf<['in', 'a']>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a third argument', () => {
        expectTypeOf<['in', 'a', 'abc', 1]>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string or array as the haystack', () => {
        expectTypeOf<['in', 't', true]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a substring in a string', () => {
        expectTypeOf<['in', 'b', 'abc']>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a non-literal substring in a string', () => {
        expectTypeOf<
            ['in', ['downcase', 'C'], ['concat', 'ab', 'cd']]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds an element in an array', () => {
        expectTypeOf<['in', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a non-literal element in an array', () => {
        expectTypeOf<
            ['in', ['*', 2, 5], ['literal', [1, 10, 100]]]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"index-of" expression', () => {
    test('type requires a needle', () => {
        expectTypeOf<['index-of']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a haystack', () => {
        expectTypeOf<['index-of', 'a']>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a fourth argument', () => {
        expectTypeOf<['index-of', 'a', 'abc', 1, 8]>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string or array as the haystack', () => {
        expectTypeOf<['index-of', 't', true]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a substring in a string', () => {
        expectTypeOf<['index-of', 'b', 'abc']>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a non-literal substring in a string', () => {
        expectTypeOf<
            ['index-of', ['downcase', 'C'], ['concat', 'ab', 'cd']]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which starts looking for the substring at a start index', () => {
        expectTypeOf<['index-of', 'a', 'abc', 1]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which starts looking for the substring at a non-literal start index', () => {
        expectTypeOf<['index-of', 'c', 'abc', ['-', 0, 1]]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds an element in an array', () => {
        expectTypeOf<['index-of', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which finds a non-literal element in an array', () => {
        expectTypeOf<
            ['index-of', ['*', 2, 5], ['literal', [1, 10, 100]]]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which starts looking for the element at a start index', () => {
        expectTypeOf<
            ['index-of', 1, ['literal', [1, 2, 3]], 1]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which starts looking for the element at a non-literal start index', () => {
        expectTypeOf<
            ['index-of', 2, ['literal', [1, 2, 3]], ['+', 0, -1, 2]]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"length" expression', () => {
    test('type requires an argument', () => {
        expectTypeOf<['length']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string or array as the argument', () => {
        expectTypeOf<['length', true]>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a second argument', () => {
        expectTypeOf<['length', 'abc', 'def']>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which measures a string', () => {
        expectTypeOf<['length', 'abc']>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which measures an array', () => {
        expectTypeOf<['length', ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
    });
});

describe('"slice" expression', () => {
    test('type requires an input argument', () => {
        expectTypeOf<['slice']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a start index argument', () => {
        expectTypeOf<['slice', 'abc']>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a fourth argument', () => {
        expectTypeOf<['slice', 'abc', 0, 1, 8]>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string or array as the input argument', () => {
        expectTypeOf<['slice', true, 0]>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a number as the start index argument', () => {
        expectTypeOf<['slice', 'abc', true]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which slices a string', () => {
        expectTypeOf<['slice', 'abc', 1]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which slices a string by a given range', () => {
        expectTypeOf<['slice', 'abc', 1, 1]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which slices an array', () => {
        expectTypeOf<['slice', ['literal', [1, 2, 3]], 1]>().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which slices an array by a given range', () => {
        expectTypeOf<['slice', ['literal', [1, 2, 3]], 1, 1]>().toExtend<ExpressionSpecification>();
    });
});

describe('"split" expression', () => {
    test('type requires an input argument', () => {
        expectTypeOf<['split']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a separator argument', () => {
        expectTypeOf<['split', '1+2+3']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string as the input argument', () => {
        expectTypeOf<['split', true, '+']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string as the separator argument', () => {
        expectTypeOf<['split', '1+2+3', true]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which splits a string', () => {
        expectTypeOf<['split', '1+2+3', '+']>().toExtend<ExpressionSpecification>();
    });
});

describe('"join" expression', () => {
    test('type requires an input argument', () => {
        expectTypeOf<['join']>().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a separator argument', () => {
        expectTypeOf<
            ['join', ['literal', ['1', '2', '3']]]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type requires a string as the separator argument', () => {
        expectTypeOf<
            ['join', ['literal', ['1', '2', '3']], true]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which joins an array', () => {
        expectTypeOf<
            ['join', ['literal', ['1', '2', '3']], '+']
        >().toExtend<ExpressionSpecification>();
    });
});

describe('comparison expressions', () => {
    describe('"!=" expression', () => {
        test('type accepts expression which compares against literal null value', () => {
            expectTypeOf<
                ['!=', null, ['get', 'nonexistent-prop']]
            >().toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a collator', () => {
            expectTypeOf<
                ['!=', null, ['get', 'nonexistent-prop'], ['collator', {locale: 'mi-NZ'}]]
            >().toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a var', () => {
            expectTypeOf<
                [
                    'let',
                    'myVariable',
                    ['collator', {'diacritic-sensitive': true}],
                    ['!=', 'münchen', 'munchen', ['var', 'myVariable']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('does not allow the third argument to be any other expression type', () => {
            expectTypeOf<
                ['!=', 'münchen', 'munchen', ['!', true]]
            >().not.toExtend<ExpressionSpecification>();
        });
    });
    describe('"==" expression', () => {
        test('type accepts expression which compares expression input against literal input', () => {
            expectTypeOf<
                ['==', ['get', 'MILITARYAIRPORT'], 1]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which compares against literal null value', () => {
            expectTypeOf<
                ['==', null, ['get', 'nonexistent-prop']]
            >().toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a collator', () => {
            expectTypeOf<
                ['==', null, ['get', 'nonexistent-prop'], ['collator', {locale: 'mi-NZ'}]]
            >().toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a var', () => {
            expectTypeOf<
                [
                    'let',
                    'myVariable',
                    ['collator', {'diacritic-sensitive': true}],
                    ['==', 'münchen', 'munchen', ['var', 'myVariable']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('does not allow the third argument to be any other expression type', () => {
            expectTypeOf<
                ['==', 'münchen', 'munchen', ['!', true]]
            >().not.toExtend<ExpressionSpecification>();
        });
    });
    describe('"<" expression', () => {
        test('type rejects boolean input', () => {
            expectTypeOf<['<', -1, true]>().not.toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a collator', () => {
            expectTypeOf<
                ['<', 'ä', 'a', ['collator', {locale: 'sv'}]]
            >().toExtend<ExpressionSpecification>();
        });
        test('allows the third argument to be a var', () => {
            expectTypeOf<
                [
                    'let',
                    'myVariable',
                    ['collator', {locale: 'sv'}],
                    ['<', 'ä', 'a', ['var', 'myVariable']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('does not allow the third argument to be any other expression type', () => {
            expectTypeOf<
                ['<', 'ä', 'a', ['get', 'prop']]
            >().not.toExtend<ExpressionSpecification>();
        });
    });
    describe('"<=" expression', () => {
        test('type rejects boolean input', () => {
            expectTypeOf<['<=', 0, true]>().not.toExtend<ExpressionSpecification>();
        });
    });
    describe('">" expression', () => {
        test('type rejects boolean input', () => {
            expectTypeOf<['>', 1, true]>().not.toExtend<ExpressionSpecification>();
        });
    });
    describe('">=" expression', () => {
        test('type rejects boolean input', () => {
            expectTypeOf<['>=', 1, true]>().not.toExtend<ExpressionSpecification>();
        });
    });
});

describe('"any" expression', () => {
    test('type accepts expression which has no arguments', () => {
        expectTypeOf<['any']>().toExtend<ExpressionSpecification>();
    });
});

describe('"case" expression', () => {
    test('type accepts expression which returns the string output of the first matching condition', () => {
        expectTypeOf<
            [
                'case',
                ['==', ['get', 'CAPITAL'], 1],
                'city-capital',
                ['>=', ['get', 'POPULATION'], 1000000],
                'city-1M',
                ['>=', ['get', 'POPULATION'], 500000],
                'city-500k',
                ['>=', ['get', 'POPULATION'], 100000],
                'city-100k',
                'city'
            ]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which returns the evaluated output of the first matching condition', () => {
        expectTypeOf<
            [
                'case',
                ['has', 'point_count'],
                ['interpolate', ['linear'], ['get', 'point_count'], 2, '#ccc', 10, '#444'],
                ['has', 'priorityValue'],
                ['interpolate', ['linear'], ['get', 'priorityValue'], 0, '#ff9', 1, '#f66'],
                '#fcaf3e'
            ]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which has literal null output', () => {
        expectTypeOf<
            ['case', false, ['get', 'prop'], true, null, 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which has literal null fallback', () => {
        expectTypeOf<['case', false, ['get', 'prop'], null]>().toExtend<ExpressionSpecification>();
    });
});

describe('"match" expression', () => {
    test('type requires label to be string literal, number literal, string literal array, or number literal array', () => {
        expectTypeOf<
            ['match', 4, true, 'matched', 'fallback']
        >().not.toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['match', 4, [true], 'matched', 'fallback']
        >().not.toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['match', 4, [4, '4'], 'matched', 'fallback']
        >().not.toExtend<ExpressionSpecification>();
        expectTypeOf<
            ['match', 4, ['literal', [4]], 'matched', 'fallback']
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which matches number input against number label', () => {
        expectTypeOf<
            ['match', 2, [0], 'o1', 1, 'o2', 2, 'o3', 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which matches string input against string label', () => {
        expectTypeOf<
            ['match', 'c', 'a', 'o1', ['b'], 'o2', 'c', 'o3', 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which matches number input against number array label', () => {
        expectTypeOf<
            ['match', 2, 0, 'o1', [1, 2, 3], 'o2', 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which matches string input against string array label', () => {
        expectTypeOf<
            ['match', 'c', 'a', 'o1', ['b', 'c', 'd'], 'o2', 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which has a non-literal input', () => {
        expectTypeOf<
            ['match', ['get', 'TYPE'], ['ADIZ', 'AMA', 'AWY'], true, false]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which has an expression output', () => {
        expectTypeOf<
            ['match', ['get', 'id'], 'exampleID', ['get', 'iconNameFocused'], ['get', 'iconName']]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which has literal null output', () => {
        expectTypeOf<
            ['match', 1, 0, ['get', 'prop'], 1, null, 'fallback']
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"within" expression', () => {
    test('type requires a GeoJSON input', () => {
        expectTypeOf<['within']>().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects an expression as input', () => {
        expectTypeOf<
            ['within', ['literal', {type: 'Polygon'; coordinates: []}]]
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type rejects a second argument', () => {
        expectTypeOf<
            ['within', {type: 'Polygon'; coordinates: []}, 'second arg']
        >().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which checks if feature fully contained within input GeoJSON geometry', () => {
        expectTypeOf<
            [
                'within',
                {
                    type: 'Polygon';
                    coordinates: [[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]];
                }
            ]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('interpolation expressions', () => {
    describe('linear interpolation type', () => {
        test('type works with "interpolate" expression', () => {
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 0, 10, 1, 20]
            >().toExtend<ExpressionSpecification>();
        });
    });

    describe('exponential interpolation type', () => {
        test('type requires a number literal as the base argument', () => {
            expectTypeOf<
                ['interpolate', ['exponential', ['+', 0.1, 0.4]], ['zoom'], 0, 10, 1, 100]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type works with "interpolate" expression', () => {
            expectTypeOf<
                ['interpolate', ['exponential', 1.1], ['zoom'], 0, 10, 1, 20]
            >().toExtend<ExpressionSpecification>();
        });
    });

    describe('cubic-bezier interpolation type', () => {
        test('type requires four numeric literal control point arguments', () => {
            expectTypeOf<
                [
                    'interpolate',
                    ['cubic-bezier', 0.4, 0, ['literal', 0.6], 1],
                    ['zoom'],
                    2,
                    0,
                    8,
                    100
                ]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type rejects a fifth control point argument', () => {
            expectTypeOf<
                ['interpolate', ['cubic-bezier', 0.4, 0, 0.6, 1, 0.8], ['zoom'], 2, 0, 8, 100]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type works with "interpolate" expression', () => {
            expectTypeOf<
                ['interpolate', ['cubic-bezier', 0.4, 0, 0.6, 1], ['zoom'], 0, 0, 10, 100]
            >().toExtend<ExpressionSpecification>();
        });
    });

    describe('"interpolate" expression', () => {
        test('type requires stop outputs to be a number, color, number array, color array, or projection', () => {
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 0, false, 2, 1024]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                [
                    'interpolate',
                    ['linear'],
                    ['zoom'],
                    0,
                    [10, 20, 30],
                    0.5,
                    [20, 30, 40],
                    1,
                    [30, 40, 50]
                ]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates with feature property input', () => {
            expectTypeOf<
                [
                    'interpolate',
                    ['linear'],
                    ['get', 'point_count'],
                    2,
                    ['/', 2, ['get', 'point_count']],
                    10,
                    ['*', 4, ['get', 'point_count']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between number outputs', () => {
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 0, 0, 0.5, ['*', 2, 5], 1, 100]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color outputs', () => {
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 2, 'white', 4, 'black']
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between number array outputs', () => {
            expectTypeOf<
                [
                    'interpolate',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['literal', [2, 3]],
                    10,
                    ['literal', [4, 5]]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color array outputs', () => {
            expectTypeOf<
                [
                    'interpolate',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['literal', ['white', 'black']],
                    10,
                    ['literal', ['black', 'white']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between projection outputs', () => {
            expectTypeOf<
                ['interpolate', ['linear'], ['zoom'], 8, 'vertical-perspective', 10, 'mercator']
            >().toExtend<ExpressionSpecification>();
        });
    });

    describe('"interpolate-hcl" expression', () => {
        test('type requires stop outputs to be a color', () => {
            expectTypeOf<
                ['interpolate-hcl', ['linear'], ['zoom'], 0, false, 2, 1024]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                [
                    'interpolate-hcl',
                    ['linear'],
                    ['zoom'],
                    0,
                    [10, 20, 30],
                    0.5,
                    [20, 30, 40],
                    1,
                    [30, 40, 50]
                ]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                ['interpolate-hcl', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color outputs', () => {
            expectTypeOf<
                ['interpolate-hcl', ['linear'], ['zoom'], 2, 'white', 4, 'black']
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color array outputs', () => {
            expectTypeOf<
                [
                    'interpolate-hcl',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['literal', ['white', 'black']],
                    10,
                    ['literal', ['black', 'white']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between non-literal color array outputs', () => {
            // eslint-disable-next-line
            const obj = {'colors-8': ['white', 'black'], 'colors-10': ['black', 'white']};
            expectTypeOf<
                [
                    'interpolate-hcl',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['get', 'colors-8', ['literal', typeof obj]],
                    10,
                    ['get', 'colors-10', ['literal', typeof obj]]
                ]
            >().toExtend<ExpressionSpecification>();
        });
    });

    describe('"interpolate-lab" expression', () => {
        test('type requires stop outputs to be a color', () => {
            expectTypeOf<
                ['interpolate-lab', ['linear'], ['zoom'], 0, false, 2, 1024]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                [
                    'interpolate-lab',
                    ['linear'],
                    ['zoom'],
                    0,
                    [10, 20, 30],
                    0.5,
                    [20, 30, 40],
                    1,
                    [30, 40, 50]
                ]
            >().not.toExtend<ExpressionSpecification>();
            expectTypeOf<
                ['interpolate-lab', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
            >().not.toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color outputs', () => {
            expectTypeOf<
                ['interpolate-lab', ['linear'], ['zoom'], 2, 'white', 4, 'black']
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between color array outputs', () => {
            expectTypeOf<
                [
                    'interpolate-lab',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['literal', ['white', 'black']],
                    10,
                    ['literal', ['black', 'white']]
                ]
            >().toExtend<ExpressionSpecification>();
        });
        test('type accepts expression which interpolates between non-literal color array outputs', () => {
            // eslint-disable-next-line
            const obj = {'colors-8': ['white', 'black'], 'colors-10': ['black', 'white']};
            expectTypeOf<
                [
                    'interpolate-lab',
                    ['linear'],
                    ['zoom'],
                    8,
                    ['get', 'colors-8', ['literal', typeof obj]],
                    10,
                    ['get', 'colors-10', ['literal', typeof obj]]
                ]
            >().toExtend<ExpressionSpecification>();
        });
    });
});

describe('"step" expression', () => {
    test('type accepts expression which outputs stepped numbers', () => {
        expectTypeOf<
            ['step', ['get', 'point_count'], 0.6, 50, 0.7, 200, 0.8]
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which outputs stepped colors', () => {
        expectTypeOf<
            ['step', ['get', 'point_count'], '#ddd', 50, '#eee', 200, '#fff']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which outputs stepped projections', () => {
        expectTypeOf<
            ['step', ['zoom'], 'vertical-perspective', 10, 'mercator']
        >().toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which outputs stepped multi-input projections', () => {
        expectTypeOf<
            [
                'step',
                ['zoom'],
                ['literal', ['vertical-perspective', 'mercator', 0.5]],
                10,
                'mercator'
            ]
        >().toExtend<ExpressionSpecification>();
    });
});

describe('"e" expression', () => {
    test('type rejects any arguments', () => {
        expectTypeOf<['e', 2]>().not.toExtend<ExpressionSpecification>();
    });
    test('type accepts expression which returns the mathematical constant e', () => {
        expectTypeOf<['e']>().toExtend<ExpressionSpecification>();
    });
});

describe('nonexistent operators', () => {
    test('ExpressionSpecification type does not contain "ExpressionSpecification" expression', () => {
        type ExpressionSpecificationExpression = Extract<
            ExpressionSpecification,
            ['ExpressionSpecification', ...any[]]
        >;
        expectTypeOf<ExpressionSpecificationExpression>().not.toExtend<ExpressionSpecification>();
    });
});

test('ExpressionSpecification type supports common variable insertion patterns', () => {
    // Checks the ability for the ExpressionSpecification type to allow arguments to be provided via constants (as opposed to in-line).
    // As in most cases the styling is read from JSON, these are rather optional tests.
    // eslint-disable-next-line
    const colorStops = [0, 'red', 0.5, 'green', 1, 'blue'];
    expectTypeOf<
        ['interpolate', ['linear'], ['line-progress'], ...typeof colorStops]
    >().toExtend<ExpressionSpecification>();
    expectTypeOf<
        ['interpolate-hcl', ['linear'], ['line-progress'], ...typeof colorStops]
    >().toExtend<ExpressionSpecification>();
    expectTypeOf<
        ['interpolate-lab', ['linear'], ['line-progress'], ...typeof colorStops]
    >().toExtend<ExpressionSpecification>();
    // eslint-disable-next-line
    const [firstOutput, ...steps] = ['#df2d43', 50, '#df2d43', 200, '#df2d43'];
    expectTypeOf<
        ['step', ['get', 'point_count'], typeof firstOutput, ...typeof steps]
    >().toExtend<ExpressionSpecification>();
    // eslint-disable-next-line
    const strings = ['first', 'second', 'third'];
    expectTypeOf<['concat', ...typeof strings]>().toExtend<ExpressionSpecification>();
    // eslint-disable-next-line
    const values: (ExpressionInputType | ExpressionSpecification)[] = [
        ['get', 'name'],
        ['get', 'code'],
        'NONE'
    ]; // type is necessary!
    expectTypeOf<['coalesce', ...typeof values]>().toExtend<ExpressionSpecification>();
});
