// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import { useGesynchroniseerdeOpslag } from "./useGesynchroniseerdeOpslag";

interface Item {
  id: string;
  naam: string;
}

function maakLokaal(initieel: Item[] = []) {
  let opgeslagen = [...initieel];
  const lezen = vi.fn(async () => [...opgeslagen]);
  const schrijven = vi.fn(async (item: Item) => {
    const idx = opgeslagen.findIndex((i) => i.id === item.id);
    if (idx >= 0) opgeslagen[idx] = item;
    else opgeslagen.push(item);
  });
  const verwijderen = vi.fn(async (id: string) => {
    opgeslagen = opgeslagen.filter((i) => i.id !== id);
  });
  return { lezen, schrijven, verwijderen };
}

describe("useGesynchroniseerdeOpslag", () => {
  const origFetch = global.fetch;

  beforeEach(() => {
    global.fetch = vi.fn();
  });

  afterEach(() => {
    global.fetch = origFetch;
    vi.restoreAllMocks();
  });

  it("toont lokale data onmiddellijk en markeert geladen na de sync", async () => {
    const lokaal = maakLokaal([{ id: "1", naam: "a" }]);
    vi.mocked(global.fetch).mockResolvedValue(
      new Response(JSON.stringify({ items: [], persistent: false }), { status: 200 })
    );

    const { result } = renderHook(() =>
      useGesynchroniseerdeOpslag<Item>("/api/sync/x", lokaal.lezen, lokaal.schrijven, lokaal.verwijderen)
    );

    await waitFor(() => expect(result.current.geladen).toBe(true));
    expect(result.current.items).toEqual([{ id: "1", naam: "a" }]);
  });

  it("schrijft serveritems lokaal en pusht lokale items die de server niet kent", async () => {
    const lokaal = maakLokaal([{ id: "lokaal-only", naam: "lokaal" }]);
    vi.mocked(global.fetch).mockImplementation(async (url, init) => {
      if (!init || init.method === undefined) {
        return new Response(
          JSON.stringify({ items: [{ id: "server-1", naam: "server" }], persistent: true }),
          { status: 200 }
        );
      }
      return new Response(JSON.stringify({ opgeslagen: "ok" }), { status: 200 });
    });

    const { result } = renderHook(() =>
      useGesynchroniseerdeOpslag<Item>("/api/sync/x", lokaal.lezen, lokaal.schrijven, lokaal.verwijderen)
    );

    await waitFor(() => expect(result.current.geladen).toBe(true));

    expect(lokaal.schrijven).toHaveBeenCalledWith({ id: "server-1", naam: "server" });
    await waitFor(() => {
      const postCalls = vi.mocked(global.fetch).mock.calls.filter(
        ([, init]) => init?.method === "POST"
      );
      expect(postCalls.some(([, init]) => JSON.parse(init!.body as string).id === "lokaal-only")).toBe(true);
    });

    const ids = result.current.items.map((i) => i.id).sort();
    expect(ids).toEqual(["lokaal-only", "server-1"]);
  });

  it("valt terug op lokale data als de fetch faalt (geen netwerk/Supabase)", async () => {
    const lokaal = maakLokaal([{ id: "1", naam: "a" }]);
    vi.mocked(global.fetch).mockRejectedValue(new Error("netwerkfout"));

    const { result } = renderHook(() =>
      useGesynchroniseerdeOpslag<Item>("/api/sync/x", lokaal.lezen, lokaal.schrijven, lokaal.verwijderen)
    );

    await waitFor(() => expect(result.current.geladen).toBe(true));
    expect(result.current.items).toEqual([{ id: "1", naam: "a" }]);
  });

  it("slaOp schrijft optimistisch lokaal/in state en pusht naar de server", async () => {
    const lokaal = maakLokaal([]);
    vi.mocked(global.fetch).mockResolvedValue(
      new Response(JSON.stringify({ items: [], persistent: false }), { status: 200 })
    );

    const { result } = renderHook(() =>
      useGesynchroniseerdeOpslag<Item>("/api/sync/x", lokaal.lezen, lokaal.schrijven, lokaal.verwijderen)
    );
    await waitFor(() => expect(result.current.geladen).toBe(true));

    await act(async () => {
      await result.current.slaOp({ id: "1", naam: "nieuw" });
    });

    expect(lokaal.schrijven).toHaveBeenCalledWith({ id: "1", naam: "nieuw" });
    expect(result.current.items).toEqual([{ id: "1", naam: "nieuw" }]);
    const postCall = vi
      .mocked(global.fetch)
      .mock.calls.find(([, init]) => init?.method === "POST");
    expect(postCall).toBeDefined();
    expect(JSON.parse(postCall![1]!.body as string)).toEqual({ id: "1", naam: "nieuw" });
  });

  it("verwijder verwijdert lokaal/uit state en stuurt een DELETE met het id", async () => {
    const lokaal = maakLokaal([{ id: "1", naam: "a" }]);
    vi.mocked(global.fetch).mockResolvedValue(
      new Response(JSON.stringify({ items: [], persistent: false }), { status: 200 })
    );

    const { result } = renderHook(() =>
      useGesynchroniseerdeOpslag<Item>("/api/sync/x", lokaal.lezen, lokaal.schrijven, lokaal.verwijderen)
    );
    await waitFor(() => expect(result.current.geladen).toBe(true));

    await act(async () => {
      await result.current.verwijder("1");
    });

    expect(lokaal.verwijderen).toHaveBeenCalledWith("1");
    expect(result.current.items).toEqual([]);
    const deleteCall = vi
      .mocked(global.fetch)
      .mock.calls.find(([, init]) => init?.method === "DELETE");
    expect(deleteCall).toBeDefined();
    expect((deleteCall![0] as string)).toContain("id=1");
  });
});
