jest.mock("ol/layer/WebGLVectorTile", () => ({
  __esModule: true,
  default: class WebGLVectorTileLayer {
    opacity: number;
    visible: boolean;
    renderer = { renderComplete: true };
    source: any;
    style: any;
    properties: Record<string, unknown>;
    constructor(options: any = {}) {
      this.opacity = options.opacity ?? 1;
      this.visible = options.visible ?? true;
      this.source = options.source;
      this.style = options.style;
      this.properties = options.properties ?? {};
    }
    getOpacity() { return this.opacity; }
    setOpacity(value: number) { this.opacity = value; }
    getVisible() { return this.visible; }
    setVisible(value: boolean) { this.visible = value; }
    getExtent() { return undefined; }
    getMinZoom() { return 0; }
    getMaxZoom() { return 24; }
    getMinResolution() { return 0; }
    getMaxResolution() { return Infinity; }
    getPreload() { return 0; }
    setPreload() {}
    getZIndex() { return undefined; }
    setZIndex() {}
    on() {}
    un() {}
    setStyle(style: any) {
      this.style = style;
      this.renderer = { renderComplete: true };
    }
    updateStyleVariables() {}
    getRenderer() { return this.renderer; }
    dispose() {}
  },
}));

import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
import { VectorTileStyleSession } from "./vectorTileStyleSession";

const makeFeature = (id: string) => {
  const properties: Record<string, unknown> = { id };
  return {
    properties,
    properties_: properties,
    get: (key: string) => properties[key],
  };
};

const makeTile = (...features: ReturnType<typeof makeFeature>[]) => ({
  getFeatures: () => features,
});

describe("VectorTileStyleSession", () => {
  it("fans one feature state out to every loaded and future tile instance", () => {
    const featureA = makeFeature("P-1");
    const featureB = makeFeature("P-1");
    const listeners = new Set<(event: any) => void>();
    const source = {
      sourceTiles_: {
        a: makeTile(featureA),
        b: makeTile(featureB),
      },
      on: (_type: string, listener: (event: any) => void) => listeners.add(listener),
      un: (_type: string, listener: (event: any) => void) => listeners.delete(listener),
    } as any;
    const layer = {
      on: jest.fn(),
      un: jest.fn(),
      getOpacity: () => 1,
      setOpacity: jest.fn(),
      setStyle: jest.fn(),
    } as any;
    const session = new VectorTileStyleSession({
      layer,
      source,
      propertyKey: "healthRisk",
      defaultStyle: { "stroke-color": "blue" },
      buildStyle: () => ({ "stroke-color": "red" }),
    });

    session.commit(new Map([["P-1", 0.25]]));

    expect(featureA.properties.healthRisk).toBe(0.25);
    expect(featureB.properties.healthRisk).toBe(0.25);
    expect(layer.setStyle).toHaveBeenCalledTimes(1);

    const futureFeature = makeFeature("P-1");
    listeners.forEach((listener) => listener({ tile: makeTile(futureFeature) }));
    expect(futureFeature.properties.healthRisk).toBe(0.25);

    session.commit(new Map());
    expect(featureA.properties).not.toHaveProperty("healthRisk");
    expect(featureB.properties).not.toHaveProperty("healthRisk");

    session.dispose();
    expect(listeners.size).toBe(0);
  });

  it("keeps the active layer visible until the standby renderer is ready", async () => {
    const feature = makeFeature("P-1");
    const listeners = new Set<(event: any) => void>();
    const source = {
      sourceTiles_: { a: makeTile(feature) },
      on: (_type: string, listener: (event: any) => void) => listeners.add(listener),
      un: (_type: string, listener: (event: any) => void) => listeners.delete(listener),
    } as any;
    const baseLayer = new WebGLVectorTileLayer({
      source,
      style: { "stroke-color": "blue" },
    }) as any;
    const layers = [baseLayer];
    const postrenderListeners = new Set<() => void>();
    const map = {
      getLayers: () => ({
        getArray: () => layers,
        getLength: () => layers.length,
        insertAt: (index: number, layer: any) => layers.splice(index, 0, layer),
      }),
      removeLayer: (layer: any) => {
        const index = layers.indexOf(layer);
        if (index >= 0) layers.splice(index, 1);
      },
      on: (_type: string, listener: () => void) => postrenderListeners.add(listener),
      un: (_type: string, listener: () => void) => postrenderListeners.delete(listener),
      render: () => postrenderListeners.forEach((listener) => listener()),
    } as any;
    const styleKeys: string[] = [];
    const session = new VectorTileStyleSession({
      layer: baseLayer,
      source,
      map,
      buffered: true,
      propertyKey: "healthRisk",
      defaultStyle: { "stroke-color": "blue" },
      buildStyle: (propertyKey, versionKey) => {
        styleKeys.push(propertyKey, versionKey);
        return { "stroke-color": "red" };
      },
    });

    const commit = session.commit(new Map([["P-1", 0.25]]));
    expect(baseLayer.getOpacity()).toBe(1);
    await commit;

    expect(layers).toHaveLength(2);
    expect(baseLayer.getOpacity()).toBe(0);
    expect(layers[1].getOpacity()).toBe(1);
    expect(styleKeys).toEqual([
      "healthRisk_buffer_1",
      "healthRisk_render_version_buffer_1",
    ]);
    expect(styleKeys.every((key) => !key.includes("__"))).toBe(true);
    session.dispose();
    expect(layers).toHaveLength(1);
  });

  it("does not replace a renderer while an obsolete buffer is still building", async () => {
    const feature = makeFeature("P-1");
    const source = {
      sourceTiles_: { a: makeTile(feature) },
      on: jest.fn(),
      un: jest.fn(),
    } as any;
    const baseLayer = new WebGLVectorTileLayer({
      source,
      style: { "stroke-color": "blue" },
    }) as any;
    const layers = [baseLayer];
    const postrenderListeners = new Set<() => void>();
    const map = {
      getLayers: () => ({
        getArray: () => layers,
        getLength: () => layers.length,
        insertAt: (index: number, layer: any) => layers.splice(index, 0, layer),
      }),
      removeLayer: jest.fn(),
      on: (_type: string, listener: () => void) => postrenderListeners.add(listener),
      un: (_type: string, listener: () => void) => postrenderListeners.delete(listener),
      render: jest.fn(),
    } as any;
    const session = new VectorTileStyleSession({
      layer: baseLayer,
      source,
      map,
      buffered: true,
      propertyKey: "healthRisk",
      defaultStyle: { "stroke-color": "blue" },
      buildStyle: () => ({ "stroke-color": "red" }),
    });

    const firstCommit = session.commit(new Map([["P-1", 0.25]]));
    await Promise.resolve();
    await Promise.resolve();
    const standbyLayer = layers[1] as any;
    standbyLayer.renderer = { renderComplete: false };
    const setStyle = jest.spyOn(standbyLayer, "setStyle");

    const latestCommit = session.commit(new Map([["P-1", 0.75]]));
    expect(setStyle).not.toHaveBeenCalled();

    postrenderListeners.forEach((listener) => listener());
    await expect(firstCommit).resolves.toBe(false);
    await Promise.resolve();
    await Promise.resolve();
    expect(setStyle).toHaveBeenCalledTimes(1);

    postrenderListeners.forEach((listener) => listener());
    postrenderListeners.forEach((listener) => listener());
    await expect(latestCommit).resolves.toBe(true);
    expect(feature.properties.healthRisk_buffer_1).toBe(0.75);
  });

  it("normalizes generated shader identifiers and mirrors variable updates", () => {
    const source = { sourceTiles_: {}, on: jest.fn(), un: jest.fn() } as any;
    const layer = {
      on: jest.fn(),
      un: jest.fn(),
      getOpacity: () => 1,
      setOpacity: jest.fn(),
      setStyle: jest.fn(),
      updateStyleVariables: jest.fn(),
    } as any;
    const keys: string[] = [];
    const session = new VectorTileStyleSession({
      layer,
      source,
      propertyKey: "healthRisk__unsafe",
      defaultStyle: { "stroke-color": "gray" },
      buildStyle: (propertyKey, versionKey) => {
        keys.push(propertyKey, versionKey);
        return { "stroke-color": "red" };
      },
    });
    session.updateStyleVariables({ tj_color_0: "red" });
    session.commit(new Map());

    expect(layer.updateStyleVariables).toHaveBeenCalledWith({ tj_color_0: "red" });
    expect(keys.every((key) => !key.includes("__"))).toBe(true);
  });
});
