import type { Map as MapLibreMap } from "maplibre-gl";
import { describe, expect, it, vi } from "vitest";
import {
  FLOW_LINE_LAYER_ID,
  FLOW_REVERSE_LINE_LAYER_ID,
  clearPipeFlowCalculations,
  createFlowOverlayState,
  getPipeFlowDirection,
  replacePipeFlowCalculations,
  showFlowOverlay,
  updatePipeFlowCalculation
} from "./flow-overlay";

describe("flow overlay", () => {
  it("classifies signed velocities relative to pipe geometry", () => {
    expect(getPipeFlowDirection(2.5)).toBe(1);
    expect(getPipeFlowDirection(-0.01)).toBe(-1);
    expect(getPipeFlowDirection(0)).toBe(0);
    expect(getPipeFlowDirection(Number.NaN)).toBe(0);
  });

  it("creates native forward and reverse layers with a deterministic all-pipe mock", async () => {
    const { map, layers } = createMap();
    const state = createFlowOverlayState();

    await showFlowOverlay(map as unknown as MapLibreMap, state, true);

    expect(layers.get(FLOW_LINE_LAYER_ID)).toMatchObject({
      source: "pipes",
      "source-layer": "geo_pipes_mat",
      paint: { "line-pattern": "workbench-network-flow-pattern-forward" }
    });
    expect(layers.get(FLOW_REVERSE_LINE_LAYER_ID)).toMatchObject({
      source: "pipes",
      "source-layer": "geo_pipes_mat",
      paint: { "line-pattern": "workbench-network-flow-pattern-reverse" }
    });
    expect([...layers.keys()].some((id) => id.includes("arrows"))).toBe(false);
    expect(
      JSON.stringify((layers.get(FLOW_LINE_LAYER_ID) as { paint: Record<string, unknown> }).paint["line-opacity"])
    ).toContain('["%",["to-number",["id"],0],2]');
  });

  it("updates one pipe direction without rebuilding its vector source", () => {
    const { map } = createMap();
    const state = createFlowOverlayState();

    const first = updatePipeFlowCalculation(map as unknown as MapLibreMap, state, {
      pipeId: "P-1",
      velocity: -2.4
    });
    const second = updatePipeFlowCalculation(map as unknown as MapLibreMap, state, {
      pipeId: "P-1",
      velocity: 1.2
    });

    expect(map.setFeatureState).toHaveBeenNthCalledWith(1, {
      source: "pipes",
      sourceLayer: "geo_pipes_mat",
      id: "P-1"
    }, {
      flowDirection: -1
    });
    expect(map.setFeatureState).toHaveBeenNthCalledWith(2, expect.anything(), {
      flowDirection: 1
    });
    expect(first).toMatchObject({ total: 1, reverse: 1 });
    expect(second).toMatchObject({ total: 1, forward: 1 });
    expect(map.getSource).not.toHaveBeenCalled();
  });

  it("replaces a snapshot and clears only stale flow state keys", async () => {
    const { map } = createMap();
    const state = createFlowOverlayState();
    updatePipeFlowCalculation(map as unknown as MapLibreMap, state, {
      pipeId: "P-old",
      velocity: 1
    });
    map.removeFeatureState.mockClear();

    const summary = await replacePipeFlowCalculations(
      map as unknown as MapLibreMap,
      state,
      [
        { pipeId: "P-new", velocity: -3 },
        { pipeId: "P-new", velocity: 4 },
        { pipeId: "", velocity: 1 }
      ]
    );

    expect(map.removeFeatureState).toHaveBeenCalledWith(expect.objectContaining({
      id: "P-old"
    }), "flowDirection");
    expect(map.removeFeatureState).not.toHaveBeenCalledWith(expect.anything(), "highlighted");
    expect(summary).toEqual({
      total: 1,
      forward: 1,
      reverse: 0,
      stopped: 0,
      invalid: 1
    });
  });

  it("returns to the all-pipe mock after clearing explicit calculations", async () => {
    const { map } = createMap();
    const state = createFlowOverlayState();
    await showFlowOverlay(map as unknown as MapLibreMap, state, true);
    updatePipeFlowCalculation(map as unknown as MapLibreMap, state, {
      pipeId: "P-1",
      velocity: 1
    });

    await clearPipeFlowCalculations(map as unknown as MapLibreMap, state);

    const latestOpacity = map.setPaintProperty.mock.calls
      .filter(([, property]) => property === "line-opacity")
      .at(-1)?.[2];
    expect(JSON.stringify(latestOpacity)).toContain('["%",["to-number",["id"],0],2]');
  });

  it("applies large snapshots in animation-frame batches", async () => {
    const callbacks: FrameRequestCallback[] = [];
    vi.stubGlobal("window", {
      matchMedia: () => ({ matches: false }),
      requestAnimationFrame: vi.fn((callback: FrameRequestCallback) => {
        callbacks.push(callback);
        return callbacks.length;
      }),
      cancelAnimationFrame: vi.fn()
    });
    const { map } = createMap();
    const state = createFlowOverlayState();
    const records = Array.from({ length: 2_001 }, (_, index) => ({
      pipeId: index,
      velocity: index % 2 === 0 ? 1 : -1
    }));

    const pending = replacePipeFlowCalculations(
      map as unknown as MapLibreMap,
      state,
      records
    );
    expect(map.setFeatureState).toHaveBeenCalledTimes(2_000);
    callbacks.shift()?.(16);
    const summary = await pending;

    expect(map.setFeatureState).toHaveBeenCalledTimes(2_001);
    expect(summary.total).toBe(2_001);
    vi.unstubAllGlobals();
  });
});

function createMap() {
  const layers = new Map<string, unknown>();
  const images = new Set<string>();
  const map = {
    addImage: vi.fn((id: string) => {
      images.add(id);
    }),
    addLayer: vi.fn((layer: { id: string }) => {
      layers.set(layer.id, layer);
      return map;
    }),
    getLayer: vi.fn((id: string) => layers.get(id)),
    getSource: vi.fn(),
    hasImage: vi.fn((id: string) => images.has(id)),
    removeFeatureState: vi.fn(),
    setFeatureState: vi.fn(),
    setLayoutProperty: vi.fn(),
    setPaintProperty: vi.fn((id: string, property: string, value: unknown) => {
      const layer = layers.get(id) as { paint?: Record<string, unknown> } | undefined;
      if (layer?.paint) layer.paint[property] = value;
    }),
    triggerRepaint: vi.fn()
  };
  return { map, layers };
}
