// @vitest-environment jsdom

import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { ColorContext } from "utils/contexts/color";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
import { ThemeContext } from "utils/contexts/theme";

const {
  state,
  router,
  i18n,
  getSettings,
  servicesResponse,
  bookmarksResponse,
  widgetsResponse,
  serverSideTranslations,
  logger,
  useSWR,
  useWindowFocus,
} = vi.hoisted(() => {
  const state = {
    throwIn: null,
    validateData: [],
    hashData: null,
    hashConfig: null,
    mutateHash: vi.fn(),
    servicesData: [],
    bookmarksData: [],
    widgetsData: [],
    quickLaunchProps: null,
    widgetCalls: [],
    windowFocused: false,
  };

  const router = { asPath: "/" };
  const i18n = { language: "en", changeLanguage: vi.fn() };

  const getSettings = vi.fn(() => ({
    providers: {},
    language: "en",
    title: "Homepage",
  }));

  const servicesResponse = vi.fn(async () => {
    if (state.throwIn === "services") throw new Error("services failed");
    return [{ name: "svc" }];
  });
  const bookmarksResponse = vi.fn(async () => {
    if (state.throwIn === "bookmarks") throw new Error("bookmarks failed");
    return [{ name: "bm" }];
  });
  const widgetsResponse = vi.fn(async () => {
    if (state.throwIn === "widgets") throw new Error("widgets failed");
    return [{ type: "search" }];
  });

  const serverSideTranslations = vi.fn(async (language) => ({ _translations: language }));
  const logger = { error: vi.fn() };

  const useSWR = vi.fn((key, config) => {
    if (key === "/api/validate") return { data: state.validateData };
    if (key === "/api/hash") {
      state.hashConfig = config;
      return { data: state.hashData, mutate: state.mutateHash };
    }
    if (key === "/api/services") return { data: state.servicesData };
    if (key === "/api/bookmarks") return { data: state.bookmarksData };
    if (key === "/api/widgets") return { data: state.widgetsData };
    return { data: undefined };
  });

  const useWindowFocus = vi.fn(() => state.windowFocused);

  return {
    state,
    router,
    i18n,
    getSettings,
    servicesResponse,
    bookmarksResponse,
    widgetsResponse,
    serverSideTranslations,
    logger,
    useSWR,
    useWindowFocus,
  };
});

vi.mock("next/dynamic", () => ({
  default: () => () => null,
}));
vi.mock("next/head", () => ({ default: ({ children }) => children }));
vi.mock("next/script", () => ({ default: () => null }));
vi.mock("next/router", () => ({ useRouter: () => router }));

vi.mock("next-i18next/pages", () => ({
  useTranslation: () => ({
    i18n,
    t: (k) => k,
  }),
}));

vi.mock("next-i18next/pages/serverSideTranslations", () => ({
  serverSideTranslations,
}));

vi.mock("swr", () => ({
  default: useSWR,
  SWRConfig: ({ children }) => children,
}));

vi.mock("utils/logger", () => ({
  default: () => logger,
}));

vi.mock("utils/config/config", () => ({
  getSettings,
}));

vi.mock("utils/config/api-response", () => ({
  servicesResponse,
  bookmarksResponse,
  widgetsResponse,
}));

vi.mock("utils/hooks/window-focus", () => ({
  default: useWindowFocus,
}));

vi.mock("components/bookmarks/group", () => ({
  default: ({ bookmarks }) => <div data-testid="bookmarks-group">{bookmarks?.name}</div>,
}));

vi.mock("components/services/group", () => ({
  default: ({ group }) => <div data-testid="services-group">{group?.name}</div>,
}));

vi.mock("components/errorboundry", () => ({
  default: ({ children }) => <>{children}</>,
}));

vi.mock("components/tab", () => ({
  default: ({ tab }) => <li data-testid="tab">{tab}</li>,
  slugifyAndEncode: (tabName) =>
    tabName !== undefined ? encodeURIComponent(tabName.toString().replace(/\\s+/g, "-").toLowerCase()) : "",
}));

vi.mock("components/quicklaunch", () => ({
  default: (props) => {
    state.quickLaunchProps = props;
    return (
      <div data-testid="quicklaunch">
        {props.isOpen ? "open" : "closed"}:{props.servicesAndBookmarks?.length ?? 0}
      </div>
    );
  },
}));

vi.mock("components/widgets/widget", () => ({
  default: ({ widget, style }) => {
    state.widgetCalls.push({ widget, style });
    return <div data-testid="widget">{widget?.type}</div>;
  },
}));

vi.mock("components/toggles/revalidate", () => ({
  default: () => null,
}));

describe("pages/index getStaticProps", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    state.throwIn = null;
    state.validateData = [];
    state.hashData = null;
    state.hashConfig = null;
    state.servicesData = [];
    state.bookmarksData = [];
    state.widgetsData = [];
    state.quickLaunchProps = null;
    state.widgetCalls = [];
    state.windowFocused = false;
    router.asPath = "/";
    i18n.changeLanguage.mockClear();
  });

  it("returns initial settings and api fallbacks for swr", async () => {
    getSettings.mockReturnValueOnce({ providers: { x: 1 }, language: "en", title: "Homepage" });

    const { getStaticProps } = await import("pages/index.jsx");
    const result = await getStaticProps();

    expect(result.props.initialSettings).toEqual({ language: "en", title: "Homepage" });
    expect(result.props.fallback["/api/services"]).toEqual([{ name: "svc" }]);
    expect(result.props.fallback["/api/bookmarks"]).toEqual([{ name: "bm" }]);
    expect(result.props.fallback["/api/widgets"]).toEqual([{ type: "search" }]);
    expect(result.props.fallback["/api/hash"]).toBe(false);
    expect(serverSideTranslations).toHaveBeenCalledWith("en");
  });

  it("normalizes legacy language codes before requesting translations", async () => {
    getSettings.mockReturnValueOnce({ providers: {}, language: "zh-CN" });

    const { getStaticProps } = await import("pages/index.jsx");
    await getStaticProps();

    expect(serverSideTranslations).toHaveBeenCalledWith("zh-Hans");
  });

  it("falls back to empty settings and en translations on errors", async () => {
    getSettings.mockReturnValueOnce({ providers: {}, language: "de" });
    state.throwIn = "services";

    const { getStaticProps } = await import("pages/index.jsx");
    const result = await getStaticProps();

    expect(result.props.initialSettings).toEqual({});
    expect(result.props.fallback["/api/services"]).toEqual([]);
    expect(result.props.fallback["/api/bookmarks"]).toEqual([]);
    expect(result.props.fallback["/api/widgets"]).toEqual([]);
    expect(serverSideTranslations).toHaveBeenCalledWith("en");
    expect(logger.error).toHaveBeenCalled();
  });
});

async function renderIndex({
  initialSettings = { title: "Homepage", layout: {} },
  fallback = {},
  theme = "dark",
  color = "slate",
  activeTab = "",
  settings = initialSettings,
} = {}) {
  const { default: Wrapper } = await import("pages/index.jsx");

  const setTheme = vi.fn();
  const setColor = vi.fn();
  const setSettings = vi.fn();
  const setActiveTab = vi.fn();

  const renderResult = render(
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <ColorContext.Provider value={{ color, setColor }}>
        <SettingsContext.Provider value={{ settings, setSettings }}>
          <TabContext.Provider value={{ activeTab, setActiveTab }}>
            <Wrapper initialSettings={initialSettings} fallback={fallback} />
          </TabContext.Provider>
        </SettingsContext.Provider>
      </ColorContext.Provider>
    </ThemeContext.Provider>,
  );

  return { ...renderResult, setTheme, setColor, setSettings, setActiveTab };
}

describe("pages/index Wrapper", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    state.validateData = [];
    state.hashData = null;
    state.servicesData = [];
    state.bookmarksData = [];
    state.widgetsData = [];
    state.widgetCalls = [];
    document.documentElement.className = "dark theme-slate";
  });

  it("applies theme/color classes and renders a background overlay when configured", async () => {
    await renderIndex({
      initialSettings: {
        title: "Homepage",
        color: "slate",
        background: { image: "https://example.com/bg.jpg", opacity: 10, blur: true, saturate: 150, brightness: 125 },
        layout: {},
      },
      theme: "dark",
      color: "emerald",
    });

    await waitFor(() => {
      expect(document.documentElement.classList.contains("scheme-dark")).toBe(true);
    });
    expect(document.documentElement.classList.contains("dark")).toBe(true);
    expect(document.documentElement.classList.contains("theme-emerald")).toBe(true);
    expect(document.documentElement.classList.contains("theme-slate")).toBe(false);

    expect(document.querySelector("#background")).toBeTruthy();
    expect(document.querySelector("#inner_wrapper")?.className).toContain("backdrop-blur");
    expect(document.querySelector("#inner_wrapper")?.className).toContain("backdrop-saturate-150");
    expect(document.querySelector("#inner_wrapper")?.className).toContain("backdrop-brightness-125");
  });

  it("supports legacy string backgrounds in settings", async () => {
    await renderIndex({
      initialSettings: {
        title: "Homepage",
        color: "slate",
        background: "https://example.com/bg.jpg",
        layout: {},
      },
      theme: "dark",
      color: "emerald",
    });

    expect(document.querySelector("#background")).toBeTruthy();
  });
});

describe("pages/index Index routing + SWR branches", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    state.hashData = null;
    state.mutateHash.mockClear();
    state.servicesData = [];
    state.bookmarksData = [];
    state.widgetsData = [];
  });

  it("renders the validation error screen when /api/validate returns an error", async () => {
    state.validateData = { error: "bad config" };

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });

    expect(screen.getByText("Error")).toBeInTheDocument();
    expect(screen.getByText("bad config")).toBeInTheDocument();
  });

  it("renders config errors when /api/validate returns a list of errors", async () => {
    state.validateData = [{ config: "services.yaml", name: "Service 1", reason: "broken", mark: { line: 4 } }];

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });

    expect(screen.getByText(/services.yaml/)).toBeInTheDocument();
    expect(screen.getByText(/line 4/)).toBeInTheDocument();
  });

  it("marks the UI stale when the hash changes and triggers a revalidate reload", async () => {
    state.validateData = [];
    state.hashData = { hash: "new-hash" };
    localStorage.setItem("hash", "old-hash");

    const fetchSpy = vi.fn(async () => ({ ok: true }));

    fetch = fetchSpy;

    let reloadSpy;
    try {
      reloadSpy = vi.spyOn(window.location, "reload").mockImplementation(() => {});
    } catch {
      // jsdom can make window.location non-configurable in some contexts.
      Object.defineProperty(window, "location", { value: { reload: vi.fn() }, writable: true });
      reloadSpy = vi.spyOn(window.location, "reload").mockImplementation(() => {});
    }

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
    act(() => state.hashConfig.onSuccess(state.hashData));

    await waitFor(() => {
      expect(fetchSpy).toHaveBeenCalledWith("/api/revalidate");
    });
    await waitFor(() => {
      expect(reloadSpy).toHaveBeenCalled();
    });
    expect(document.querySelector(".animate-spin")).toBeTruthy();
  });

  it("mutates the hash when the window regains focus", async () => {
    state.validateData = [];
    state.hashData = { hash: "h" };
    state.windowFocused = true;

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });

    await waitFor(() => {
      expect(state.mutateHash).toHaveBeenCalled();
    });
  });

  it("stores the initial hash in localStorage when none exists", async () => {
    state.validateData = [];
    state.hashData = { hash: "first-hash" };
    localStorage.removeItem("hash");

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
    act(() => state.hashConfig.onSuccess(state.hashData));

    await waitFor(() => {
      expect(localStorage.getItem("hash")).toBe("first-hash");
    });
  });

  it("ignores a hash response without a hash", async () => {
    state.validateData = [];
    localStorage.setItem("hash", "old-hash");

    await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });

    expect(() => act(() => state.hashConfig.onSuccess(null))).not.toThrow();
    expect(() => act(() => state.hashConfig.onSuccess({}))).not.toThrow();
    expect(localStorage.getItem("hash")).toBe("old-hash");
    expect(document.querySelector(".animate-spin")).toBeFalsy();
  });
});

describe("pages/index Home behavior", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    state.validateData = [];
    state.hashData = null;
    state.servicesData = [
      {
        name: "Services",
        services: [{ name: "s1", href: "http://svc/1" }, { name: "s2" }],
        groups: [{ name: "Nested", services: [{ name: "s3", href: "http://svc/3" }], groups: [] }],
      },
    ];
    state.bookmarksData = [{ name: "Bookmarks", bookmarks: [{ name: "b1", href: "http://bm/1" }, { name: "b2" }] }];
    state.widgetsData = [{ type: "glances" }, { type: "search" }];
    state.quickLaunchProps = null;
    state.widgetCalls = [];
  });

  it("passes href-bearing services and bookmarks to QuickLaunch and toggles search on keydown", async () => {
    await renderIndex({
      initialSettings: { title: "Homepage", layout: {} },
      settings: { title: "Homepage", layout: {}, language: "en" },
    });

    await waitFor(() => {
      expect(state.quickLaunchProps).toBeTruthy();
    });

    expect(state.quickLaunchProps.servicesAndBookmarks.map((i) => i.name)).toEqual(["b1", "s1", "s3"]);
    expect(screen.getByTestId("quicklaunch")).toHaveTextContent("closed:3");

    fireEvent.keyDown(document.body, { key: "a" });
    expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
    expect(state.quickLaunchProps.searchString).toBe("a");

    fireEvent.keyDown(document.body, { key: "Escape" });
    expect(screen.getByTestId("quicklaunch")).toHaveTextContent("closed:3");
  });

  it("opens search on space without seeding the query with whitespace", async () => {
    await renderIndex({
      initialSettings: { title: "Homepage", layout: {} },
      settings: { title: "Homepage", layout: {}, language: "en" },
    });

    await waitFor(() => {
      expect(state.quickLaunchProps).toBeTruthy();
    });

    fireEvent.keyDown(document.body, { key: " " });
    expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
    expect(state.quickLaunchProps.searchString).toBe("");
  });

  it("renders services and bookmark groups when present", async () => {
    await renderIndex({
      initialSettings: { title: "Homepage", layout: {} },
      settings: { title: "Homepage", layout: {}, language: "en" },
    });

    expect(await screen.findByTestId("services-group")).toHaveTextContent("Services");
    expect(screen.getByTestId("bookmarks-group")).toHaveTextContent("Bookmarks");
  });

  it("renders tab navigation and filters groups by active tab", async () => {
    state.servicesData = [{ name: "Services", services: [], groups: [] }];
    state.bookmarksData = [{ name: "Bookmarks", bookmarks: [] }];

    await renderIndex({
      initialSettings: { title: "Homepage", layout: { Services: { tab: "Main" }, Bookmarks: { tab: "Main" } } },
      settings: { title: "Homepage", layout: { Services: { tab: "Main" }, Bookmarks: { tab: "Main" } } },
      activeTab: "main",
    });

    expect(await screen.findAllByTestId("tab")).toHaveLength(1);
    expect(screen.getAllByTestId("services-group")[0]).toHaveTextContent("Services");
    expect(screen.getAllByTestId("bookmarks-group")[0]).toHaveTextContent("Bookmarks");
  });

  it("waits for settings.layout to populate when it differs from initial settings", async () => {
    state.servicesData = [{ name: "Services", services: [], groups: [] }];
    state.bookmarksData = [{ name: "Bookmarks", bookmarks: [] }];

    await renderIndex({
      initialSettings: { title: "Homepage", layout: {} },
      // Missing layout triggers the temporary `<div />` return to avoid eager widget fetches.
      settings: { title: "Homepage" },
    });

    expect(screen.queryByTestId("services-group")).toBeNull();
    expect(screen.queryByTestId("bookmarks-group")).toBeNull();
  });

  it("applies cardBlur classes for tabs and boxed headers when configured", async () => {
    state.servicesData = [{ name: "Services", services: [], groups: [] }];
    state.bookmarksData = [{ name: "Bookmarks", bookmarks: [] }];
    state.widgetsData = [{ type: "search" }];

    await renderIndex({
      initialSettings: { title: "Homepage", layout: { Services: { tab: "Main" }, Bookmarks: { tab: "Main" } } },
      settings: {
        title: "Homepage",
        layout: { Services: { tab: "Main" }, Bookmarks: { tab: "Main" } },
        headerStyle: "boxed",
        cardBlur: "sm",
      },
      activeTab: "main",
    });

    expect(document.querySelector("#myTab")?.className).toContain("backdrop-blur-sm");
    expect(document.querySelector("#information-widgets")?.className).toContain("backdrop-blur-sm");
  });

  it("applies settings-driven language/theme/color updates and renders head tags", async () => {
    state.servicesData = [];
    state.bookmarksData = [];
    state.widgetsData = [];

    const { setTheme, setColor, setSettings } = await renderIndex({
      initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
      settings: {
        title: "Homepage",
        layout: {},
        language: "en",
        theme: "light",
        color: "emerald",
        disableIndexing: true,
        base: "/base/",
      },
      theme: "dark",
      color: "slate",
    });

    await waitFor(() => {
      expect(setSettings).toHaveBeenCalled();
    });
    expect(i18n.changeLanguage).toHaveBeenCalledWith("en");
    expect(setTheme).toHaveBeenCalledWith("light");
    expect(setColor).toHaveBeenCalledWith("emerald");

    expect(document.querySelector('meta[name="robots"][content="noindex, nofollow"]')).toBeTruthy();
    expect(document.querySelector("base")?.getAttribute("href")).toBe("/base/");
    expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
  });

  // Safari reads the head without running JS, so the favicon has to be there before the
  // settings context is populated, and mask-icon must not be
  it("renders a custom favicon before the settings context is populated", async () => {
    await renderIndex({
      initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
      settings: {},
    });

    expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
    expect(document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute("href")).toBe("/x.ico");
    expect(document.querySelector('link[rel="mask-icon"]')).toBeNull();
    expect(document.querySelector('link[rel="shortcut icon"]')).toBeNull();
  });

  it("marks information widgets as right-aligned for known widget types", async () => {
    await renderIndex({
      initialSettings: { title: "Homepage", layout: {} },
      settings: { title: "Homepage", layout: {}, language: "en" },
    });

    await waitFor(() => {
      expect(state.widgetCalls.length).toBeGreaterThan(0);
    });

    const rightAligned = state.widgetCalls.filter((c) => c.style?.isRightAligned).map((c) => c.widget.type);
    expect(rightAligned).toEqual(["search"]);
  });
});
