/**
 * Scoped search through the library store.
 *
 * TRACES: UR-049 | DR-065 | UT-*
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { get } from "svelte/store";

const searchMock = vi.fn();

vi.mock("@tauri-apps/api/event", () => ({
  listen: vi.fn(async () => () => {}),
}));

vi.mock("./auth", () => ({
  auth: {
    getRepository: () => ({ search: searchMock }),
  },
}));

import { library } from "./library";

function result(items: { id: string; type: string }[] = []) {
  return { items, totalRecordCount: items.length };
}

describe("library.search scoping", () => {
  beforeEach(() => {
    searchMock.mockReset();
    searchMock.mockResolvedValue(result());
    library.clearSearch();
  });

  // The frontend sends the OPAQUE scope and never names a Jellyfin item type.
  // Expansion (music → MusicAlbum/MusicArtist/Audio/Playlist) is asserted in
  // Rust — see `search_scope_tests` in src-tauri/src/repository/types.rs.
  // Asserting item types here would mean the frontend knows the taxonomy again,
  // which is the leak docs/specs/scoped-search-boundary.md exists to prevent.

  it("sends the default (all) scope and never an item-type list", async () => {
    await library.search("office");

    const options = searchMock.mock.calls[0][1];
    expect(options.scope).toBe("all");
    expect(options).not.toHaveProperty("includeItemTypes");
    expect(options.limit).toBe(10000);
  });

  it("sends the opaque scope when scoped to music", async () => {
    await library.search("office", "music");

    const options = searchMock.mock.calls[0][1];
    expect(options.scope).toBe("music");
    expect(options).not.toHaveProperty("includeItemTypes");
  });

  it("sends the opaque scope when scoped to tv", async () => {
    await library.search("office", "tv");

    const options = searchMock.mock.calls[0][1];
    expect(options.scope).toBe("tv");
    expect(options).not.toHaveProperty("includeItemTypes");
  });

  it("sends the opaque scope when scoped to movies", async () => {
    await library.search("office", "movies");

    const options = searchMock.mock.calls[0][1];
    expect(options.scope).toBe("movies");
    expect(options).not.toHaveProperty("includeItemTypes");
  });

  it("stores results and the query on success", async () => {
    searchMock.mockResolvedValue(result([{ id: "1", type: "Movie" }]));

    await library.search("office", "movies");

    const state = get(library);
    expect(state.searchQuery).toBe("office");
    expect(state.searchResults.map((i) => i.id)).toEqual(["1"]);
  });

  it("clears results for an empty query without hitting the repository", async () => {
    searchMock.mockResolvedValue(result([{ id: "1", type: "Movie" }]));
    await library.search("office", "movies");
    searchMock.mockClear();

    await library.search("   ");

    expect(searchMock).not.toHaveBeenCalled();
    const state = get(library);
    expect(state.searchQuery).toBe("");
    expect(state.searchResults).toEqual([]);
  });

  it("discards a superseded response (stale requestId guard)", async () => {
    // First search resolves *after* a newer one has already started; its
    // results must not clobber the fresher ones.
    let resolveFirst: (value: unknown) => void = () => {};
    searchMock.mockImplementationOnce(
      () => new Promise((resolve) => (resolveFirst = resolve))
    );
    searchMock.mockResolvedValueOnce(result([{ id: "new", type: "Movie" }]));

    const first = library.search("old", "all");
    await library.search("new", "movies");

    resolveFirst(result([{ id: "old", type: "Audio" }]));
    await first;

    expect(get(library).searchResults.map((i) => i.id)).toEqual(["new"]);
  });

  it("passes an increasing requestId to the repository", async () => {
    await library.search("a");
    await library.search("b");

    const [firstId, secondId] = searchMock.mock.calls.map((c) => c[2]);
    expect(secondId).toBeGreaterThan(firstId);
  });

  it("surfaces a repository failure as a store error", async () => {
    searchMock.mockRejectedValue(new Error("boom"));

    await expect(library.search("office", "tv")).rejects.toThrow("boom");
    expect(get(library).error).toBe("boom");
    expect(get(library).loadingCount).toBe(0);
  });
});
