import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// Mock modules BEFORE any imports that might use them
vi.mock("@tauri-apps/api/core", () => ({
  invoke: vi.fn(),
}));

vi.mock("@tauri-apps/plugin-os", () => ({
  platform: vi.fn(() => "linux"),
}));

vi.mock("$lib/api/client", () => {
  return {
    default: class {
      static getDeviceName = () => "test-device";
    },
  };
});

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

vi.mock("$lib/stores/queue", () => ({
  queue: {
    setQueue: vi.fn(),
    addToQueue: vi.fn(),
  },
  currentQueueItem: { subscribe: vi.fn((fn: any) => { fn(null); return () => {}; }) },
}));

vi.mock("./DownloadButton.svelte", () => ({
  default: vi.fn(() => ({ $$: {}, $set: vi.fn(), $on: vi.fn(), $destroy: vi.fn() })),
}));

vi.mock("$lib/stores/library", () => ({
  library: {
    loadLibraries: vi.fn(),
    loadItems: vi.fn(),
    loadItem: vi.fn(),
    setCurrentLibrary: vi.fn(),
  },
  libraries: { subscribe: vi.fn((fn: any) => { fn([]); return () => {}; }) },
  libraryItems: { subscribe: vi.fn((fn: any) => { fn([]); return () => {}; }) },
  currentLibrary: { subscribe: vi.fn((fn: any) => { fn(null); return () => {}; }) },
  isLibraryLoading: { subscribe: vi.fn((fn: any) => { fn(false); return () => {}; }) },
}));

// Now import the modules after mocks are set up
import { render, fireEvent, waitFor } from "@testing-library/svelte";
import { invoke } from "@tauri-apps/api/core";
import TrackList from "./TrackList.svelte";
import type { MediaItem } from "$lib/api/types";
import { auth } from "$lib/stores/auth";
import { toast } from "$lib/stores/toast";

describe("TrackList", () => {
  const mockRepository = {
    getAudioStreamUrl: vi.fn(),
    getImageUrl: vi.fn(),
    getHandle: vi.fn(() => "mock-repository-handle"),
  };

  const mockTracks: MediaItem[] = [
    {
      id: "track-1",
      name: "Song 1",
      type: "Audio",
      serverId: "server-1",
      artists: ["Artist 1"],
      albumName: "Album 1",
      albumId: "album-1",
      runTimeTicks: 1800000000, // 3 minutes
      primaryImageTag: "tag1",
      indexNumber: 1,
    },
    {
      id: "track-2",
      name: "Song 2",
      type: "Audio",
      serverId: "server-1",
      artists: ["Artist 2"],
      albumName: "Album 2",
      albumId: "album-2",
      runTimeTicks: 2400000000, // 4 minutes
      primaryImageTag: "tag2",
      indexNumber: 2,
    },
    {
      id: "track-3",
      name: "Song 3 with a Very Long Name That Should Be Truncated",
      type: "Audio",
      serverId: "server-1",
      artists: ["Artist 3", "Artist 4"],
      albumName: "Album 3",
      albumId: "album-3",
      runTimeTicks: 3000000000, // 5 minutes
      indexNumber: 3,
    },
  ];

  beforeEach(() => {
    vi.clearAllMocks();
    (auth.getRepository as any).mockReturnValue(mockRepository as any);
    mockRepository.getAudioStreamUrl.mockResolvedValue("http://stream.url/track");
    mockRepository.getImageUrl.mockReturnValue("http://image.url/artwork");
  });

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

  describe("Rendering Tests", () => {
    it("renders track list with tracks", () => {
      // Component renders both desktop and mobile views, so use getAllByText
      const { getAllByText } = render(TrackList, { props: { tracks: mockTracks } });

      expect(getAllByText("Song 1").length).toBeGreaterThan(0);
      expect(getAllByText("Song 2").length).toBeGreaterThan(0);
      // Long names are abbreviated in the middle via truncateMiddle(name, 48).
      expect(getAllByText(/Song 3 with a Very Long .*Should Be Truncated/).length).toBeGreaterThan(0);
    });

    it("shows loading skeleton when loading=true", () => {
      const { container } = render(TrackList, {
        props: { tracks: [], loading: true },
      });

      const skeletons = container.querySelectorAll(".animate-pulse");
      expect(skeletons.length).toBeGreaterThan(0);
    });

    it("shows empty state when no tracks", () => {
      const { getByText } = render(TrackList, { props: { tracks: [] } });

      expect(getByText("No tracks found")).toBeTruthy();
    });

    it("shows artist column by default", () => {
      const { getByText, getAllByText } = render(TrackList, { props: { tracks: mockTracks } });

      // Header only exists in the desktop table.
      expect(getByText("Artist")).toBeTruthy();
      // Artist name renders in both desktop and mobile views.
      expect(getAllByText("Artist 1").length).toBeGreaterThan(0);
    });

    it("hides artist column when showArtist=false", () => {
      const { queryByText } = render(TrackList, {
        props: { tracks: mockTracks, showArtist: false },
      });

      // Header should not be present
      expect(queryByText("Artist")).toBeFalsy();
    });

    it("shows album column by default", () => {
      const { getByText, getAllByText } = render(TrackList, { props: { tracks: mockTracks } });

      // Header only exists in the desktop table.
      expect(getByText("Album")).toBeTruthy();
      // Album name renders in both desktop and mobile views.
      expect(getAllByText("Album 1").length).toBeGreaterThan(0);
    });

    it("hides album column when showAlbum=false", () => {
      const { queryByText } = render(TrackList, {
        props: { tracks: mockTracks, showAlbum: false },
      });

      // Header should not be present
      expect(queryByText("Album")).toBeFalsy();
    });

    it("shows duration in correct format", () => {
      // Component renders both desktop and mobile views
      const { getAllByText } = render(TrackList, { props: { tracks: mockTracks } });

      expect(getAllByText("3:00").length).toBeGreaterThan(0); // track-1: 3 minutes
      expect(getAllByText("4:00").length).toBeGreaterThan(0); // track-2: 4 minutes
      expect(getAllByText("5:00").length).toBeGreaterThan(0); // track-3: 5 minutes
    });

    it("handles tracks without duration", () => {
      const tracksWithoutDuration: MediaItem[] = [
        {
          ...mockTracks[0],
          runTimeTicks: undefined,
        },
      ];

      // formatDuration(undefined) renders an empty string, so the row still
      // renders without crashing and the track title is present.
      const { getAllByText } = render(TrackList, {
        props: { tracks: tracksWithoutDuration },
      });

      expect(getAllByText("Song 1").length).toBeGreaterThan(0);
    });

    it("handles tracks without artist", () => {
      const tracksWithoutArtist: MediaItem[] = [
        {
          ...mockTracks[0],
          artists: undefined,
          artistItems: undefined,
        },
      ];

      // The artist fallback renders "-" in both desktop and mobile views.
      const { getAllByText } = render(TrackList, {
        props: { tracks: tracksWithoutArtist },
      });

      expect(getAllByText("-").length).toBeGreaterThan(0);
    });

    it("renders multiple artists joined with comma", () => {
      // Tracks fall back to artists.join(", ") when artistItems is absent;
      // the joined string renders in both desktop and mobile views.
      const { getAllByText } = render(TrackList, { props: { tracks: mockTracks } });

      expect(getAllByText("Artist 3, Artist 4").length).toBeGreaterThan(0);
    });
  });

  describe("Default Click Handler Tests", () => {
    it("calls player_play_queue when track is clicked", async () => {
      const invokeMock = (invoke as any).mockResolvedValue(undefined);
      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      // Find and click the first track button
      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      expect(firstTrackButton).toBeTruthy();
      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(invokeMock).toHaveBeenCalledWith(
          "player_play_tracks",
          expect.objectContaining({
            repositoryHandle: "mock-repository-handle",
            request: expect.objectContaining({
              trackIds: expect.arrayContaining(["track-1"]),
              startIndex: 0,
              shuffle: false,
            }),
          })
        );
      });
    });

    it("builds queue with all tracks in order", async () => {
      const invokeMock = (invoke as any).mockResolvedValue(undefined);
      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const secondTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 2")
      );

      await fireEvent.click(secondTrackButton!);

      await waitFor(() => {
        const callArgs = invokeMock.mock.calls[0][1] as any;
        expect(callArgs.request.trackIds).toEqual(["track-1", "track-2", "track-3"]);
      });
    });

    it("sets correct startIndex for clicked track", async () => {
      const invokeMock = (invoke as any).mockResolvedValue(undefined);
      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const thirdTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 3")
      );

      await fireEvent.click(thirdTrackButton!);

      await waitFor(() => {
        const callArgs = invokeMock.mock.calls[0][1] as any;
        expect(callArgs.request.startIndex).toBe(2);
      });
    });

    it("shows error toast when playback fails", async () => {
      const toastSpy = vi.spyOn(toast, "error").mockImplementation(() => "");
      (invoke as any).mockRejectedValue(new Error("Network error"));

      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(toastSpy).toHaveBeenCalledWith(
          expect.stringContaining("Failed to play track"),
          expect.anything()
        );
      });

      toastSpy.mockRestore();
    });

    it("handles auth errors gracefully", async () => {
      const toastSpy = vi.spyOn(toast, "error").mockImplementation(() => "");
      // No repository → requireHandle() throws "No repository available",
      // which the default handler surfaces via toast.error.
      (auth.getRepository as any).mockReturnValue(null as any);

      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(toastSpy).toHaveBeenCalledWith(
          expect.stringContaining("Failed to play track"),
          expect.anything()
        );
      });

      toastSpy.mockRestore();

      // Restore mock for other tests
      (auth.getRepository as any).mockReturnValue(mockRepository as any);
    });

  });

  describe("Custom Callback Tests", () => {
    it("calls custom callback when provided", async () => {
      const onTrackClick = vi.fn();
      const { container } = render(TrackList, {
        props: { tracks: mockTracks, onTrackClick },
      });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(onTrackClick).toHaveBeenCalledWith(mockTracks[0], 0);
      });
    });

    it("does not call player_play_queue when custom callback provided", async () => {
      const onTrackClick = vi.fn();
      const invokeMock = (invoke as any);

      const { container } = render(TrackList, {
        props: { tracks: mockTracks, onTrackClick },
      });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(onTrackClick).toHaveBeenCalled();
      });

      expect(invokeMock).not.toHaveBeenCalled();
    });

    it("receives correct track and index in callback", async () => {
      const onTrackClick = vi.fn();
      const { container } = render(TrackList, {
        props: { tracks: mockTracks, onTrackClick },
      });

      const buttons = container.querySelectorAll("button");
      const secondTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 2")
      );

      await fireEvent.click(secondTrackButton!);

      await waitFor(() => {
        expect(onTrackClick).toHaveBeenCalledWith(mockTracks[1], 1);
      });
    });

    it("handles async custom callbacks", async () => {
      const onTrackClick = vi.fn().mockResolvedValue(undefined);
      const { container } = render(TrackList, {
        props: { tracks: mockTracks, onTrackClick },
      });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(onTrackClick).toHaveBeenCalled();
      });
    });

    it("calls custom callback even when it might throw", async () => {
      // Test that custom callbacks are called - error handling is caller's responsibility
      const onTrackClick = vi.fn().mockResolvedValue(undefined);

      const { container } = render(TrackList, {
        props: { tracks: mockTracks, onTrackClick },
      });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        expect(onTrackClick).toHaveBeenCalledWith(mockTracks[0], 0);
      });
    });
  });

  describe("Edge Cases", () => {
    it("handles empty tracks array", () => {
      const { getByText } = render(TrackList, { props: { tracks: [] } });

      expect(getByText("No tracks found")).toBeTruthy();
    });

    it("handles single track", async () => {
      const singleTrack = [mockTracks[0]];
      (invoke as any).mockResolvedValue(undefined);

      const { container } = render(TrackList, { props: { tracks: singleTrack } });

      const buttons = container.querySelectorAll("button");
      const trackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(trackButton!);

      await waitFor(() => {
        const callArgs = (invoke as any).mock.calls[0][1] as any;
        expect(callArgs.request.trackIds).toEqual(["track-1"]);
        expect(callArgs.request.startIndex).toBe(0);
      });
    });

    it("handles click on first track (index 0)", async () => {
      (invoke as any).mockResolvedValue(undefined);
      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      await fireEvent.click(firstTrackButton!);

      await waitFor(() => {
        const callArgs = (invoke as any).mock.calls[0][1] as any;
        expect(callArgs.request.startIndex).toBe(0);
      });
    });

    it("handles click on last track", async () => {
      (invoke as any).mockResolvedValue(undefined);
      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const lastTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 3")
      );

      await fireEvent.click(lastTrackButton!);

      await waitFor(() => {
        const callArgs = (invoke as any).mock.calls[0][1] as any;
        expect(callArgs.request.startIndex).toBe(2);
      });
    });
  });

  describe("Loading State", () => {
    it("shows loading spinner when track is clicked", async () => {
      // Make invoke slow to capture loading state
      (invoke as any).mockImplementation(
        () => new Promise((resolve) => setTimeout(resolve, 100))
      );

      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      fireEvent.click(firstTrackButton!);

      // Check for loading spinner
      await waitFor(() => {
        const spinner = container.querySelector(".animate-spin");
        expect(spinner).toBeTruthy();
      });
    });

    it("disables track buttons during loading", async () => {
      (invoke as any).mockImplementation(
        () => new Promise((resolve) => setTimeout(resolve, 100))
      );

      const { container } = render(TrackList, { props: { tracks: mockTracks } });

      const buttons = container.querySelectorAll("button");
      const firstTrackButton = Array.from(buttons).find((btn) =>
        btn.textContent?.includes("Song 1")
      );

      fireEvent.click(firstTrackButton!);

      // Track selection buttons should be disabled during loading
      await waitFor(() => {
        // Find track buttons (ones containing song names)
        const trackButtons = Array.from(container.querySelectorAll("button")).filter(
          (btn) => btn.textContent?.includes("Song")
        );
        expect(trackButtons.length).toBeGreaterThan(0);
        trackButtons.forEach((btn) => {
          expect(btn.disabled).toBe(true);
        });
      });
    });
  });
});
