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

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

// Mock stores
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(() => ({
      getItems: vi.fn(),
      search: vi.fn(),
    })),
  },
}));

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

// The component lazily subscribes to the backend `search-event` when searching.
vi.mock("@tauri-apps/api/event", () => ({
  listen: vi.fn(async () => () => {}),
}));

describe("GenericMediaListPage", () => {
  // Component integration tests - core sorting/search/debouncing logic tested in backend-integration.test.ts
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.clearAllTimers();
  });

  describe("Component Initialization", () => {
    it("should render with title and search bar", () => {
      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

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

      const heading = screen.getByText("Tracks");
      expect(heading).toBeTruthy();

      const searchInput = container.querySelector('input[type="text"]');
      expect(searchInput).toBeTruthy();
    });

    it("should load items on mount", async () => {
      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
        getItems: mockGetItemsFn,
        search: vi.fn(),
      } as any);

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.anything()));
    });

    it("should display sort options", () => {
      const config = {
        itemType: "MusicAlbum" as const,
        title: "Albums",
        backPath: "/library/music",
        searchPlaceholder: "Search albums...",
        sortOptions: [
          { key: "SortName", label: "Title" },
          { key: "Artist", label: "Artist" },
          { key: "ProductionYear", label: "Year" },
        ],
        defaultSort: "SortName",
        displayComponent: "grid" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      // Check that all sort options are rendered
      const titleOption = screen.queryByText("Title");
      expect(titleOption).toBeTruthy();
    });
  });

  describe("Search Functionality", () => {
    it("should debounce rapid keystrokes into a single search for the final value", async () => {
      const mockSearchFn = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });
      const mockGetItemsFn = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
        getItems: mockGetItemsFn,
        search: mockSearchFn,
      } as any);

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

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

      // Let the mount load settle so the debounce effect is armed.
      await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());

      // Drive the debounce window deterministically with fake timers, flushing
      // the async loadItems() microtasks after the timer fires.
      vi.useFakeTimers();
      const searchInput = container.querySelector("input") as HTMLInputElement;

      fireEvent.input(searchInput, { target: { value: "t" } });
      await vi.advanceTimersByTimeAsync(100);
      fireEvent.input(searchInput, { target: { value: "te" } });
      await vi.advanceTimersByTimeAsync(100);
      fireEvent.input(searchInput, { target: { value: "tes" } });
      await vi.advanceTimersByTimeAsync(100);
      fireEvent.input(searchInput, { target: { value: "test" } });

      // 200ms after the final keystroke: still inside the 300ms window, so no
      // search has fired despite four keystrokes.
      await vi.advanceTimersByTimeAsync(200);
      expect(mockSearchFn).not.toHaveBeenCalled();

      // Cross the threshold: exactly one search, for the final value.
      await vi.advanceTimersByTimeAsync(100);
      vi.useRealTimers();

      expect(mockSearchFn).toHaveBeenCalledTimes(1);
      expect(mockSearchFn).toHaveBeenCalledWith("test", expect.anything(), expect.any(Number));
    });

    it("should use backend search when search query is provided", async () => {
      const mockSearchFn = vi.fn().mockResolvedValue({
        items: [{ id: "item1", name: "Test Track" }],
        totalRecordCount: 1,
      });

      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: mockSearchFn,
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

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

      // Wait for the initial mount load so the debounced search effect is armed.
      await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());

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

      // search() is called as search(query, options, requestId).
      await waitFor(() => {
        expect(mockSearchFn).toHaveBeenCalledWith(
          "test",
          expect.objectContaining({
            includeItemTypes: ["Audio"],
            limit: 10000,
          }),
          expect.any(Number)
        );
      });
    });

    it("should use getItems without search for empty query", async () => {
      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => {
        expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.objectContaining({
          includeItemTypes: ["Audio"],
          sortBy: "SortName",
          sortOrder: "Ascending",
        }));
      });
    });

    it("should clear previous search when input becomes empty", async () => {
      vi.useFakeTimers();

      const mockSearchFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: mockSearchFn,
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

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

      const searchInput = container.querySelector("input") as HTMLInputElement;

      // Type search query
      fireEvent.input(searchInput, { target: { value: "test" } });
      vi.advanceTimersByTime(300);

      // Clear search
      fireEvent.input(searchInput, { target: { value: "" } });
      vi.advanceTimersByTime(300);

      await waitFor(() => {
        // Should call getItems when search is cleared
        expect(mockGetItemsFn).toHaveBeenCalled();
      });

      vi.useRealTimers();
    });
  });

  describe("Sorting Functionality", () => {
    it("should pass sortBy parameter to backend", async () => {
      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

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

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => {
        expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.objectContaining({
          sortBy: "SortName",
          sortOrder: "Ascending",
        }));
      });
    });

    it("should pass Jellyfin field names to backend (not custom compareFn)", async () => {
      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [
          { key: "SortName", label: "Title" },
          { key: "Artist", label: "Artist" },
          { key: "Album", label: "Album" },
          { key: "DatePlayed", label: "Recent" },
        ],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => {
        const lastCall = (mockGetItemsFn as any).mock.calls[0];
        const options = lastCall[1];

        // Should pass Jellyfin field names directly
        expect(typeof options.sortBy).toBe("string");
        expect(["SortName", "Artist", "Album", "DatePlayed"]).toContain(options.sortBy);
      });
    });
  });

  describe("ItemType Filtering", () => {
    it("should include correct itemType in getItems request", async () => {
      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => {
        expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.objectContaining({
          includeItemTypes: ["Audio"],
        }));
      });
    });

    it("should include correct itemType in search request", async () => {
      const mockSearchFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockGetItemsFn = vi.fn().mockResolvedValue({
        items: [],
        totalRecordCount: 0,
      });

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: mockSearchFn,
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

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

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

      await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());

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

      await waitFor(() => {
        expect(mockSearchFn).toHaveBeenCalledWith(
          "album",
          expect.objectContaining({
            includeItemTypes: ["MusicAlbum"],
          }),
          expect.any(Number)
        );
      });
    });
  });

  describe("Loading State", () => {
    it("should show loading indicator during data fetch", async () => {
      const mockGetItemsFn = vi.fn(
        () => new Promise((resolve) => setTimeout(
          () => resolve({ items: [], totalRecordCount: 0 }),
          100
        ))
      );

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      vi.useFakeTimers();
      const { container } = render(GenericMediaListPage, {
        props: { config },
      });

      // Component should be rendering (will show loading state internally)
      expect(container).toBeTruthy();

      vi.advanceTimersByTime(100);
      vi.useRealTimers();
    });
  });

  describe("Error Handling", () => {
    it("should handle backend errors gracefully", async () => {
      const mockGetItemsFn = vi.fn().mockRejectedValue(new Error("Network error"));

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      await waitFor(() => {
        // Should handle error without throwing
        expect(mockGetItemsFn).toHaveBeenCalled();
      });
    });

    it("should handle missing library gracefully", async () => {
      const { goto } = await import("$app/navigation");
      vi.mocked(goto).mockClear();

      const mockGetItemsFn = vi.fn();

      const mockRepository = {
        getItems: mockGetItemsFn,
        search: vi.fn(),
      };

      vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
        mockRepository as any
      );

      // Deliver a null current library for this test only.
      const currentLibrary = vi.mocked((await import("$lib/stores/library")).currentLibrary);
      currentLibrary.subscribe.mockImplementation((fn: any) => {
        fn(null);
        return vi.fn();
      });

      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      render(GenericMediaListPage, {
        props: { config },
      });

      // With no current library, loadItems bails out to the back path and never
      // queries the repository.
      await waitFor(() => expect(goto).toHaveBeenCalledWith("/library/music"));
      expect(mockGetItemsFn).not.toHaveBeenCalled();

      // Restore the default (non-null) library for subsequent tests.
      currentLibrary.subscribe.mockImplementation((fn: any) => {
        fn({ id: "lib123", name: "Music" });
        return vi.fn();
      });
    });
  });

  describe("Display Component Props", () => {
    it("should support grid display component", () => {
      const config = {
        itemType: "MusicAlbum" as const,
        title: "Albums",
        backPath: "/library/music",
        searchPlaceholder: "Search albums...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "grid" as const,
      };

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

      expect(container).toBeTruthy();
    });

    it("should support tracklist display component", () => {
      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

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

      expect(container).toBeTruthy();
    });
  });

  describe("Config Simplification", () => {
    it("should not require searchFields in config", () => {
      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [{ key: "SortName", label: "Title" }],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
        // Note: searchFields is NOT present
      };

      // Should render without searchFields
      expect(() => {
        render(GenericMediaListPage, {
          props: { config },
        });
      }).not.toThrow();
    });

    it("should not require compareFn in sort options", () => {
      const config = {
        itemType: "Audio" as const,
        title: "Tracks",
        backPath: "/library/music",
        searchPlaceholder: "Search tracks...",
        sortOptions: [
          { key: "SortName", label: "Title" },
          // Note: no compareFn property
        ],
        defaultSort: "SortName",
        displayComponent: "tracklist" as const,
      };

      // Should render without compareFn in sort options
      expect(() => {
        render(GenericMediaListPage, {
          props: { config },
        });
      }).not.toThrow();
    });
  });
});
