import {expectToMatchColor} from '../../test/lib/util';
import {createFunction} from './index';
import {Color} from '../expression/types/color';
import {Formatted} from '../expression/types/formatted';
import {Padding} from '../expression/types/padding';
import {NumberArray} from '../expression/types/number_array';
import {ColorArray} from '../expression/types/color_array';
import {describe, test, expect} from 'vitest';

describe('binary search', () => {
    test('will eventually terminate.', () => {
        const f = createFunction(
            {
                stops: [
                    [9, 10],
                    [17, 11],
                    [17, 11],
                    [18, 13]
                ],
                base: 2
            },
            {
                type: 'number',
                'property-type': 'data-constant',
                expression: {
                    interpolated: true,
                    parameters: ['zoom']
                }
            }
        ).evaluate;

        expect(f({zoom: 17}, undefined)).toBe(11);
    });
});

describe('exponential function', () => {
    test('is the default for interpolated properties', () => {
        const f = createFunction(
            {
                stops: [
                    [1, 2],
                    [3, 6]
                ],
                base: 2
            },
            {
                type: 'number',
                'property-type': 'data-constant',
                expression: {
                    interpolated: true,
                    parameters: ['zoom']
                }
            }
        ).evaluate;

        expect(f({zoom: 2}, undefined)).toBeCloseTo(30 / 9);
    });

    test('base', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 2],
                    [3, 6]
                ],
                base: 2
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toBeCloseTo(2);
        expect(f({zoom: 1}, undefined)).toBeCloseTo(2);
        expect(f({zoom: 2}, undefined)).toBeCloseTo(30 / 9);
        expect(f({zoom: 3}, undefined)).toBeCloseTo(6);
        expect(f({zoom: 4}, undefined)).toBeCloseTo(6);
    });

    test('one stop', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [[1, 2]]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toBe(2);
        expect(f({zoom: 1}, undefined)).toBe(2);
        expect(f({zoom: 2}, undefined)).toBe(2);
    });

    test('two stops', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 2],
                    [3, 6]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toBe(2);
        expect(f({zoom: 1}, undefined)).toBe(2);
        expect(f({zoom: 2}, undefined)).toBe(4);
        expect(f({zoom: 3}, undefined)).toBe(6);
        expect(f({zoom: 4}, undefined)).toBe(6);
    });

    test('three stops', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 2],
                    [3, 6],
                    [5, 10]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toBe(2);
        expect(f({zoom: 1}, undefined)).toBe(2);
        expect(f({zoom: 2}, undefined)).toBe(4);
        expect(f({zoom: 2.5}, undefined)).toBe(5);
        expect(f({zoom: 3}, undefined)).toBe(6);
        expect(f({zoom: 4}, undefined)).toBe(8);
        expect(f({zoom: 4.5}, undefined)).toBe(9);
        expect(f({zoom: 5}, undefined)).toBe(10);
        expect(f({zoom: 6}, undefined)).toBe(10);
    });

    test('four stops', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 2],
                    [3, 6],
                    [5, 10],
                    [7, 14]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toBe(2);
        expect(f({zoom: 1}, undefined)).toBe(2);
        expect(f({zoom: 2}, undefined)).toBe(4);
        expect(f({zoom: 2.5}, undefined)).toBe(5);
        expect(f({zoom: 3}, undefined)).toBe(6);
        expect(f({zoom: 3.5}, undefined)).toBe(7);
        expect(f({zoom: 4}, undefined)).toBe(8);
        expect(f({zoom: 4.5}, undefined)).toBe(9);
        expect(f({zoom: 5}, undefined)).toBe(10);
        expect(f({zoom: 6}, undefined)).toBe(12);
        expect(f({zoom: 6.5}, undefined)).toBe(13);
        expect(f({zoom: 7}, undefined)).toBe(14);
        expect(f({zoom: 8}, undefined)).toBe(14);
    });

    test('many stops', () => {
        const stops = [
            [2, 100],
            [55, 200],
            [132, 300],
            [607, 400],
            [1287, 500],
            [1985, 600],
            [2650, 700],
            [3299, 800],
            [3995, 900],
            [4927, 1000],
            [7147, 10000], // 10
            [10028, 100000], // 11
            [12889, 1000000],
            [40000, 10000000]
        ];
        const f = createFunction(
            {
                type: 'exponential',
                stops
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 2}, undefined)).toBeCloseTo(100); //, 1e-6);
        expect(f({zoom: 20}, undefined)).toBeCloseTo(133.9622641509434);
        expect(f({zoom: 607}, undefined)).toBeCloseTo(400);
        expect(f({zoom: 680}, undefined)).toBeCloseTo(410.7352941176471);
        expect(f({zoom: 4927}, undefined)).toBeCloseTo(1000); //86
        expect(f({zoom: 7300}, undefined)).toBeCloseTo(14779.590419993057);
        expect(f({zoom: 10000}, undefined)).toBeCloseTo(99125.30371398819);
        expect(f({zoom: 20000}, undefined)).toBeCloseTo(3360628.527166095);
        expect(f({zoom: 40000}, undefined)).toBeCloseTo(10000000);
    });

    test('color', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 'red'],
                    [11, 'blue']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expectToMatchColor(f({zoom: 0}, undefined), 'rgb(100% 0% 0% / 1)');
        expectToMatchColor(f({zoom: 5}, undefined), 'rgb(60% 0% 40% / 1)');
        expectToMatchColor(f({zoom: 11}, undefined), 'rgb(0% 0% 100% / 1)');
    });

    test('color hcl colorspace', () => {
        const f = createFunction(
            {
                type: 'exponential',
                colorSpace: 'hcl',
                stops: [
                    [0, 'rgb(36 98 36 / 0.6)'],
                    [10, 'hsl(222,73%,32%)']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expectToMatchColor(f({zoom: 0}, undefined), 'rgb(14.12% 38.43% 14.12% / .6)', 4);
        expectToMatchColor(f({zoom: 5}, undefined), 'rgb(0.00% 35.13% 43.84% / 0.8)', 4);
        expectToMatchColor(f({zoom: 10}, undefined), 'rgb(8.64% 22.66% 55.36% / 1)', 4);
    });

    test('color lab colorspace', () => {
        const f = createFunction(
            {
                type: 'exponential',
                colorSpace: 'lab',
                stops: [
                    [0, '#0009'],
                    [10, 'rgba(0,255,255,1)']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expectToMatchColor(f({zoom: 0}, undefined), 'rgb(0% 0% 0% / .6)');
        expectToMatchColor(f({zoom: 5}, undefined), 'rgb(14.15% 46.74% 46.63% / 0.8)', 4);
        expectToMatchColor(f({zoom: 10}, undefined), 'rgb(0% 100% 100% / 1)');
    });

    test('color invalid colorspace', () => {
        expect(() => {
            createFunction(
                {
                    type: 'exponential',
                    colorSpace: 'invalid',
                    stops: [
                        [1, [0, 0, 0, 1]],
                        [10, [0, 1, 1, 1]]
                    ]
                },
                {
                    type: 'color'
                }
            );
        }).toThrowError('Unknown color space: "invalid"');
    });

    test('exponential interpolation of colorArray', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, ['red', 'blue']],
                    [11, ['blue', 'red']]
                ]
            },
            {
                type: 'colorArray'
            }
        ).evaluate;

        expect(f({zoom: 5}, undefined).values).toHaveLength(2);
        expectToMatchColor(f({zoom: 0}, undefined).values[0], 'rgb(100% 0% 0% / 1)');
        expectToMatchColor(f({zoom: 0}, undefined).values[1], 'rgb(0% 0% 100% / 1)');
        expectToMatchColor(f({zoom: 5}, undefined).values[0], 'rgb(60% 0% 40% / 1)');
        expectToMatchColor(f({zoom: 5}, undefined).values[1], 'rgb(40% 0% 60% / 1)');
        expectToMatchColor(f({zoom: 11}, undefined).values[0], 'rgb(0% 0% 100% / 1)');
        expectToMatchColor(f({zoom: 11}, undefined).values[1], 'rgb(100% 0% 0% / 1)');
    });

    test('exponential interpolation of colorArray (hcl colorspace)', () => {
        const f = createFunction(
            {
                type: 'exponential',
                colorSpace: 'hcl',
                stops: [
                    [0, 'rgb(36 98 36 / 0.6)'],
                    [10, 'hsl(222,73%,32%)']
                ]
            },
            {
                type: 'colorArray'
            }
        ).evaluate;

        expect(f({zoom: 5}, undefined).values).toHaveLength(1);
        expectToMatchColor(f({zoom: 0}, undefined).values[0], 'rgb(14.12% 38.43% 14.12% / .6)', 4);
        expectToMatchColor(f({zoom: 5}, undefined).values[0], 'rgb(0.00% 35.13% 43.84% / 0.8)', 4);
        expectToMatchColor(f({zoom: 10}, undefined).values[0], 'rgb(8.64% 22.66% 55.36% / 1)', 4);
    });

    test('exponential interpolation of colorArray (lab colorspace)', () => {
        const f = createFunction(
            {
                type: 'exponential',
                colorSpace: 'lab',
                stops: [
                    [0, '#0009'],
                    [10, 'rgba(0,255,255,1)']
                ]
            },
            {
                type: 'colorArray'
            }
        ).evaluate;

        expect(f({zoom: 5}, undefined).values).toHaveLength(1);
        expectToMatchColor(f({zoom: 0}, undefined).values[0], 'rgb(0% 0% 0% / .6)');
        expectToMatchColor(f({zoom: 5}, undefined).values[0], 'rgb(14.15% 46.74% 46.63% / 0.8)', 4);
        expectToMatchColor(f({zoom: 10}, undefined).values[0], 'rgb(0% 100% 100% / 1)');
    });

    test('exponential interpolation of colorArray (invalid colorspace)', () => {
        expect(() => {
            createFunction(
                {
                    type: 'exponential',
                    colorSpace: 'invalid',
                    stops: [
                        [1, [0, 0, 0, 1]],
                        [10, [0, 1, 1, 1]]
                    ]
                },
                {
                    type: 'colorArray'
                }
            );
        }).toThrowError('Unknown color space: "invalid"');
    });

    test('exponential interpolation of numberArray', () => {
        const f = createFunction(
            {
                type: 'exponential',
                colorSpace: 'lab',
                stops: [
                    [0, [1, 2]],
                    [10, [3, 4]]
                ]
            },
            {
                type: 'numberArray'
            }
        ).evaluate;

        expect(f({zoom: 5}, undefined).values).toHaveLength(2);
        expect(f({zoom: 0}, undefined).values).toEqual([1, 2]);
        expect(f({zoom: 5}, undefined).values).toEqual([2, 3]);
        expect(f({zoom: 10}, undefined).values).toEqual([3, 4]);
    });

    test('interpolation mutation avoidance', () => {
        const params = {
            type: 'exponential',
            colorSpace: 'lab',
            stops: [
                [1, [0, 0, 0, 1]],
                [10, [0, 1, 1, 1]]
            ]
        };
        const paramsCopy = JSON.parse(JSON.stringify(params));
        createFunction(params, {
            type: 'color'
        });
        expect(params).toEqual(paramsCopy);
    });

    test('exponential interpolation of padding', () => {
        const f = createFunction(
            {
                type: 'exponential',
                stops: [
                    [1, 2],
                    [11, [2, 5, 2, 7]]
                ]
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toEqual(new Padding([2, 2, 2, 2]));
        expect(f({zoom: 5}, undefined)).toEqual(new Padding([2, 3.2, 2, 4]));
        expect(f({zoom: 11}, undefined)).toEqual(new Padding([2, 5, 2, 7]));
    });

    test('property present', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'exponential',
                stops: [
                    [0, 0],
                    [1, 2]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(2);
    });

    test('property absent, function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'exponential',
                stops: [
                    [0, 0],
                    [1, 2]
                ],
                default: 3
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe(3);
    });

    test('property absent, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'exponential',
                stops: [
                    [0, 0],
                    [1, 2]
                ]
            },
            {
                type: 'number',
                default: 3
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe(3);
    });

    test('property type mismatch, function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'exponential',
                stops: [
                    [0, 0],
                    [1, 2]
                ],
                default: 3
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'string'}})).toBe(3);
    });

    test('property type mismatch, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'exponential',
                stops: [
                    [0, 0],
                    [1, 2]
                ]
            },
            {
                type: 'string',
                default: 3
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'string'}})).toBe(3);
    });

    test('zoom-and-property function, one stop', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                stops: [[{zoom: 1, value: 1}, 2]]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {prop: 0}})).toBe(2);
        expect(f({zoom: 1}, {properties: {prop: 0}})).toBe(2);
        expect(f({zoom: 2}, {properties: {prop: 0}})).toBe(2);
        expect(f({zoom: 0}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 1}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 2}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 0}, {properties: {prop: 2}})).toBe(2);
        expect(f({zoom: 1}, {properties: {prop: 2}})).toBe(2);
        expect(f({zoom: 2}, {properties: {prop: 2}})).toBe(2);
    });

    test('zoom-and-property function, two stops', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                base: 1,
                stops: [
                    [{zoom: 1, value: 0}, 0],
                    [{zoom: 1, value: 2}, 4],
                    [{zoom: 3, value: 0}, 0],
                    [{zoom: 3, value: 2}, 12]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 1}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 2}, {properties: {prop: 1}})).toBe(4);
        expect(f({zoom: 3}, {properties: {prop: 1}})).toBe(6);
        expect(f({zoom: 4}, {properties: {prop: 1}})).toBe(6);

        expect(f({zoom: 2}, {properties: {prop: -1}})).toBe(0);
        expect(f({zoom: 2}, {properties: {prop: 0}})).toBe(0);
        expect(f({zoom: 2}, {properties: {prop: 2}})).toBe(8);
        expect(f({zoom: 2}, {properties: {prop: 3}})).toBe(8);
    });

    test('zoom-and-property function, three stops', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                base: 1,
                stops: [
                    [{zoom: 1, value: 0}, 0],
                    [{zoom: 1, value: 2}, 4],
                    [{zoom: 3, value: 0}, 0],
                    [{zoom: 3, value: 2}, 12],
                    [{zoom: 5, value: 0}, 0],
                    [{zoom: 5, value: 2}, 20]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 1}, {properties: {prop: 1}})).toBe(2);
        expect(f({zoom: 2}, {properties: {prop: 1}})).toBe(4);
    });

    test('zoom-and-property function, two stops, fractional zoom', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                base: 1,
                stops: [
                    [{zoom: 1.9, value: 0}, 4],
                    [{zoom: 2.1, value: 0}, 8]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 1.9}, {properties: {prop: 1}})).toBe(4);
        expect(f({zoom: 2}, {properties: {prop: 1}})).toBe(6);
        expect(f({zoom: 2.1}, {properties: {prop: 1}})).toBe(8);
    });

    test('zoom-and-property function, four stops, integer and fractional zooms', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                base: 1,
                stops: [
                    [{zoom: 1, value: 0}, 0],
                    [{zoom: 1.5, value: 0}, 1],
                    [{zoom: 2, value: 0}, 10],
                    [{zoom: 2.5, value: 0}, 20]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 1}, {properties: {prop: 0}})).toBe(0);
        expect(f({zoom: 1.5}, {properties: {prop: 0}})).toBe(1);
        expect(f({zoom: 2}, {properties: {prop: 0}})).toBe(10);
        expect(f({zoom: 2.5}, {properties: {prop: 0}})).toBe(20);
    });

    test('zoom-and-property function, no default', () => {
        // This can happen for fill-outline-color, where the spec has no default.

        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                base: 1,
                stops: [
                    [{zoom: 0, value: 1}, 'red'],
                    [{zoom: 1, value: 1}, 'red']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBeUndefined();
        expect(f({zoom: 0.5}, {properties: {}})).toBeUndefined();
        expect(f({zoom: 1}, {properties: {}})).toBeUndefined();
    });

    test('zoom-and-property function, color', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                stops: [
                    [{zoom: 0, value: 0}, 'red'],
                    [{zoom: 1, value: 0}, 'blue'],
                    [{zoom: 0, value: 1}, 'lime'],
                    [{zoom: 1, value: 1}, 'magenta']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0.0}, {properties: {prop: 0}})).toEqual(new Color(1, 0, 0, 1));
        expect(f({zoom: 1.0}, {properties: {prop: 0}})).toEqual(new Color(0, 0, 1, 1));
        expect(f({zoom: 0.5}, {properties: {prop: 1}})).toEqual(new Color(0.5, 0.5, 0.5, 1));
    });

    test('zoom-and-property function, padding', () => {
        const f = createFunction(
            {
                type: 'exponential',
                property: 'prop',
                stops: [
                    [{zoom: 0, value: 0}, [2]],
                    [{zoom: 1, value: 0}, [4]],
                    [{zoom: 0, value: 1}, [6]],
                    [{zoom: 1, value: 1}, [8]]
                ]
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0.0}, {properties: {prop: 0}})).toEqual(new Padding([2, 2, 2, 2]));
        expect(f({zoom: 1.0}, {properties: {prop: 0}})).toEqual(new Padding([4, 4, 4, 4]));
        expect(f({zoom: 0.5}, {properties: {prop: 1}})).toEqual(new Padding([7, 7, 7, 7]));
    });
});

describe('interval function', () => {
    test('is the default for non-interpolated properties', () => {
        const f = createFunction(
            {
                stops: [
                    [-1, 11],
                    [0, 111]
                ]
            },
            {
                type: 'number',
                'property-type': 'data-constant',
                expression: {
                    interpolated: false,
                    parameters: ['zoom']
                }
            }
        ).evaluate;

        expect(f({zoom: -1.5}, undefined)).toBe(11);
        expect(f({zoom: -0.5}, undefined)).toBe(11);
        expect(f({zoom: 0}, undefined)).toBe(111);
        expect(f({zoom: 0.5}, undefined)).toBe(111);
    });

    test('one stop', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [[0, 11]]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: -0.5}, undefined)).toBe(11);
        expect(f({zoom: 0}, undefined)).toBe(11);
        expect(f({zoom: 0.5}, undefined)).toBe(11);
    });

    test('two stops', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [
                    [-1, 11],
                    [0, 111]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: -1.5}, undefined)).toBe(11);
        expect(f({zoom: -0.5}, undefined)).toBe(11);
        expect(f({zoom: 0}, undefined)).toBe(111);
        expect(f({zoom: 0.5}, undefined)).toBe(111);
    });

    test('three stops', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [
                    [-1, 11],
                    [0, 111],
                    [1, 1111]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: -1.5}, undefined)).toBe(11);
        expect(f({zoom: -0.5}, undefined)).toBe(11);
        expect(f({zoom: 0}, undefined)).toBe(111);
        expect(f({zoom: 0.5}, undefined)).toBe(111);
        expect(f({zoom: 1}, undefined)).toBe(1111);
        expect(f({zoom: 1.5}, undefined)).toBe(1111);
    });

    test('four stops', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [
                    [-1, 11],
                    [0, 111],
                    [1, 1111],
                    [2, 11111]
                ]
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: -1.5}, undefined)).toBe(11);
        expect(f({zoom: -0.5}, undefined)).toBe(11);
        expect(f({zoom: 0}, undefined)).toBe(111);
        expect(f({zoom: 0.5}, undefined)).toBe(111);
        expect(f({zoom: 1}, undefined)).toBe(1111);
        expect(f({zoom: 1.5}, undefined)).toBe(1111);
        expect(f({zoom: 2}, undefined)).toBe(11111);
        expect(f({zoom: 2.5}, undefined)).toBe(11111);
    });

    test('color', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [
                    [1, 'red'],
                    [11, 'blue']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toEqual(new Color(1, 0, 0, 1));
        expect(f({zoom: 10}, undefined)).toEqual(new Color(1, 0, 0, 1));
        expect(f({zoom: 11}, undefined)).toEqual(new Color(0, 0, 1, 1));
    });

    test('padding', () => {
        const f = createFunction(
            {
                type: 'interval',
                stops: [
                    [1, 2],
                    [11, 4]
                ]
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, undefined)).toEqual(new Padding([2, 2, 2, 2]));
        expect(f({zoom: 10}, undefined)).toEqual(new Padding([2, 2, 2, 2]));
        expect(f({zoom: 11}, undefined)).toEqual(new Padding([4, 4, 4, 4]));
    });

    test('property present', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'interval',
                stops: [
                    [0, 'bad'],
                    [1, 'good'],
                    [2, 'bad']
                ]
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 1.5}})).toBe('good');
    });

    test('property absent, function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'interval',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ],
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe('default');
    });

    test('property absent, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'interval',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ]
            },
            {
                type: 'string',
                default: 'default'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe('default');
    });

    test('property type mismatch, function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'interval',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ],
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'string'}})).toBe('default');
    });

    test('property type mismatch, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'interval',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ]
            },
            {
                type: 'string',
                default: 'default'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'string'}})).toBe('default');
    });
});

describe('categorical function', () => {
    test('string', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'bad'],
                    [1, 'good'],
                    [2, 'bad']
                ]
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 0}})).toBe('bad');
        expect(f({zoom: 0}, {properties: {foo: 1}})).toBe('good');
        expect(f({zoom: 0}, {properties: {foo: 2}})).toBe('bad');
    });

    test('string function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ],
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe('default');
        expect(f({zoom: 0}, {properties: {foo: 3}})).toBe('default');
    });

    test('string zoom-and-property function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [[{zoom: 0, value: 'bar'}, 'zero']],
                default: 'default'
            },
            {
                type: 'string',
                function: 'interval'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe('default');
        expect(f({zoom: 0}, {properties: {foo: 3}})).toBe('default');
        expect(f({zoom: 0}, {properties: {foo: 'bar'}})).toBe('zero');
    });

    test('strict type checking', () => {
        const numberKeys = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ],
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        const stringKeys = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    ['0', 'zero'],
                    ['1', 'one'],
                    ['2', 'two'],
                    ['true', 'yes'],
                    ['false', 'no']
                ],
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(numberKeys(0 as any, {properties: {foo: '0'}})).toBe('default');
        expect(numberKeys(0 as any, {properties: {foo: '1'}})).toBe('default');
        expect(numberKeys(0 as any, {properties: {foo: false}})).toBe('default');
        expect(numberKeys(0 as any, {properties: {foo: true}})).toBe('default');

        expect(stringKeys(0 as any, {properties: {foo: 0}})).toBe('default');
        expect(stringKeys(0 as any, {properties: {foo: 1}})).toBe('default');
        expect(stringKeys(0 as any, {properties: {foo: false}})).toBe('default');
        expect(stringKeys(0 as any, {properties: {foo: true}})).toBe('default');
    });

    test('string spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'zero'],
                    [1, 'one'],
                    [2, 'two']
                ]
            },
            {
                type: 'string',
                default: 'default'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe('default');
        expect(f({zoom: 0}, {properties: {foo: 3}})).toBe('default');
    });

    test('color', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'red'],
                    [1, 'blue']
                ]
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 0}})).toEqual(new Color(1, 0, 0, 1));
        expect(f({zoom: 1}, {properties: {foo: 1}})).toEqual(new Color(0, 0, 1, 1));
    });

    test('color function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'red'],
                    [1, 'blue']
                ],
                default: 'lime'
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Color(0, 1, 0, 1));
        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new Color(0, 1, 0, 1));
    });

    test('color spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 'red'],
                    [1, 'blue']
                ]
            },
            {
                type: 'color',
                default: 'lime'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Color(0, 1, 0, 1));
        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new Color(0, 1, 0, 1));
    });

    test('padding', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 2],
                    [1, 4]
                ]
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 0}})).toEqual(new Padding([2, 2, 2, 2]));
        expect(f({zoom: 1}, {properties: {foo: 1}})).toEqual(new Padding([4, 4, 4, 4]));
    });

    test('padding function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 2],
                    [1, 4]
                ],
                default: 6
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Padding([6, 6, 6, 6]));
        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new Padding([6, 6, 6, 6]));
    });

    test('padding spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [0, 2],
                    [1, 4]
                ]
            },
            {
                type: 'padding',
                default: 6
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Padding([6, 6, 6, 6]));
        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new Padding([6, 6, 6, 6]));
    });

    test('boolean', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'categorical',
                stops: [
                    [true, 'true'],
                    [false, 'false']
                ]
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: true}})).toBe('true');
        expect(f({zoom: 0}, {properties: {foo: false}})).toBe('false');
    });
});

describe('identity function', () => {
    test('number', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'number'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(1);
    });

    test('number function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity',
                default: 1
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe(1);
    });

    test('number spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'string',
                default: 1
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toBe(1);
    });

    test('color', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'red'}})).toEqual(new Color(1, 0, 0, 1));
        expect(f({zoom: 1}, {properties: {foo: 'blue'}})).toEqual(new Color(0, 0, 1, 1));
    });

    test('color function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity',
                default: 'red'
            },
            {
                type: 'color'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Color(1, 0, 0, 1));
    });

    test('color spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'color',
                default: 'red'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Color(1, 0, 0, 1));
    });

    test('color invalid', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'color',
                default: 'red'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'invalid'}})).toEqual(new Color(1, 0, 0, 1));
    });

    test('padding', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new Padding([3, 3, 3, 3]));
        expect(f({zoom: 1}, {properties: {foo: [3, 4]}})).toEqual(new Padding([3, 4, 3, 4]));
    });

    test('padding function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity',
                default: [1, 2, 3, 4]
            },
            {
                type: 'padding'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Padding([1, 2, 3, 4]));
    });

    test('padding spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'padding',
                default: [1, 2, 3, 4]
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {}})).toEqual(new Padding([1, 2, 3, 4]));
    });

    test('padding invalid', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'padding',
                default: [1, 2, 3, 4]
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'invalid'}})).toEqual(new Padding([1, 2, 3, 4]));
    });

    test('colorArray identity', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'colorArray'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'red'}})).toEqual(new ColorArray([Color.red]));
        expect(f({zoom: 1}, {properties: {foo: ['black', 'white']}})).toEqual(
            new ColorArray([Color.black, Color.white])
        );
    });

    test('numberArray identity', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'numberArray'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 3}})).toEqual(new NumberArray([3]));
        expect(f({zoom: 1}, {properties: {foo: [3, 4]}})).toEqual(new NumberArray([3, 4]));
    });

    test('property type mismatch, function default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity',
                default: 'default'
            },
            {
                type: 'string'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 0}})).toBe('default');
    });

    test('property type mismatch, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'string',
                default: 'default'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 0}})).toBe('default');
    });

    test('valid enum', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'enum',
                values: {
                    bar: {}
                },
                default: 'def'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'bar'}})).toBe('bar');
    });

    test('invalid enum, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'enum',
                values: {
                    bar: {}
                },
                default: 'def'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'baz'}})).toBe('def');
    });

    test('invalid type for enum, spec default', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'enum',
                values: {
                    bar: {}
                },
                default: 'def'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 3}})).toBe('def');
    });

    test('formatted', () => {
        const f = createFunction(
            {
                property: 'foo',
                type: 'identity'
            },
            {
                type: 'formatted'
            }
        ).evaluate;

        expect(f({zoom: 0}, {properties: {foo: 'foo'}})).toEqual(Formatted.fromString('foo'));
        expect(f({zoom: 1}, {properties: {foo: 'bar'}})).toEqual(Formatted.fromString('bar'));
        expect(f({zoom: 2}, {properties: {foo: 2}})).toEqual(Formatted.fromString('2'));
        expect(f({zoom: 3}, {properties: {foo: true}})).toEqual(Formatted.fromString('true'));
    });
});

test('unknown function', () => {
    expect(() =>
        createFunction(
            {
                type: 'nonesuch',
                stops: [[]]
            },
            {
                type: 'string'
            }
        )
    ).toThrowError(/Unknown function type "nonesuch"/);
});

describe('kind', () => {
    test('camera', () => {
        const f = createFunction(
            {
                stops: [[1, 1]]
            },
            {
                type: 'number'
            }
        );

        expect(f.kind).toBe('camera');
    });

    test('source', () => {
        const f = createFunction(
            {
                stops: [[1, 1]],
                property: 'mapbox'
            },
            {
                type: 'number'
            }
        );

        expect(f.kind).toBe('source');
    });

    test('composite', () => {
        const f = createFunction(
            {
                stops: [[{zoom: 1, value: 1}, 1]],
                property: 'mapbox'
            },
            {
                type: 'number'
            }
        );

        expect(f.kind).toBe('composite');
    });
});
