/**
 * Regression test: media-list search must surface server results.
 *
 * `repository_search` is two-phase — `repo.search()` resolves instantly with
 * cache-only (downloaded) results, and the merged cache+server union arrives
 * later via a `search-event`. A consumer that ignores that event only ever
 * shows downloaded content, so search "finds nothing" for un-downloaded media.
 *
 * This test models that two-phase backend faithfully and would fail against a
 * version of GenericMediaListPage that does not subscribe to `search-event`.
 *
 * TRACES: UR-008
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/svelte";
import GenericMediaListPage from "./GenericMediaListPage.svelte";
import type { MediaListConfig } from "./GenericMediaListPage.svelte";

vi.mock("$app/navigation", () => ({ goto: vi.fn() }));

vi.mock("$lib/stores/library", () => ({
  currentLibrary: {
    subscribe: vi.fn((fn) => {
      fn({ id: "lib123", name: "Music" });
      return vi.fn();
    }),
  },
  // Consumed as a store ($viewMode) by LibraryGrid, which this page renders.
  viewMode: {
    subscribe: vi.fn((fn) => {
      fn("grid");
      return vi.fn();
    }),
  },
}));

vi.mock("$lib/stores/auth", () => ({
  auth: { getRepository: vi.fn() },
}));

vi.mock("$lib/composables/useServerReachabilityReload", () => ({
  useServerReachabilityReload: vi.fn(() => ({ markLoaded: vi.fn() })),
}));

// Capture the `search-event` handler the component registers so the test can
// drive the deferred (server) phase manually.
let searchEventHandler: ((event: { payload: unknown }) => void) | null = null;
vi.mock("@tauri-apps/api/event", () => ({
  listen: vi.fn(async (name: string, handler: (event: { payload: unknown }) => void) => {
    if (name === "search-event") searchEventHandler = handler;
    return () => {};
  }),
}));

const ALBUM_CONFIG: MediaListConfig = {
  itemType: "MusicAlbum",
  title: "Albums",
  backPath: "/library/music",
  searchPlaceholder: "Search albums...",
  sortOptions: [{ key: "SortName", label: "Title" }],
  defaultSort: "SortName",
  displayComponent: "grid",
};

describe("GenericMediaListPage — two-phase search", () => {
  beforeEach(() => {
    searchEventHandler = null;
    vi.clearAllMocks();
  });

  it("renders server results that arrive after the cache-only phase", async () => {
    // Phase 1 (synchronous) returns cache-only — empty, as it is for a user who
    // has downloaded nothing. This is the exact condition that used to show
    // "nothing found" even though the server has matching albums.
    let capturedRequestId: number | undefined;
    const search = vi.fn(async (_q: string, _opts: unknown, requestId: number) => {
      capturedRequestId = requestId;
      return { items: [], totalRecordCount: 0 };
    });

    const getItems = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });
    vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
      getItems,
      search,
    } as any);

    const { container } = render(GenericMediaListPage, { props: { config: ALBUM_CONFIG } });

    // Let the initial (mount) load finish so the debounced search effect is armed.
    await waitFor(() => expect(getItems).toHaveBeenCalled());

    const input = container.querySelector("input") as HTMLInputElement;
    fireEvent.input(input, { target: { value: "Rumours" } });

    // Debounced search fires after 300ms and returns the empty cache result.
    // The `search-event` listener is registered lazily as part of searching.
    await waitFor(() => expect(search).toHaveBeenCalled());
    await waitFor(() => expect(searchEventHandler).not.toBeNull());
    // Cache-only phase: nothing to show yet (the results counter reads zero).
    await waitFor(() =>
      expect(screen.getByText(/0 musicalbums matching/)).toBeTruthy()
    );

    // Phase 2: backend emits the merged cache+server union for this request.
    expect(capturedRequestId).toBeTypeOf("number");
    searchEventHandler!({
      payload: {
        requestId: capturedRequestId,
        result: {
          items: [{ id: "album1", name: "Rumours", type: "MusicAlbum" }],
          totalRecordCount: 1,
        },
      },
    });

    // The server result must now be reflected in the list. Old code (no
    // listener) never reached this state — the count stayed at zero.
    await waitFor(() =>
      expect(screen.getByText(/1 musicalbum matching/)).toBeTruthy()
    );
  });

  it("ignores a search-event whose requestId is stale", async () => {
    const search = vi.fn(async () => ({ items: [], totalRecordCount: 0 }));
    const getItems = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });
    vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
      getItems,
      search,
    } as any);

    const { container } = render(GenericMediaListPage, { props: { config: ALBUM_CONFIG } });
    await waitFor(() => expect(getItems).toHaveBeenCalled());

    const input = container.querySelector("input") as HTMLInputElement;
    fireEvent.input(input, { target: { value: "Rumours" } });
    await waitFor(() => expect(search).toHaveBeenCalled());
    await waitFor(() => expect(searchEventHandler).not.toBeNull());

    // A superseded query's late result (wrong requestId) must not render.
    searchEventHandler!({
      payload: {
        requestId: -999,
        result: {
          items: [{ id: "stale", name: "Stale Album", type: "MusicAlbum" }],
          totalRecordCount: 1,
        },
      },
    });

    await new Promise((r) => setTimeout(r, 0));
    expect(screen.queryByText("Stale Album")).toBeNull();
  });
});
