import {describe, test, expect, vi, afterEach} from 'vitest';
import {validateAndEmit, validateStyle, validateStyleAndEmit} from './validate_style.ts';
import {Evented, type ErrorEventType} from '../util/evented.ts';
import type {StyleSpecification} from '@maplibre/maplibre-gl-style-spec';

class TestEmitter extends Evented<ErrorEventType> {}

afterEach(() => {
    vi.restoreAllMocks();
});

/** An emitter collecting the messages of the {@link ErrorEvent}s fired at it, plus a `console.warn` spy. */
function setup() {
    const emitter = new TestEmitter();
    const fired: string[] = [];
    emitter.on('error', ({error}) => fired.push(error.message));
    return {emitter, fired, warn: vi.spyOn(console, 'warn').mockImplementation(() => {})};
}

describe('validateAndEmit', () => {
    const key = 'layers.symbol.filter';

    test('fires an error and reports failure for a filter the spec rejects', () => {
        const {emitter, fired} = setup();

        const hasErrors = validateAndEmit(emitter, validateStyle.filter, {
            key,
            value: ['all', ['==', ['get', 'class'], 'rail'], ['nope', 1]]
        });

        expect(hasErrors).toBe(true);
        expect(fired).toEqual([expect.stringContaining('Unknown expression "nope"')]);
    });

    test('only warns for a filter that mixes legacy syntax into an expression, so the style keeps loading', () => {
        const {emitter, fired, warn} = setup();

        const hasErrors = validateAndEmit(emitter, validateStyle.filter, {
            key,
            value: ['all', ['==', ['get', 'class'], 'rail'], ['in', 'name', '']]
        });

        expect(hasErrors).toBe(false);
        expect(fired).toEqual([]);
        expect(warn).toHaveBeenCalledWith(expect.stringContaining('Mixing deprecated filter syntax with expression syntax'));
    });

    test('still fails when an error accompanies a warning, emitting only the error', () => {
        const {emitter, fired, warn} = setup();

        // `!in` has no expression equivalent, so the spec reports it as an error on top of the
        // warning about the mixing itself. A warning must not swallow that error.
        const hasErrors = validateAndEmit(emitter, validateStyle.filter, {
            key,
            value: ['all', ['==', ['get', 'class'], 'rail'], ['!in', 'name', 'a']]
        });

        expect(hasErrors).toBe(true);
        expect(fired).toEqual([expect.stringContaining('Unknown expression "!in"')]);
        expect(warn).toHaveBeenCalledWith(expect.stringContaining('Mixing deprecated filter syntax with expression syntax'));
    });

    test('skips validation entirely when the validate option is false', () => {
        const {emitter, fired} = setup();

        expect(validateAndEmit(emitter, validateStyle.filter, {key, value: 'notafilter'}, {validate: false})).toBe(false);
        expect(fired).toEqual([]);
    });
});

describe('validateStyleAndEmit', () => {
    const coordinates = [[0, 0], [1, 0], [1, 1], [0, 1]];
    const styleWith = (sources: object, layers: object[] = []) =>
        ({version: 8, sources, layers}) as any as StyleSpecification;

    test('ignores sources the spec has no schema for, which MapLibre renders anyway', () => {
        const {emitter, fired} = setup();

        // `canvas` is not in the spec at all, and `addSourceType` can register anything else.
        const hasErrors = validateStyleAndEmit(emitter, styleWith({
            canvas: {type: 'canvas', canvas: 'c', coordinates},
            custom: {type: 'registered-at-runtime', url: 'https://example.com'}
        }, [{id: 'l', type: 'raster', source: 'canvas'}]));

        expect(hasErrors).toBe(false);
        expect(fired).toEqual([]);
    });

    test('still fails on a source the spec does know', () => {
        const {emitter, fired} = setup();

        const hasErrors = validateStyleAndEmit(emitter, styleWith({v: {type: 'vector', tiles: 'not-an-array'}}));

        expect(hasErrors).toBe(true);
        expect(fired).toEqual([expect.stringContaining('sources.v.tiles')]);
    });

    test('still fails elsewhere in a style that also holds an ignored source', () => {
        const {emitter, fired} = setup();

        const hasErrors = validateStyleAndEmit(emitter, styleWith(
            {canvas: {type: 'canvas', canvas: 'c', coordinates}},
            [{id: 'l', type: 'not-a-layer-type', source: 'canvas'}]
        ));

        expect(hasErrors).toBe(true);
        expect(fired).toEqual([expect.stringContaining('layers[0]')]);
    });
});
