import { afterEach, describe, expect, it, vi } from "vitest";
import { buildOnlineStyle } from "./onlineStyle";

type Layer = { id?: string; type?: string; "source-layer"?: string; source?: string };

/** A stand-in hosted style with its own POI + 3D-extrusion layers. */
const HOSTED = {
  version: 8,
  sources: { territory: { type: "vector", url: "pmtiles://x" } },
  layers: [
    { id: "background", type: "background" },
    { id: "building", type: "fill", source: "territory", "source-layer": "building" },
    { id: "road-minor", type: "line", source: "territory", "source-layer": "transportation", paint: { "line-color": "#ffffff", "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 4] } },
    { id: "road-major", type: "line", source: "territory", "source-layer": "transportation", paint: { "line-color": "#ffe0a3", "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 16, 8] } },
    { id: "building-3d", type: "fill-extrusion", source: "territory", "source-layer": "building" },
    { id: "poi-branded", type: "symbol", source: "territory", "source-layer": "poi" },
    { id: "some-poi-icon", type: "circle", source: "territory", "source-layer": "landuse" },
  ],
};

function mockFetchOk(): void {
  vi.stubGlobal(
    "fetch",
    vi.fn(async () => ({ ok: true, json: async () => structuredClone(HOSTED) }))
  );
}

function mockFetchFail(): void {
  vi.stubGlobal(
    "fetch",
    vi.fn(async () => {
      throw new Error("network down");
    })
  );
}

async function layersOf(...args: Parameters<typeof buildOnlineStyle>): Promise<Layer[]> {
  const json = await buildOnlineStyle(...args);
  return (JSON.parse(json) as { layers: Layer[] }).layers;
}

afterEach(() => {
  vi.unstubAllGlobals();
});

describe("buildOnlineStyle — hosted style toggles", () => {
  it("swaps the pmtiles source for the authenticated XYZ source", async () => {
    mockFetchOk();
    const style = JSON.parse(await buildOnlineStyle("KEY", "uk")) as {
      sources: Record<string, { tiles?: string[] }>;
    };
    expect(style.sources.territory.tiles?.[0]).toContain("api_key=KEY");
  });

  it("adds showcase POI + 3D layers when both toggles are on", async () => {
    mockFetchOk();
    const ids = (await layersOf("KEY", "uk", { pois: true, buildings3d: true })).map((l) => l.id);
    expect(ids).toContain("mmnav-poi");
    expect(ids).toContain("mmnav-poi-label");
    expect(ids).toContain("mmnav-building-3d");
  });

  it("strips the hosted POI layers and skips ours when POIs are off", async () => {
    mockFetchOk();
    const layers = await layersOf("KEY", "uk", { pois: false, buildings3d: true });
    const ids = layers.map((l) => l.id);
    expect(ids).not.toContain("poi-branded");
    expect(ids).not.toContain("some-poi-icon");
    expect(ids).not.toContain("mmnav-poi");
    expect(layers.some((l) => l["source-layer"] === "poi")).toBe(false);
  });

  it("strips fill-extrusion layers and skips ours when 3D is off", async () => {
    mockFetchOk();
    const layers = await layersOf("KEY", "uk", { pois: true, buildings3d: false });
    const ids = layers.map((l) => l.id);
    expect(layers.some((l) => l.type === "fill-extrusion")).toBe(false);
    expect(ids).not.toContain("mmnav-building-3d");
    // The flat building fill and POIs are untouched.
    expect(ids).toContain("building");
    expect(ids).toContain("mmnav-poi");
  });
});

describe("buildOnlineStyle — road weights aligned to the platform", () => {
  it("replaces road fill widths with the platform exponential curve and adds casings beneath fills", async () => {
    mockFetchOk();
    const layers = await layersOf("KEY", "uk", { pois: true, buildings3d: true });

    const major = layers.find((l) => l.id === "road-major") as { paint?: Record<string, unknown> };
    const width = major.paint?.["line-width"] as unknown[];
    // ["interpolate",["exponential",1.2],["zoom"], 6,1, 14,5.5, 20,26]
    expect(width[1]).toEqual(["exponential", 1.2]);
    expect(width[width.length - 1]).toBe(26); // ramps to z20

    const ids = layers.map((l) => l.id);
    expect(ids).toContain("road-major-casing");
    expect(ids).toContain("road-minor-casing");
    // Every casing is drawn before every road fill (junction-correct ordering).
    const lastCasing = Math.max(ids.indexOf("road-major-casing"), ids.indexOf("road-minor-casing"));
    const firstFill = Math.min(ids.indexOf("road-major"), ids.indexOf("road-minor"));
    expect(lastCasing).toBeLessThan(firstFill);
  });

  it("also aligns + casings the fallback style's roads when the hosted fetch fails", async () => {
    mockFetchFail();
    const ids = (await layersOf("KEY", "uk", { pois: true, buildings3d: true })).map((l) => l.id);
    expect(ids).toContain("road-major-casing");
    expect(ids).toContain("road-minor-casing");
    expect(ids).toContain("road-path");
  });
});

describe("buildOnlineStyle — fallback style", () => {
  it("still honours the toggles when the hosted fetch fails", async () => {
    mockFetchFail();
    const on = (await layersOf("KEY", "uk", { pois: true, buildings3d: true })).map((l) => l.id);
    expect(on).toContain("mmnav-poi");
    expect(on).toContain("mmnav-building-3d");

    mockFetchFail();
    const off = await layersOf("KEY", "uk", { pois: false, buildings3d: false });
    expect(off.map((l) => l.id)).not.toContain("mmnav-poi");
    expect(off.some((l) => l.type === "fill-extrusion")).toBe(false);
  });
});
