import type { Map as MapLibreMap } from "maplibre-gl";
import { describe, expect, it, vi } from "vitest";
import {
  SCADA_HIT_LAYER_ID,
  SCADA_ICON_EXPRESSION,
  SCADA_IMAGE_IDS,
  SCADA_MAP_ICON_PATHS,
  registerScadaImages,
  scadaFallbackLayers,
  scadaLayers
} from "./scada";
import { waterNetworkLayers } from "./layers";
import { MAP_STYLE_TOKENS } from "./map-colors";
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";

describe("SCADA map presentation", () => {
  it("maps the authoritative device types and falls back to unknown", () => {
    expect(SCADA_ICON_EXPRESSION).toEqual([
      "match", ["downcase", ["to-string", ["coalesce", ["get", "type"], ""]]],
      "pipe_flow", SCADA_IMAGE_IDS.flow,
      "pressure", SCADA_IMAGE_IDS.pressure,
      SCADA_IMAGE_IDS.pressure
    ]);
  });

  it("keeps the hit target last in normal and fallback presentations", () => {
    expect(scadaLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
    expect(scadaFallbackLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
  });

  it("keeps the icon and selected ring consistent with junction selection", () => {
    const symbolLayer = scadaLayers.find((layer) => layer.id === "scada-symbol");
    const outerLayer = scadaLayers.find((layer) => layer.id === "scada-selected-outer");
    const selectedLayer = scadaLayers.find((layer) => layer.id === "scada-selected");
    const junctionOuterLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected-outer");
    const junctionSelectedLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected");
    const junctionOuterPaint = junctionOuterLayer?.paint as Record<string, unknown> | undefined;
    const junctionSelectedPaint = junctionSelectedLayer?.paint as Record<string, unknown> | undefined;

    expect(symbolLayer).toMatchObject({
      type: "symbol",
      layout: {
        "icon-image": SCADA_ICON_EXPRESSION,
        "icon-size": SUPPLY_LAYER_VISUALS.scada.iconSize
      }
    });
    expect(outerLayer).toMatchObject({
      type: "circle",
      paint: {
        "circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius,
        "circle-stroke-width": junctionOuterPaint?.["circle-stroke-width"]
      }
    });
    expect(selectedLayer).toMatchObject({
      type: "circle",
      paint: {
        "circle-stroke-width": junctionSelectedPaint?.["circle-stroke-width"]
      }
    });
    expect(JSON.stringify((selectedLayer as { paint?: unknown })?.paint)).toContain("highlighted");
  });

  it("uses distinct colors for authoritative pressure and pipe flow types", () => {
    const fillLayer = scadaLayers.find((layer) => layer.id === "scada-fill");
    const colorExpression = JSON.stringify((fillLayer as { paint?: unknown })?.paint);

    expect(colorExpression).toContain("pipe_flow");
    expect(colorExpression).toContain(MAP_STYLE_TOKENS.supply.scadaFlow);
    expect(colorExpression).toContain(MAP_STYLE_TOKENS.supply.scadaPressure);
  });

  it("uses MapLibre-compatible PNG assets", () => {
    expect(Object.values(SCADA_MAP_ICON_PATHS)).toHaveLength(2);
    Object.values(SCADA_MAP_ICON_PATHS).forEach((path) => expect(path).toMatch(/\.png$/));
  });

  it("registers all map images at two-times pixel density", async () => {
    const image = { width: 64, height: 64 };
    const map = createImageMap(() => Promise.resolve({ data: image }));

    await expect(registerScadaImages(map)).resolves.toEqual({ status: "ready", failedImageIds: [] });
    expect(map.loadImage).toHaveBeenCalledTimes(2);
    expect(map.addImage).toHaveBeenCalledTimes(2);
    expect(map.addImage).toHaveBeenCalledWith(expect.any(String), image, { pixelRatio: 2 });
  });

  it("falls back to the pressure image when flow fails", async () => {
    const image = { width: 64, height: 64 };
    const map = createImageMap((path) => path.includes("scada-flow")
      ? Promise.reject(new Error("decode failed"))
      : Promise.resolve({ data: image }));

    await expect(registerScadaImages(map)).resolves.toEqual({
      status: "degraded",
      failedImageIds: [SCADA_IMAGE_IDS.flow]
    });
    expect(map.addImage).toHaveBeenCalledWith(SCADA_IMAGE_IDS.flow, image, { pixelRatio: 2 });
  });

  it("returns unavailable instead of rejecting when the fallback image fails", async () => {
    const map = createImageMap(() => Promise.reject(new Error("unsupported image")));

    await expect(registerScadaImages(map)).resolves.toEqual({
      status: "unavailable",
      failedImageIds: Object.values(SCADA_IMAGE_IDS)
    });
    expect(scadaFallbackLayers.map((layer) => layer.id).slice(0, 3)).toEqual([
      "scada-casing",
      "scada-fill",
      "scada-fallback"
    ]);
  });
});

function createImageMap(loadImage: (path: string) => Promise<{ data: { width: number; height: number } }>) {
  const loadedImageIds = new Set<string>();
  return {
    loadImage: vi.fn(loadImage),
    hasImage: vi.fn((imageId: string) => loadedImageIds.has(imageId)),
    addImage: vi.fn((imageId: string) => loadedImageIds.add(imageId))
  } as unknown as Pick<MapLibreMap, "loadImage" | "addImage" | "hasImage"> & {
    loadImage: ReturnType<typeof vi.fn>;
    addImage: ReturnType<typeof vi.fn>;
  };
}
