// The offline "downloaded only" gate is a *backend* flag, so flipping it only
// changes what the user sees if the listing is re-queried afterwards. Nothing
// re-queried: the toggle greyed cards instantly (a pure frontend derivation in
// MediaCard) while the item list stayed exactly as it was loaded, which is why
// the filter read as "shows everything until I toggle, then greys some of it".
//
// `catalogFilterVersion` is the refetch signal, and it must bump only once the
// backend has actually accepted the new flag — a reload racing the push would
// re-query under the old gate and land back where it started.
//
// TRACES: UR-052 | DR-078, DR-143 | UT-137, UT-138, UT-139
import { describe, it, expect, vi, beforeEach } from "vitest";
import { get } from "svelte/store";

const h = vi.hoisted(() => {
  function shim<T>(initial: T) {
    let value = initial;
    const subs = new Set<(v: T) => void>();
    return {
      set(v: T) {
        value = v;
        subs.forEach((fn) => fn(value));
      },
      subscribe(fn: (v: T) => void) {
        subs.add(fn);
        fn(value);
        return () => subs.delete(fn);
      },
      /**
       * Drop subscribers from previously imported copies of the module.
       * `vi.resetModules()` gives each test a fresh module instance, but this
       * store outlives them all — without this the stale instances keep
       * consuming `mockImplementationOnce` and pushing their own state.
       */
      reset(v: T) {
        subs.clear();
        value = v;
      },
    };
  }
  return {
    isConnectedStore: shim(true),
    // Resolution is deferred so a test can observe the window between "command
    // issued" and "command accepted".
    pending: [] as Array<() => void>,
    setShowServerCatalog: vi.fn(
      () => new Promise<void>((resolve) => h.pending.push(() => resolve()))
    ),
  };
});

vi.mock("$lib/stores/connectivity", () => ({
  isConnected: { subscribe: h.isConnectedStore.subscribe },
}));

vi.mock("$lib/api/bindings", () => ({
  commands: {
    setShowServerCatalog: h.setShowServerCatalog,
    syncFullCatalog: vi.fn(),
    resumeQueuedDownloads: vi.fn(),
    catalogSyncStatus: vi.fn(),
  },
}));

vi.mock("$lib/stores/auth", () => ({
  auth: { getRepository: () => ({ getHandle: () => "handle-1" }) },
}));

/** Let every issued setShowServerCatalog settle. */
async function settle() {
  h.pending.splice(0).forEach((resolve) => resolve());
  await Promise.resolve();
  await Promise.resolve();
}

describe("offline filter refetch signal (DR-143)", () => {
  beforeEach(() => {
    h.setShowServerCatalog.mockReset();
    h.setShowServerCatalog.mockImplementation(
      () => new Promise<void>((resolve) => h.pending.push(() => resolve()))
    );
    h.pending.length = 0;
    h.isConnectedStore.reset(true);
    vi.resetModules();
  });

  it("bumps the version when going offline closes the gate", async () => {
    const mod = await import("./offlineCatalog");
    await settle();
    const before = get(mod.catalogFilterVersion);

    h.isConnectedStore.set(false); // include: true → false
    await settle();

    expect(h.setShowServerCatalog).toHaveBeenLastCalledWith(false);
    expect(get(mod.catalogFilterVersion)).toBeGreaterThan(before);
  });

  it("bumps only after the backend accepts the flag, never before", async () => {
    const mod = await import("./offlineCatalog");
    await settle();
    const before = get(mod.catalogFilterVersion);

    h.isConnectedStore.set(false);
    await Promise.resolve(); // command issued, not yet resolved

    expect(h.setShowServerCatalog).toHaveBeenLastCalledWith(false);
    expect(get(mod.catalogFilterVersion)).toBe(before);

    await settle();
    expect(get(mod.catalogFilterVersion)).toBeGreaterThan(before);
  });

  it("bumps both ways as the offline toggle is flipped", async () => {
    const mod = await import("./offlineCatalog");
    h.isConnectedStore.set(false);
    await settle();
    const offlineGated = get(mod.catalogFilterVersion);

    mod.showServerCatalog.set(true); // reveal the server catalog
    await settle();
    expect(h.setShowServerCatalog).toHaveBeenLastCalledWith(true);
    const revealed = get(mod.catalogFilterVersion);
    expect(revealed).toBeGreaterThan(offlineGated);

    mod.showServerCatalog.set(false); // back to downloaded-only
    await settle();
    expect(h.setShowServerCatalog).toHaveBeenLastCalledWith(false);
    expect(get(mod.catalogFilterVersion)).toBeGreaterThan(revealed);
  });

  it("does not bump when the effective gate is unchanged", async () => {
    const mod = await import("./offlineCatalog");
    await settle();
    const before = get(mod.catalogFilterVersion);

    // Online, the toggle cannot close the gate — browsing reads the same cache.
    mod.showServerCatalog.set(true);
    await settle();

    expect(get(mod.catalogFilterVersion)).toBe(before);
  });

  it("retries the push after a failed one rather than latching the old gate", async () => {
    const mod = await import("./offlineCatalog");
    await settle();
    const before = get(mod.catalogFilterVersion);

    h.setShowServerCatalog.mockImplementationOnce(() => Promise.reject(new Error("ipc down")));
    h.isConnectedStore.set(false);
    await settle();
    expect(get(mod.catalogFilterVersion)).toBe(before); // nothing to reload for

    // The same transition must be attempted again, not skipped as "already sent".
    h.isConnectedStore.set(true);
    await settle();
    h.isConnectedStore.set(false);
    await settle();
    expect(h.setShowServerCatalog).toHaveBeenLastCalledWith(false);
    expect(get(mod.catalogFilterVersion)).toBeGreaterThan(before);
  });
});
