import {beforeEach, afterEach, test, expect, vi} from 'vitest';
import {createMap, beforeMapTest} from '../../util/test/util.ts';
import {GPUInitializationError} from '../../util/gpu_initialization_error.ts';

let originalGetContext: typeof HTMLCanvasElement.prototype.getContext;
beforeEach(() => {
    beforeMapTest();
    global.fetch = null;
    originalGetContext = HTMLCanvasElement.prototype.getContext;
});

afterEach(() => {
    HTMLCanvasElement.prototype.getContext = originalGetContext;
});

test('does not fire "webglcontextlost" after remove has been called', () => {
    const map = createMap();
    const canvas = map.getCanvas();
    const spy = vi.fn();
    map.on('webglcontextlost', spy);
    map.remove();
    // Dispatch the event manually because at the time of this writing, gl does not support
    // the WEBGL_lose_context extension.
    canvas.dispatchEvent(new window.Event('webglcontextlost'));
    expect(spy).not.toHaveBeenCalled();
});

test('handles "webglcontextlost" when map is created without style', () => {
    // This test verifies fix for #7022 - map should not throw when WebGL context
    // is lost before the style is loaded (i.e., when style is null/undefined)
    const map = createMap({deleteStyle: true});
    const canvas = map.getCanvas();
    const spy = vi.fn();
    map.on('webglcontextlost', spy);
    // Dispatch the event manually because at the time of this writing, gl does not support
    // the WEBGL_lose_context extension.
    expect(() => {
        canvas.dispatchEvent(new window.Event('webglcontextlost'));
    }).not.toThrow();
    expect(spy).toHaveBeenCalled();
    map.remove();
});

test('handles "webglcontextrestored" when map is created without style', async () => {
    const map = createMap({deleteStyle: true});
    const canvas = map.getCanvas();

    const contextLostPromise = map.once('webglcontextlost');
    canvas.dispatchEvent(new window.Event('webglcontextlost'));
    await contextLostPromise;

    expect(() => {
        canvas.dispatchEvent(new window.Event('webglcontextrestored'));
    }).not.toThrow();
    map.remove();
});

test('style methods do not throw after WebGL context loss', async () => {
    const map = createMap();
    const canvas = map.getCanvas();

    const contextLostPromise = map.once('webglcontextlost');
    canvas.dispatchEvent(new window.Event('webglcontextlost'));
    await contextLostPromise;

    expect(() => {
        map.getLayer('missing-layer');
        map.getSource('missing-source');
        map.getLayersOrder();
        map.listImages();
        map.setFilter('missing-layer', null);
        map.setPaintProperty('missing-layer', 'background-color', '#000000');
    }).not.toThrow();

    map.remove();
});

test('does not fire "webglcontextrestored" after remove has been called', async () => {
    const map = createMap();
    const canvas = map.getCanvas();

    const contextLostPromise =  map.once('webglcontextlost');

    // Dispatch the event manually because at the time of this writing, gl does not support
    // the WEBGL_lose_context extension.
    canvas.dispatchEvent(new window.Event('webglcontextlost'));

    await contextLostPromise;
    const spy = vi.fn();
    map.on('webglcontextrestored', spy);
    map.remove();
    canvas.dispatchEvent(new window.Event('webglcontextrestored'));
    expect(spy).not.toHaveBeenCalled();
});

test('a WebGL style image is told to release its GPU resources on context loss, and survives it', async () => {
    const map = createMap();
    const canvas = map.getCanvas();
    await map.once('load');

    const userImage = {
        width: 2, height: 2,
        data: {renderWithWebGL: vi.fn()},
        onRemove: vi.fn()
    };
    map.addImage('gpu-image', userImage);

    const contextLostPromise = map.once('webglcontextlost');
    canvas.dispatchEvent(new window.Event('webglcontextlost'));
    await contextLostPromise;

    expect(userImage.onRemove).toHaveBeenCalled();

    const contextRestoredPromise = map.once('webglcontextrestored');
    canvas.dispatchEvent(new window.Event('webglcontextrestored'));
    await contextRestoredPromise;

    expect(map.hasImage('gpu-image')).toBe(true);
    expect(map.getImage('gpu-image').userImage).toBe(userImage);
    expect(map.style.imageManager.updatedImages['gpu-image']).toBe(true);

    map.remove();
});

test('WebGL2 context creation error fires ErrorEvent with structured GPUInitializationError', () => {
    HTMLCanvasElement.prototype.getContext = function (type: string) {
        if (type === 'webgl2') {
            const errorEvent = new Event('webglcontextcreationerror');
            (errorEvent as any).statusMessage = 'mocked webglcontextcreationerror message';
            (this as HTMLCanvasElement).dispatchEvent(errorEvent);
            return null;
        }
    };
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    createMap({canvasContextAttributes: {antialias: true}});
    const err = consoleErrorSpy.mock.calls[0][0];
    expect(err.constructor).toBe(GPUInitializationError);
    expect(err.message).toBe('WebGL2 is required to display this map. We are sorry, but it seems that your browser does not support WebGL2, a technology for rendering 3D graphics on the web. Read more on https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL');
    expect(err.statusMessage).toBe('mocked webglcontextcreationerror message');
    expect(err.requestedAttributes.antialias).toBe(true);
    consoleErrorSpy.mockRestore();
});

test('GPUInitializationError has null statusMessage when no webglcontextcreationerror is dispatched', () => {
    HTMLCanvasElement.prototype.getContext = function (_type: string) {
        return null;
    };
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    createMap();
    const err = consoleErrorSpy.mock.calls[0][0];
    expect(err.constructor).toBe(GPUInitializationError);
    expect(err.statusMessage).toBeNull();
    consoleErrorSpy.mockRestore();
});

test('Hit WebGL max drawing buffer limit', () => {
    // Simulate a device with MAX_TEXTURE_SIZE=16834 and max rendering area of ~32Mpx
    const container = window.document.createElement('div');
    Object.defineProperty(container, 'clientWidth', {value: 8000});
    Object.defineProperty(container, 'clientHeight', {value: 4500});
    const map = createMap({container, maxCanvasSize: [16834, 16834], pixelRatio: 1});
    vi.spyOn(map.painter.context.gl, 'drawingBufferWidth', 'get').mockReturnValue(7536);
    vi.spyOn(map.painter.context.gl, 'drawingBufferHeight', 'get').mockReturnValue(4239);
    map.resize();
    expect(map.getCanvas().width).toBe(7536);
    expect(map.getCanvas().height).toBe(4239);
    // Check if maxCanvasSize is updated
    expect(map._maxCanvasSize).toEqual([7536, 4239]);
});
