import type { FeatureCollection } from "geojson";
import type { Map as MapLibreMap } from "maplibre-gl";
import { describe, expect, it, vi } from "vitest";
import {
  FLOW_LINE_LAYER_ID,
  createFlowOverlayState,
  showFlowOverlay
} from "./flow-overlay";
import { WorkbenchMapController } from "./workbench-map-controller";

describe("WorkbenchMapController", () => {
  it("locates points with zoom 18 and keeps selected state untouched", async () => {
    const map = createMap();
    const controller = createController(map, pointCollection);
    await controller.locateAndHighlight({ sourceId: "junctions", featureId: "J-1" });
    expect(map.easeTo).toHaveBeenCalledWith(expect.objectContaining({ center: [120.7, 28], zoom: 18 }));
    expect(map.setFeatureState).toHaveBeenCalledWith(expect.objectContaining({ source: "junctions", id: "J-1" }), { highlighted: true });
    const clearHighlight = controller.clearHighlight;
    clearHighlight();
    expect(map.removeFeatureState).toHaveBeenCalledWith(expect.anything(), "highlighted");
    expect(map.removeFeatureState).not.toHaveBeenCalledWith(expect.anything(), "selected");
  });

  it("fits line bounds and uses responsive workbench padding", async () => {
    const map = createMap();
    const padding = { top: 50, right: 420, bottom: 50, left: 320 };
    const controller = createController(map, lineCollection, padding);
    await controller.locateAndHighlight({ sourceId: "pipes", featureId: "P-1" });
    expect(map.fitBounds).toHaveBeenCalledWith([[120, 28], [121, 29]], expect.objectContaining({ padding, maxZoom: 18 }));
  });

  it("zooms to a feature without changing its highlight state", async () => {
    const map = createMap();
    const controller = createController(map, pointCollection);

    await controller.zoomToFeature({ sourceId: "junctions", featureId: "J-1" });

    expect(map.easeTo).toHaveBeenCalledWith(expect.objectContaining({ center: [120.7, 28], zoom: 18 }));
    expect(map.setFeatureState).not.toHaveBeenCalled();
    expect(controller.getSnapshot().target).toBeNull();
  });

  it("queries GeoServer WFS directly when no feature adapter is supplied", async () => {
    const map = createMap();
    const fetchMock = vi.fn(async (_input: RequestInfo | URL) => new Response(JSON.stringify(pointCollection), {
      status: 200,
      headers: { "Content-Type": "application/json" }
    }));
    vi.stubGlobal("fetch", fetchMock);
    const controller = new WorkbenchMapController({
      getMap: () => map as unknown as MapLibreMap,
      isReady: () => true,
      getPadding: () => ({ top: 0, right: 0, bottom: 0, left: 0 })
    });

    await controller.highlight({ sourceId: "junctions", featureId: "J-1" });

    const requestUrl = new URL(String(fetchMock.mock.calls[0]?.[0]));
    expect(requestUrl.pathname).toBe("/geoserver/tjwater/ows");
    expect(requestUrl.searchParams.get("request")).toBe("GetFeature");
    expect(requestUrl.searchParams.get("cql_filter")).toBe("\"id\" IN ('J-1')");
    expect(map.setFeatureState).toHaveBeenCalled();
    vi.unstubAllGlobals();
  });

  it("renders flow from the complete pipes vector source", async () => {
    const map = createMap();
    const layers = new Map<string, unknown>();
    map.getLayer.mockImplementation((id: string) => layers.get(id));
    map.addLayer.mockImplementation((layer: { id: string }) => {
      layers.set(layer.id, layer);
      return map as never;
    });
    vi.stubGlobal("window", {
      matchMedia: () => ({ matches: true }),
      requestAnimationFrame: vi.fn(),
      cancelAnimationFrame: vi.fn()
    });

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

    expect(layers.get(FLOW_LINE_LAYER_ID)).toMatchObject({
      source: "pipes",
      "source-layer": "geo_pipes_mat"
    });
    expect([...layers.keys()].some((id) => id.includes("arrows"))).toBe(false);
    vi.unstubAllGlobals();
  });

  it("updates signed pipe velocity and exposes direction counts", () => {
    const map = createMap();
    const controller = createController(map, pointCollection);

    controller.updatePipeFlowCalculation({ pipeId: "P-1", velocity: -1.8 });

    expect(map.setFeatureState).toHaveBeenCalledWith(
      expect.objectContaining({
        source: "pipes",
        sourceLayer: "geo_pipes_mat",
        id: "P-1"
      }),
      { flowDirection: -1 }
    );
    expect(controller.getSnapshot().flowSummary).toMatchObject({
      total: 1,
      forward: 0,
      reverse: 1,
      stopped: 0
    });
  });

  it("replaces complete pipe calculation snapshots", async () => {
    const map = createMap();
    const controller = createController(map, pointCollection);

    await controller.replacePipeFlowCalculations([
      { pipeId: "P-1", velocity: 2 },
      { pipeId: "P-2", velocity: 0 },
      { pipeId: "", velocity: 4 }
    ]);

    expect(controller.getSnapshot()).toMatchObject({
      pending: false,
      flowSummary: {
        total: 2,
        forward: 1,
        reverse: 0,
        stopped: 1,
        invalid: 1
      }
    });
  });

  it("keeps MVT labels per group and supports group or global clearing", async () => {
    const map = createMap();
    map.getLayer.mockImplementation((id: string) => id.startsWith("workbench-value-label-") ? {} as never : undefined);
    const controller = createController(map, pointCollection);

    await controller.showValueLabel({
      sourceId: "pipes",
      property: "diameter",
      precision: 0,
      unit: "mm",
      showValue: true,
      showId: false,
      textSize: 12,
      textColor: "#0F172A"
    });
    map.setLayoutProperty.mockClear();

    await controller.showValueLabel({
      sourceId: "junctions",
      property: "pressure",
      precision: 1,
      unit: "m",
      showValue: true,
      showId: true,
      textSize: 13,
      textColor: "#0F172A"
    });

    expect(map.setLayoutProperty).toHaveBeenCalledWith(
      "workbench-value-label-junctions",
      "text-field",
      expect.arrayContaining(["concat"])
    );
    expect(map.setLayoutProperty).toHaveBeenCalledWith(
      "workbench-value-label-junctions",
      "visibility",
      "visible"
    );
    expect(map.setFilter).toHaveBeenCalledWith(
      "workbench-value-label-junctions",
      ["has", "pressure"]
    );
    expect(controller.getSnapshot().valueLabels.junctions).toMatchObject({
      sourceId: "junctions",
      property: "pressure",
      showId: true
    });
    expect(controller.getSnapshot().valueLabels.pipes).toMatchObject({
      sourceId: "pipes",
      property: "diameter"
    });
    expect(map.setLayoutProperty).not.toHaveBeenCalledWith(
      "workbench-value-label-pipes",
      "visibility",
      "none"
    );

    controller.clearValueLabel("junctions");

    expect(map.setLayoutProperty).toHaveBeenCalledWith(
      "workbench-value-label-junctions",
      "visibility",
      "none"
    );
    expect(controller.getSnapshot().valueLabels).toEqual({
      pipes: expect.objectContaining({ property: "diameter" })
    });

    const clearValueLabel = controller.clearValueLabel;
    clearValueLabel();

    expect(map.setLayoutProperty).toHaveBeenCalledWith(
      "workbench-value-label-pipes",
      "visibility",
      "none"
    );
    expect(controller.getSnapshot().valueLabels).toEqual({});
  });

  it("applies icon scaling to symbol-backed point groups", () => {
    const map = createMap();
    map.getLayer.mockReturnValue({} as never);
    const controller = createController(map, pointCollection);

    controller.applyLayerGroupStyle("valves", { iconMultiplier: 1.8 });

    expect(map.setLayoutProperty).toHaveBeenCalledWith(
      "valves-symbol",
      "icon-size",
      expect.arrayContaining(["*", expect.anything(), 1.8])
    );
  });

  it("does not clear the last valid highlight when a target is missing", async () => {
    const map = createMap();
    let collection = pointCollection;
    const controller = new WorkbenchMapController({
      getMap: () => map as unknown as MapLibreMap,
      isReady: () => true,
      getPadding: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
      fetchFeatures: async () => collection
    });
    await controller.highlight({ sourceId: "junctions", featureId: "J-1" });
    collection = { type: "FeatureCollection", features: [] };
    await controller.highlight({ sourceId: "junctions", featureId: "missing" });
    expect(controller.getSnapshot().target).toEqual({ sourceId: "junctions", featureId: "J-1" });
    expect(controller.getSnapshot().errorCode).toBe("FEATURE_NOT_FOUND");
    expect(map.removeFeatureState).not.toHaveBeenCalled();
  });

  it("renders SCADA analysis through the registered GeoJSON source", async () => {
    const map = createMap();
    const source = { setData: vi.fn() };
    map.getSource.mockReturnValue(source);
    const controller = createController(map, {
      type: "FeatureCollection",
      features: [
        {
          type: "Feature",
          geometry: { type: "Point", coordinates: [120.7, 28] },
          properties: { sensor_id: "MP01" }
        }
      ]
    });

    await expect(
      controller.renderScadaAnalysis([{ sensor_id: "MP01", level: "high" }])
    ).resolves.toMatchObject({
      rendered_ids: ["MP01"],
      fitted: true
    });
    expect(source.setData).toHaveBeenCalled();
    expect(controller.getSnapshot()).toMatchObject({
      errorCode: null,
      pending: false,
      scadaAnalysis: {
        rendered_ids: ["MP01"],
        fitted: true
      }
    });
  });
});

const pointCollection: FeatureCollection = { type: "FeatureCollection", features: [{ type: "Feature", geometry: { type: "Point", coordinates: [120.7, 28] }, properties: { id: "J-1" } }] };
const lineCollection: FeatureCollection = { type: "FeatureCollection", features: [{ type: "Feature", geometry: { type: "LineString", coordinates: [[120, 28], [121, 29]] }, properties: { id: "P-1" } }] };

function createMap() {
  const images = new Set<string>();
  return {
    easeTo: vi.fn(), fitBounds: vi.fn(), setFeatureState: vi.fn(), removeFeatureState: vi.fn(),
    getSource: vi.fn(), getLayer: vi.fn(), addLayer: vi.fn(), setPaintProperty: vi.fn(),
    setLayoutProperty: vi.fn(), setFilter: vi.fn(), removeControl: vi.fn(), triggerRepaint: vi.fn(),
    hasImage: vi.fn((id: string) => images.has(id)),
    addImage: vi.fn((id: string) => {
      images.add(id);
    })
  };
}

function createController(map: ReturnType<typeof createMap>, collection: FeatureCollection, padding = { top: 48, right: 48, bottom: 48, left: 48 }) {
  return new WorkbenchMapController({
    getMap: () => map as unknown as MapLibreMap,
    isReady: () => true,
    getPadding: () => padding,
    fetchFeatures: vi.fn(async () => collection)
  });
}
