import { describe, expect, it, vi } from "vitest";

import { fetchSpeedLimits, speedLimitKphAtFraction } from "./speedLimits";

function okResponse(edges: unknown): Response {
  return { ok: true, json: async () => ({ edges }) } as unknown as Response;
}

describe("fetchSpeedLimits", () => {
  it("builds a cumulative track from trace_attributes edges (km → m)", async () => {
    const fetchImpl = vi.fn(async () =>
      okResponse([
        { length: 1.0, speed_limit: 48 }, // 30 mph
        { length: 0.5, speed_limit: 32 }, // 20 mph
        { length: 2.0, speed_limit: 0 }, // unposted
      ])
    );
    const track = await fetchSpeedLimits("snk_x", "abc", fetchImpl as unknown as typeof fetch);
    expect(fetchImpl).toHaveBeenCalledWith(
      "https://api.mapmap.ai/trace_attributes",
      expect.objectContaining({ method: "POST" })
    );
    expect(track).toEqual({
      cumMeters: [1000, 1500, 3500],
      limitsKph: [48, 32, 0],
      totalMeters: 3500,
    });
    // The request carries the encoded polyline + map_snap.
    const [, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit];
    const body = JSON.parse(init.body as string);
    expect(body.encoded_polyline).toBe("abc");
    expect(body.shape_match).toBe("map_snap");
  });

  it("returns null without a key or shape, and on a non-2xx / network error", async () => {
    const spy = vi.fn();
    expect(await fetchSpeedLimits("", "abc", spy as unknown as typeof fetch)).toBeNull();
    expect(await fetchSpeedLimits("snk_x", "", spy as unknown as typeof fetch)).toBeNull();
    expect(spy).not.toHaveBeenCalled();

    const fail = vi.fn(async () => ({ ok: false, json: async () => ({}) }) as unknown as Response);
    expect(await fetchSpeedLimits("snk_x", "abc", fail as unknown as typeof fetch)).toBeNull();

    const throws = vi.fn(async () => {
      throw new Error("offline");
    });
    expect(await fetchSpeedLimits("snk_x", "abc", throws as unknown as typeof fetch)).toBeNull();
  });
});

describe("speedLimitKphAtFraction", () => {
  const track = { cumMeters: [1000, 1500, 3500], limitsKph: [48, 32, 0], totalMeters: 3500 };

  it("returns the posted limit for the edge at that fraction", () => {
    expect(speedLimitKphAtFraction(track, 0)).toBe(48); // start, first edge
    expect(speedLimitKphAtFraction(track, 1000 / 3500 - 0.01)).toBe(48); // within edge 0
    expect(speedLimitKphAtFraction(track, 1200 / 3500)).toBe(32); // within edge 1
  });

  it("returns null on an unposted edge and clamps out-of-range fractions", () => {
    expect(speedLimitKphAtFraction(track, 0.9)).toBeNull(); // edge 2 is unposted (0)
    expect(speedLimitKphAtFraction(track, 2)).toBeNull(); // clamps to end (unposted)
    expect(speedLimitKphAtFraction(track, -1)).toBe(48); // clamps to start
  });
});
