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

const { fs, yaml, config, widgetHelpers, serviceHelpers } = vi.hoisted(() => ({
  fs: {
    readFile: vi.fn(),
  },
  yaml: {
    load: vi.fn(),
  },
  config: {
    CONF_DIR: "/conf",
    getSettings: vi.fn(),
    substituteEnvironmentVars: vi.fn((s) => s),
    default: vi.fn(),
  },
  widgetHelpers: {
    widgetsFromConfig: vi.fn(),
    cleanWidgetGroups: vi.fn(),
  },
  serviceHelpers: {
    servicesFromDocker: vi.fn(),
    servicesFromKubernetes: vi.fn(),
    servicesFromConfig: vi.fn(),
    cleanServiceGroups: vi.fn((g) => g),
    findGroupByName: vi.fn(),
  },
}));

vi.mock("fs", () => ({
  promises: fs,
}));

vi.mock("utils/config/yaml", () => ({ loadYaml: yaml.load }));

vi.mock("utils/config/config", () => config);
vi.mock("utils/config/widget-helpers", () => widgetHelpers);
vi.mock("utils/config/service-helpers", () => serviceHelpers);

import { bookmarksResponse, servicesResponse, widgetsResponse } from "./api-response";

describe("utils/config/api-response", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("bookmarksResponse returns [] when bookmarks are missing", async () => {
    fs.readFile.mockResolvedValueOnce("ignored");
    yaml.load.mockReturnValueOnce(null);

    const res = await bookmarksResponse();
    expect(res).toEqual([]);
    expect(config.getSettings).not.toHaveBeenCalled();
  });

  it("bookmarksResponse falls back when settings cannot be loaded", async () => {
    const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    fs.readFile.mockResolvedValueOnce("ignored");
    config.getSettings.mockRejectedValueOnce(new Error("bad settings"));
    yaml.load.mockReturnValueOnce([{ A: [{ LinkA: [{ href: "a" }] }] }, { B: [{ LinkB: [{ href: "b" }] }] }]);

    const res = await bookmarksResponse();
    expect(res.map((g) => g.name)).toEqual(["A", "B"]);
    expect(errSpy).toHaveBeenCalled();

    errSpy.mockRestore();
  });

  it("bookmarksResponse sorts groups based on settings layout", async () => {
    fs.readFile.mockResolvedValueOnce("ignored");
    config.getSettings.mockResolvedValueOnce({ layout: { B: {}, A: {} } });
    yaml.load.mockReturnValueOnce([{ A: [{ LinkA: [{ href: "a" }] }] }, { B: [{ LinkB: [{ href: "b" }] }] }]);

    const res = await bookmarksResponse();
    expect(res.map((g) => g.name)).toEqual(["B", "A"]);
  });

  it("bookmarksResponse appends groups not present in the layout", async () => {
    fs.readFile.mockResolvedValueOnce("ignored");
    config.getSettings.mockResolvedValueOnce({ layout: { A: {} } });
    yaml.load.mockReturnValueOnce([{ A: [{ LinkA: [{ href: "a" }] }] }, { C: [{ LinkC: [{ href: "c" }] }] }]);

    const res = await bookmarksResponse();
    expect(res.map((g) => g.name)).toEqual(["A", "C"]);
  });

  it("widgetsResponse returns sanitized configured widgets", async () => {
    widgetHelpers.widgetsFromConfig.mockResolvedValueOnce([{ type: "search", options: { url: "x" } }]);
    widgetHelpers.cleanWidgetGroups.mockResolvedValueOnce([{ type: "search", options: { index: 0 } }]);

    expect(await widgetsResponse()).toEqual([{ type: "search", options: { index: 0 } }]);
  });

  it("widgetsResponse returns [] when widgets cannot be loaded", async () => {
    const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    widgetHelpers.widgetsFromConfig.mockRejectedValueOnce(new Error("bad widgets"));

    expect(await widgetsResponse()).toEqual([]);
    expect(errSpy).toHaveBeenCalled();

    errSpy.mockRestore();
  });

  it("servicesResponse merges groups and sorts services by weight then name", async () => {
    // Minimal stubs for findGroupByName used within servicesResponse.
    serviceHelpers.findGroupByName.mockImplementation((groups, name) => groups.find((g) => g.name === name) ?? null);

    serviceHelpers.servicesFromDocker.mockResolvedValueOnce([
      {
        name: "GroupA",
        services: [
          { name: "b", weight: 200 },
          { name: "a", weight: 200 },
        ],
        groups: [],
      },
    ]);
    serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([
      { name: "GroupA", services: [{ name: "c", weight: 100 }], groups: [] },
    ]);
    serviceHelpers.servicesFromConfig.mockResolvedValueOnce([
      { name: "GroupA", services: [{ name: "d", weight: 50 }], groups: [] },
      { name: "Empty", services: [], groups: [] },
    ]);

    config.getSettings.mockResolvedValueOnce({ layout: { GroupA: {}, GroupB: {} } });

    const groups = await servicesResponse();
    expect(groups.map((g) => g.name)).toEqual(["GroupA"]);
    expect(groups[0].services.map((s) => s.name)).toEqual(["d", "c", "a", "b"]);
  });

  it("servicesResponse logs when no docker services are discovered", async () => {
    const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
    serviceHelpers.findGroupByName.mockImplementation((groups, name) => groups.find((g) => g.name === name) ?? null);

    serviceHelpers.servicesFromDocker.mockResolvedValueOnce([]);
    serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([]);
    serviceHelpers.servicesFromConfig.mockResolvedValueOnce([]);
    config.getSettings.mockResolvedValueOnce({});

    const groups = await servicesResponse();

    expect(groups).toEqual([]);
    expect(debugSpy).toHaveBeenCalledWith("No containers were found with homepage labels.");

    debugSpy.mockRestore();
  });

  it("servicesResponse tolerates discovery/load failures and returns [] when nothing can be loaded", async () => {
    const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    serviceHelpers.servicesFromDocker.mockRejectedValueOnce(new Error("docker bad"));
    serviceHelpers.servicesFromKubernetes.mockRejectedValueOnce(new Error("kube bad"));
    serviceHelpers.servicesFromConfig.mockRejectedValueOnce(new Error("config bad"));
    config.getSettings.mockRejectedValueOnce(new Error("settings bad"));

    const groups = await servicesResponse();
    expect(groups).toEqual([]);
    expect(errSpy).toHaveBeenCalled();

    errSpy.mockRestore();
  });

  it("servicesResponse supports multi-level nested layout groups and ensures the top-level parent exists", async () => {
    serviceHelpers.findGroupByName.mockImplementation(function find(groups, name, parent) {
      for (const group of groups ?? []) {
        if (group.name === name) {
          if (parent) group.parent = parent;
          return group;
        }
        const found = find(group.groups, name, group.name);
        if (found) return found;
      }
      return null;
    });

    serviceHelpers.servicesFromDocker.mockResolvedValueOnce([
      { name: "Child", services: [{ name: "svc", weight: 1 }], groups: [] },
    ]);
    serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([]);
    serviceHelpers.servicesFromConfig.mockResolvedValueOnce([{ name: "Root", services: [], groups: [] }]);

    config.getSettings.mockResolvedValueOnce({ layout: { Root: { Top: { Child: {} } } } });

    const groups = await servicesResponse();

    expect(groups.map((g) => g.name)).toEqual(["Root"]);
    expect(groups[0].groups[0].name).toBe("Top");
    expect(groups[0].groups[0].groups[0].name).toBe("Child");
    expect(groups[0].groups[0].groups[0].services).toEqual([{ name: "svc", weight: 1 }]);
  });

  it("servicesResponse merges discovered nested groups into their configured parent layout group", async () => {
    serviceHelpers.findGroupByName.mockImplementation(function find(groups, name, parent) {
      for (const group of groups ?? []) {
        if (group.name === name) {
          if (parent) group.parent = parent;
          return group;
        }
        const found = find(group.groups, name, group.name);
        if (found) return found;
      }
      return null;
    });

    serviceHelpers.servicesFromDocker.mockResolvedValueOnce([
      {
        name: "Child",
        services: [
          { name: "svcB", weight: 50 },
          { name: "svcA", weight: 10 },
        ],
        groups: [],
      },
    ]);
    serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([]);
    serviceHelpers.servicesFromConfig.mockResolvedValueOnce([
      {
        name: "Top",
        services: [],
        groups: [{ name: "Child", services: [], groups: [] }],
      },
    ]);

    config.getSettings.mockResolvedValueOnce({ layout: { Top: { Child: {} } } });

    const groups = await servicesResponse();

    expect(groups.map((g) => g.name)).toEqual(["Top"]);
    expect(groups[0].groups).toHaveLength(1);
    expect(groups[0].groups[0].name).toBe("Child");
    expect(groups[0].groups[0].services.map((s) => s.name)).toEqual(["svcA", "svcB"]);
  });

  it("servicesResponse merges nested discovered groups into their configured parent when no layout is defined", async () => {
    serviceHelpers.findGroupByName.mockImplementation(function find(groups, name, parent) {
      for (const group of groups ?? []) {
        if (group.name === name) {
          if (parent) group.parent = parent;
          return group;
        }
        const found = find(group.groups, name, group.name);
        if (found) return found;
      }
      return null;
    });

    serviceHelpers.servicesFromDocker.mockResolvedValueOnce([
      { name: "Child", services: [{ name: "svc", weight: 1 }], groups: [] },
    ]);
    serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([]);
    serviceHelpers.servicesFromConfig.mockResolvedValueOnce([
      { name: "Top", services: [], groups: [{ name: "Child", services: [], groups: [] }] },
    ]);
    config.getSettings.mockResolvedValueOnce({});

    const groups = await servicesResponse();

    expect(groups.map((g) => g.name)).toEqual(["Top"]);
    expect(groups[0].groups[0].name).toBe("Child");
    expect(groups[0].groups[0].services).toEqual([{ name: "svc", weight: 1 }]);
  });
});
