/**
 * Regression: `/search` must not fight the user's typing when the page was
 * seeded from the URL (`?q=`), i.e. when the desktop header search navigated
 * here.
 *
 * The original seeding effect read `$library.searchQuery`, so every store write
 * re-ran it and re-asserted the URL's `q` over whatever had been typed since.
 * Typing one more character therefore snapped the input back to the query the
 * header had sent, and picking a scope chip snapped back to the URL's scope.
 * Arriving from the bottom-nav Search tab (no `?q=`) hit the effect's
 * empty-query early return, so the same component behaved correctly — the bug
 * only showed up on the header's route into the page.
 *
 * TRACES: UR-049 | DR-147
 */

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

const { pageStore, libState, search, clearSearch, goto } = vi.hoisted(() => {
  // svelte/store is safe to import inside a hoisted factory (no test-file TDZ).
  const { writable } = require("svelte/store");
  return {
    pageStore: writable({ url: new URL("http://localhost/search") }),
    libState: writable({ searchQuery: "", searchResults: [], loadingCount: 0 }),
    search: vi.fn(),
    clearSearch: vi.fn(),
    goto: vi.fn(),
  };
});

vi.mock("$app/stores", () => ({ page: pageStore, navigating: { subscribe: () => () => {} } }));
vi.mock("$app/navigation", () => ({ goto, afterNavigate: vi.fn(), beforeNavigate: vi.fn() }));
vi.mock("$lib/stores/library", () => ({
  library: { subscribe: libState.subscribe, search, clearSearch },
}));

import Page from "./+page.svelte";

/** The store write `library.search` performs — the trigger for the old loop. */
function primeSearch() {
  search.mockImplementation(async (query: string) => {
    libState.update((s: Record<string, unknown>) => ({ ...s, searchQuery: query }));
  });
  clearSearch.mockImplementation(() => {
    libState.update((s: Record<string, unknown>) => ({ ...s, searchQuery: "", searchResults: [] }));
  });
}

/** Wait past Search.svelte's 300ms input debounce, then let effects settle. */
const afterDebounce = () => new Promise((resolve) => setTimeout(resolve, 450));

function input(): HTMLInputElement {
  return screen.getByPlaceholderText("Search your library...") as HTMLInputElement;
}

beforeEach(() => {
  vi.clearAllMocks();
  libState.set({ searchQuery: "", searchResults: [], loadingCount: 0 });
  pageStore.set({ url: new URL("http://localhost/search") });
  primeSearch();
  // Model SvelteKit: a goto publishes the new URL through the page store, so a
  // scope chip's URL round trip really runs through the seeding effect.
  goto.mockImplementation(async (url: string) => {
    pageStore.set({ url: new URL(url, "http://localhost") });
  });
});

describe("/search seeded from the URL", () => {
  it("keeps the text the user types on top of the seeded query", async () => {
    pageStore.set({ url: new URL("http://localhost/search?q=abc") });
    render(Page);

    await waitFor(() => expect(search).toHaveBeenCalledWith("abc", "all"));

    await fireEvent.input(input(), { target: { value: "abcd" } });
    await afterDebounce();

    expect(input().value).toBe("abcd");
    expect(search).toHaveBeenLastCalledWith("abcd", "all");
  });

  it("keeps a scope chip the user picks instead of snapping back to the URL", async () => {
    pageStore.set({ url: new URL("http://localhost/search?q=abc&scope=music") });
    render(Page);

    await waitFor(() => expect(search).toHaveBeenCalledWith("abc", "music"));

    await fireEvent.click(screen.getByRole("radio", { name: "All" }));
    await afterDebounce();

    expect(screen.getByRole("radio", { name: "All" }).getAttribute("aria-checked")).toBe("true");
    expect(search).toHaveBeenLastCalledWith("abc", "all");
  });

  it("re-seeds when a fresh query arrives in the URL (header search on this page)", async () => {
    pageStore.set({ url: new URL("http://localhost/search?q=abc") });
    render(Page);
    await waitFor(() => expect(search).toHaveBeenCalledWith("abc", "all"));

    pageStore.set({ url: new URL("http://localhost/search?q=zeppelin") });

    await waitFor(() => expect(search).toHaveBeenLastCalledWith("zeppelin", "all"));
    expect(input().value).toBe("zeppelin");
  });

  it("clears when the URL query is emptied", async () => {
    pageStore.set({ url: new URL("http://localhost/search?q=abc") });
    render(Page);
    await waitFor(() => expect(search).toHaveBeenCalledWith("abc", "all"));

    pageStore.set({ url: new URL("http://localhost/search") });

    await waitFor(() => expect(input().value).toBe(""));
    expect(screen.getByText("Search your entire library")).toBeTruthy();
  });
});

describe("/search entered with no query (bottom-nav Search tab)", () => {
  it("searches what the user types and leaves it alone", async () => {
    render(Page);

    await fireEvent.input(input(), { target: { value: "moon" } });
    await afterDebounce();

    expect(input().value).toBe("moon");
    expect(search).toHaveBeenLastCalledWith("moon", "all");
  });
});
