import { afterEach, describe, expect, test } from "vitest";
import { renderHook, waitFor, within } from "@testing-library/react";
import { QueryClientProvider, useMutation } from "@tanstack/react-query";
import { http, HttpResponse } from "msw";
import type { ReactNode } from "react";

import i18n from "../i18n";
import { ToastRegion } from "../components/ui/toast";
import { server } from "../test/server";
import { makeQueryClient } from "./query-client";
import { HttpError, useDeleteVocabulary, useUpdateTerm } from "./queries";

// The toast manager is a module-scope singleton shared across renders, so each
// test mounts a fresh region and tears it down afterwards to keep toasts from
// one case bleeding into the next.
function makeWrapper() {
  const queryClient = makeQueryClient();

  return function Wrapper({ children }: { children: ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <ToastRegion>{children}</ToastRegion>
      </QueryClientProvider>
    );
  };
}

afterEach(async () => {
  // Allow any pending toast state updates to flush before the next test mounts.
  await Promise.resolve();
});

describe("mutation feedback toasts", () => {
  test("a meta.successMessage mutation shows its success toast on success", async () => {
    server.use(
      http.patch(
        "/api/admin/vocabularies/:id/terms/:term_id",
        () => new HttpResponse(null, { status: 204 }),
      ),
    );

    const { result, unmount } = renderHook(() => useUpdateTerm(), {
      wrapper: makeWrapper(),
    });

    await result.current.mutateAsync({
      vocabularyId: "v1",
      termId: "t1",
      external_uri: null,
      labels: [{ lang: "en", label: "Bronze" }],
    });

    await waitFor(() => {
      expect(
        within(document.body).getByText(i18n.t("toast.saved")),
      ).toBeInTheDocument();
    });

    unmount();
  });

  test("a non-suppressed mutation failing shows the status-mapped error toast", async () => {
    const { result, unmount } = renderHook(
      () =>
        useMutation({
          mutationFn: async () => {
            throw new HttpError(500);
          },
        }),
      { wrapper: makeWrapper() },
    );

    await expect(result.current.mutateAsync()).rejects.toThrow();

    await waitFor(() => {
      expect(
        within(document.body).getByText(i18n.t("errors.server")),
      ).toBeInTheDocument();
    });

    unmount();
  });

  test("a suppressErrorToast mutation failing adds no toast", async () => {
    server.use(
      http.delete(
        "/api/admin/vocabularies/:id",
        () => new HttpResponse(null, { status: 500 }),
      ),
    );

    const { result, unmount } = renderHook(() => useDeleteVocabulary(), {
      wrapper: makeWrapper(),
    });

    await expect(result.current.mutateAsync("v1")).rejects.toThrow();

    // Give the MutationCache onError a turn; assert it stayed silent.
    await Promise.resolve();
    expect(within(document.body).queryByText(i18n.t("toast.error"))).toBeNull();

    unmount();
  });
});
