import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import {
  createDashboardTileCache,
  dashboardTileCacheKey,
  handleDashboardTilesRoute,
  handleDashboardTileRoute,
  loadDashboardResponse,
  loadDashboardTilesResponse,
  loadDashboardTileResponse,
  type DashboardTileResolutionLogEvent,
} from "./dashboard";

describe("dashboard API route", () => {
  const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);

  afterEach(() => {
    consoleInfo.mockClear();
  });

  afterAll(() => {
    consoleInfo.mockRestore();
  });

  test("returns the ready dashboard shell without hydrating live datasources", async () => {
    const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
      new Error("live datasource fetch should not run for the shell response"),
    );

    const response = await loadDashboardResponse({
      refreshSeedDocument: true,
      seedIfEmpty: true,
    });

    expect(response.state).toBe("ready");
    if (response.state !== "ready") throw new Error("expected ready dashboard");
    expect(response.document.metadata.title).toBe(
      dimensionLabDashboardFixture.metadata.title,
    );
    expect(fetch).not.toHaveBeenCalled();

    fetch.mockRestore();
  });

  test("reports when client-side live hydration is disabled", async () => {
    const previous = process.env.DISABLE_LIVE_DATASOURCES;
    process.env.DISABLE_LIVE_DATASOURCES = "1";

    try {
      const response = await loadDashboardResponse({
        refreshSeedDocument: true,
        seedIfEmpty: true,
      });

      expect(response.state).toBe("ready");
      if (response.state !== "ready") throw new Error("expected ready dashboard");
      expect(response.liveDatasourceHydration).toEqual({ enabled: false });
    } finally {
      if (previous === undefined) {
        delete process.env.DISABLE_LIVE_DATASOURCES;
      } else {
        process.env.DISABLE_LIVE_DATASOURCES = previous;
      }
    }
  });

  test("hydrates a telemetry tile independently from the dashboard shell", async () => {
    const fetch = vi.fn(async (input: RequestInfo | URL) => {
      const url = String(input);

      if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
        return jsonResponse({
          status: "success",
          data: {
            result: [
              {
                metric: { host: "linux-infra" },
                values: [
                  [1771430000, "10"],
                  [1771430060, "20"],
                  [1771430120, "42"],
                ],
              },
            ],
          },
        });
      }

      if (url.startsWith("https://prometheus.example/api/v1/query")) {
        return jsonResponse({
          status: "success",
          data: {
            result: [
              {
                metric: { host: "linux-infra" },
                value: [1771430400, "42"],
              },
            ],
          },
        });
      }

      throw new Error(`Unhandled test request: ${url}`);
    });

    const response = await loadDashboardTileResponse(
      { kind: "telemetry", id: "infra-ram" },
      {
        fetch,
        prometheusBaseUrl: "https://prometheus.example",
        refreshSeedDocument: true,
        seedIfEmpty: true,
      },
    );

    expect(response.state).toBe("ready");
    if (response.state !== "ready") throw new Error("expected ready tile");
    expect(response.tile).toEqual({ kind: "telemetry", id: "infra-ram" });
    expect(response.item).toMatchObject({
      id: "infra-ram",
      value: { kind: "percent", value: 42 },
      severity: "ok",
      detail: "linux-infra",
      sparkline: [10, 20, 42],
    });
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining("/api/v1/query?"),
      expect.objectContaining({ cache: "no-store" }),
    );
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining("/api/v1/query_range?"),
      expect.objectContaining({ cache: "no-store" }),
    );
  });

  test("hydrates a service tile with its service group identity", async () => {
    const fetch = vi.fn(async () =>
      jsonResponse({
        status: "UP",
        ping: 42,
      }),
    );

    const response = await loadDashboardTileResponse(
      { kind: "service", groupId: "essentials", id: "vaultwarden" },
      {
        fetch,
        refreshSeedDocument: true,
        seedIfEmpty: true,
      },
    );

    expect(response.state).toBe("ready");
    if (response.state !== "ready") throw new Error("expected ready tile");
    expect(response.tile).toEqual({
      kind: "service",
      groupId: "essentials",
      id: "vaultwarden",
    });
    expect(response.item).toMatchObject({
      id: "vaultwarden",
      severity: "ok",
      detail: "42 ms",
    });
  });

  test("caches ready tile responses until the tile ttl expires", async () => {
    const cache = createDashboardTileCache();
    let now = 1_000;
    const fetch = vi.fn(async () => {
      now += 7;
      return jsonResponse({
        status: "UP",
        ping: 42,
      });
    });
    const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;

    const first = await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    const second = await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now + 29_999,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    now += 30_001;
    const third = await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });

    expect(first).toEqual(second);
    expect(third.state).toBe("ready");
    expect(fetch).toHaveBeenCalledTimes(2);
  });

  test("logs tile duration and cache hit or miss metadata", async () => {
    let now = 1_000;
    const logs: DashboardTileResolutionLogEvent[] = [];
    const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
    const service = dimensionLabDashboardFixture.serviceGroups
      .flatMap((group) => group.services)
      .find((item) => item.id === tile.id);
    if (!service) throw new Error("missing service fixture");

    let cacheCalls = 0;
    const tileCache = {
      async resolve() {
        cacheCalls += 1;
        if (cacheCalls === 1) now += 7;
        const cacheState = cacheCalls === 1 ? "miss" as const : "hit" as const;

        return {
          cache: cacheState,
          coalesced: false,
          response: {
            state: "ready" as const,
            tile,
            item: service,
          },
        };
      },
    };

    await loadDashboardTileResponse(tile, {
      logTileResolution: (event) => logs.push(event),
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache,
    });
    now += 10;
    await loadDashboardTileResponse(tile, {
      logTileResolution: (event) => logs.push(event),
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache,
    });

    expect(logs).toEqual([
      expect.objectContaining({
        cache: "miss",
        coalesced: false,
        durationMs: 7,
        status: "ready",
        tileKey: dashboardTileCacheKey(tile),
      }),
      expect.objectContaining({
        cache: "hit",
        coalesced: false,
        durationMs: 0,
        status: "ready",
        tileKey: dashboardTileCacheKey(tile),
      }),
    ]);
  });

  test("keeps telemetry tiles cached for fifteen seconds", async () => {
    const cache = createDashboardTileCache();
    let now = 1_000;
    const fetch = telemetryFetch();
    const tile = { kind: "telemetry", id: "infra-ram" } as const;

    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      prometheusBaseUrl: "https://prometheus.example",
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now + 14_999,
      prometheusBaseUrl: "https://prometheus.example",
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    now += 15_001;
    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      prometheusBaseUrl: "https://prometheus.example",
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });

    expect(fetch).toHaveBeenCalledTimes(4);
  });

  test("keeps weather module tiles cached for ten minutes", async () => {
    const cache = createDashboardTileCache();
    let now = 1_000;
    const fetch = vi.fn(async () =>
      jsonResponse({
        current: {
          apparent_temperature: 19,
          temperature_2m: 20,
          weather_code: 0,
          wind_speed_10m: 11,
        },
      }),
    );
    const tile = { kind: "module", id: "weather-amsterdam" } as const;

    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now + 599_999,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    now += 600_001;
    await loadDashboardTileResponse(tile, {
      fetch,
      now: () => now,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });

    expect(fetch).toHaveBeenCalledTimes(2);
  });

  test("coalesces concurrent tile requests for the same cache key", async () => {
    const cache = createDashboardTileCache();
    const logs: DashboardTileResolutionLogEvent[] = [];
    let resolveFetch: ((response: Response) => void) | undefined;
    const fetch = vi.fn(() =>
      new Promise<Response>((resolve) => {
        resolveFetch = resolve;
      })
    );
    const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;

    const first = loadDashboardTileResponse(tile, {
      fetch,
      logTileResolution: (event) => logs.push(event),
      now: () => 1_000,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });
    const second = loadDashboardTileResponse(tile, {
      fetch,
      logTileResolution: (event) => logs.push(event),
      now: () => 1_000,
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: cache,
    });

    await Promise.resolve();
    expect(fetch).toHaveBeenCalledTimes(1);

    resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
    expect(await first).toEqual(await second);
    expect(logs).toEqual([
      expect.objectContaining({
        cache: "miss",
        coalesced: false,
        status: "ready",
        tileKey: dashboardTileCacheKey(tile),
      }),
      expect.objectContaining({
        cache: "miss",
        coalesced: true,
        status: "ready",
        tileKey: dashboardTileCacheKey(tile),
      }),
    ]);
  });

  test("logs coalesced metadata when shared tile requests fail", async () => {
    const cache = createDashboardTileCache();
    let rejectLoad: ((error: Error) => void) | undefined;
    const load = vi.fn(() =>
      new Promise<never>((_resolve, reject) => {
        rejectLoad = reject;
      })
    );

    const first = cache.resolve("tile-a", 30_000, 1_000, load);
    const second = cache.resolve("tile-a", 30_000, 1_000, load);

    await Promise.resolve();
    expect(load).toHaveBeenCalledTimes(1);

    rejectLoad?.(new TypeError("upstream failed"));
    await expect(first).rejects.toThrow("upstream failed");
    await expect(second).rejects.toMatchObject({
      cache: "miss",
      coalesced: true,
      cause: expect.any(TypeError),
    });
  });

  test("logs coalesced metadata for failed tile cache resolution", async () => {
    const logs: DashboardTileResolutionLogEvent[] = [];
    const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;

    await expect(
      loadDashboardTileResponse(tile, {
        logTileResolution: (event) => logs.push(event),
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: {
          async resolve() {
            throw {
              cache: "miss",
              cause: new TypeError("coalesced cache failed"),
              coalesced: true,
            };
          },
        },
      }),
    ).rejects.toThrow("coalesced cache failed");

    expect(logs).toEqual([
      expect.objectContaining({
        cache: "miss",
        coalesced: true,
        errorCategory: "TypeError",
        status: "error",
        tileKey: dashboardTileCacheKey(tile),
      }),
    ]);
  });

  test("logs error categories for failed tile cache resolution", async () => {
    const logs: DashboardTileResolutionLogEvent[] = [];
    const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;

    await expect(
      loadDashboardTileResponse(tile, {
        logTileResolution: (event) => logs.push(event),
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: {
          async resolve() {
            throw new TypeError("cache failed");
          },
        },
      }),
    ).rejects.toThrow("cache failed");

    expect(logs).toEqual([
      expect.objectContaining({
        cache: "miss",
        coalesced: false,
        errorCategory: "TypeError",
        status: "error",
        tileKey: dashboardTileCacheKey(tile),
      }),
    ]);
  });

  test("uses structured tile cache keys when identifiers contain delimiters", () => {
    expect(
      dashboardTileCacheKey({ kind: "service", groupId: "a:b", id: "c" }),
    ).not.toBe(
      dashboardTileCacheKey({ kind: "service", groupId: "a", id: "b:c" }),
    );
    expect(
      dashboardTileCacheKey({ kind: "status", stripId: "a:b", id: "c" }),
    ).not.toBe(
      dashboardTileCacheKey({ kind: "status", stripId: "a", id: "b:c" }),
    );
  });

  test("uses thirty-second status aggregate and five-minute static status ttl buckets", async () => {
    const cache = createDashboardTileCache();
    const fetch = vi.fn(async () => jsonResponse({ status: "UP", ping: 42 }));
    const serviceCheckCount = dimensionLabDashboardFixture.serviceGroups
      .flatMap((group) => group.services).length;

    await loadDashboardTileResponse(
      { kind: "status", stripId: "footer-status", id: "system-status" },
      {
        fetch,
        now: () => 1_000,
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: cache,
      },
    );
    await loadDashboardTileResponse(
      { kind: "status", stripId: "footer-status", id: "system-status" },
      {
        fetch,
        now: () => 31_001,
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: cache,
      },
    );

    await loadDashboardTileResponse(
      { kind: "status", stripId: "footer-status", id: "auto-refresh" },
      {
        fetch,
        now: () => 1_000,
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: cache,
      },
    );
    await loadDashboardTileResponse(
      { kind: "status", stripId: "footer-status", id: "auto-refresh" },
      {
        fetch,
        now: () => 300_999,
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: cache,
      },
    );
    await loadDashboardTileResponse(
      { kind: "status", stripId: "footer-status", id: "auto-refresh" },
      {
        fetch,
        now: () => 301_001,
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: cache,
      },
    );

    expect(fetch).toHaveBeenCalledTimes(serviceCheckCount * 2);
  });

  test("serves tile route responses with short private cache headers", async () => {
    const response = await handleDashboardTileRoute(
      "/api/dashboard/tile/service/essentials/vaultwarden",
      {
        fetch: vi.fn(async () => jsonResponse({ status: "UP", ping: 42 })),
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: createDashboardTileCache(),
      },
    );

    expect(response.headers.get("cache-control")).toBe(
      "private, max-age=5, stale-while-revalidate=30",
    );
  });

  test("serves batch tile route responses", async () => {
    const response = await handleDashboardTilesRoute(
      new Request("https://example.test/api/dashboard/tiles", {
        method: "POST",
        body: JSON.stringify({
          tiles: [
            {
              kind: "status",
              stripId: "footer-status",
              id: "auto-refresh",
            },
          ],
        }),
      }),
      {
        refreshSeedDocument: true,
        seedIfEmpty: true,
        tileCache: createDashboardTileCache(),
      },
    );

    expect(response.status).toBe(200);
    expect(response.headers.get("cache-control")).toBe(
      "private, max-age=5, stale-while-revalidate=30",
    );
    await expect(response.json()).resolves.toMatchObject({
      state: "ready",
      tiles: [
        {
          state: "ready",
          tile: {
            kind: "status",
            stripId: "footer-status",
            id: "auto-refresh",
          },
        },
      ],
    });
  });

  test("rejects invalid batch tile requests", async () => {
    const response = await handleDashboardTilesRoute(
      new Request("https://example.test/api/dashboard/tiles", {
        method: "POST",
        body: JSON.stringify({
          tiles: [{ kind: "service", id: "missing-group" }],
        }),
      }),
    );

    expect(response.status).toBe(400);
  });

  test("resolves batch tile responses with server concurrency capped at six", async () => {
    let active = 0;
    let maxActive = 0;
    const started: string[] = [];
    const releases = new Map<string, () => void>();
    const service = dimensionLabDashboardFixture.serviceGroups
      .flatMap((group) => group.services)[0];
    if (!service) throw new Error("missing service fixture");
    const tiles = Array.from({ length: 7 }, (_, index) => ({
      kind: "service" as const,
      groupId: "essentials",
      id: `service-${index}`,
    }));

    const batch = loadDashboardTilesResponse(tiles, {
      refreshSeedDocument: true,
      seedIfEmpty: true,
      tileCache: {
        async resolve(key) {
          active += 1;
          maxActive = Math.max(maxActive, active);
          started.push(key);
          await new Promise<void>((resolve) => releases.set(key, resolve));
          active -= 1;

          return {
            cache: "miss",
            coalesced: false,
            response: {
              state: "ready",
              tile: JSON.parse(key),
              item: service,
            },
          };
        },
      },
    });

    await waitFor(() => started.length === 6);
    expect(maxActive).toBe(6);
    releases.get(started[0])?.();
    await waitFor(() => started.length === 7);
    for (const release of releases.values()) release();

    await expect(batch).resolves.toMatchObject({
      state: "ready",
      tiles: expect.arrayContaining([
        expect.objectContaining({
          state: "ready",
          tile: tiles[0],
        }),
      ]),
    });
    expect(maxActive).toBe(6);
  });

  test("does not hydrate tile routes when live datasources are disabled", async () => {
    const previous = process.env.DISABLE_LIVE_DATASOURCES;
    process.env.DISABLE_LIVE_DATASOURCES = "1";
    const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
      new Error("live datasource fetch should not run when disabled"),
    );

    try {
      const response = await loadDashboardTileResponse(
        { kind: "telemetry", id: "infra-ram" },
        {
          refreshSeedDocument: true,
          seedIfEmpty: true,
        },
      );

      expect(response.state).toBe("disabled");
      expect(fetch).not.toHaveBeenCalled();
    } finally {
      fetch.mockRestore();
      if (previous === undefined) {
        delete process.env.DISABLE_LIVE_DATASOURCES;
      } else {
        process.env.DISABLE_LIVE_DATASOURCES = previous;
      }
    }
  });
});

function jsonResponse(payload: unknown): Response {
  return new Response(JSON.stringify(payload), {
    headers: { "content-type": "application/json" },
  });
}

async function waitFor(predicate: () => boolean) {
  for (let attempt = 0; attempt < 20; attempt += 1) {
    if (predicate()) return;
    await Promise.resolve();
  }

  throw new Error("condition was not met");
}

function telemetryFetch() {
  return vi.fn(async (input: RequestInfo | URL) => {
    const url = String(input);

    if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
      return jsonResponse({
        status: "success",
        data: {
          result: [
            {
              metric: { host: "linux-infra" },
              values: [
                [1771430000, "10"],
                [1771430060, "20"],
                [1771430120, "42"],
              ],
            },
          ],
        },
      });
    }

    if (url.startsWith("https://prometheus.example/api/v1/query")) {
      return jsonResponse({
        status: "success",
        data: {
          result: [
            {
              metric: { host: "linux-infra" },
              value: [1771430400, "42"],
            },
          ],
        },
      });
    }

    throw new Error(`Unhandled test request: ${url}`);
  });
}
