import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { flushSync } from 'svelte';
import { readable } from 'svelte/store';
import { mockInfiniteQuery } from '../test-utils/query';
import type { ArtistRef, Page } from '$lib/api/types';

const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/') }));

vi.mock('$app/state', () => ({
  page: { get url() { return state.pageUrl; } }
}));

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

vi.mock('$lib/api/queries', () => ({
  qk: { artists: (sort: string) => ['artists', { sort }] },
  createArtistsQuery: vi.fn()
}));

vi.mock('$lib/api/likes', () => ({
  createLikedIdsQuery: () => readable({
    data: { track_ids: [], album_ids: [], artist_ids: [] },
    isPending: false,
    isError: false
  }),
  likeEntity: vi.fn(),
  unlikeEntity: vi.fn()
}));

vi.mock('@tanstack/svelte-query', async (orig) => {
  const actual = (await orig()) as Record<string, unknown>;
  return { ...actual, useQueryClient: () => ({}) };
});

import ArtistsPage from './+page.svelte';
import { createArtistsQuery } from '$lib/api/queries';
import { goto } from '$app/navigation';

function page<T>(items: T[], total: number, offset = 0, limit = 50): Page<T> {
  return { items, total, offset, limit };
}

afterEach(() => {
  vi.clearAllMocks();
  state.pageUrl = new URL('http://localhost/');
});

describe('artists list page', () => {
  test('renders a row per artist', () => {
    const alice: ArtistRef = { id: 'a', name: 'Alice', album_count: 3 };
    const bob:   ArtistRef = { id: 'b', name: 'Bob',   album_count: 1 };
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ pages: [page([alice, bob], 2)] })
    );
    render(ArtistsPage);
    expect(screen.getByRole('link', { name: /Alice/ })).toHaveAttribute('href', '/artists/a');
    expect(screen.getByRole('link', { name: /Bob/ })).toHaveAttribute('href', '/artists/b');
  });

  test('renders the total count from the first page', () => {
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ pages: [page<ArtistRef>([], 1247)] })
    );
    render(ArtistsPage);
    expect(screen.getByText(/1247 artists/i)).toBeInTheDocument();
  });

  test('Load more button calls fetchNextPage when hasNextPage', async () => {
    const fetchNextPage = vi.fn();
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({
        pages: [page<ArtistRef>([], 100)],
        hasNextPage: true,
        fetchNextPage
      })
    );
    render(ArtistsPage);
    await fireEvent.click(screen.getByRole('button', { name: /load more/i }));
    expect(fetchNextPage).toHaveBeenCalledTimes(1);
  });

  test('"End of library" when hasNextPage is false and at least one page loaded', () => {
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a', name: 'A', album_count: 1 }], 1)] })
    );
    render(ArtistsPage);
    expect(screen.getByText(/end of library/i)).toBeInTheDocument();
  });

  test('empty total renders empty-library message', () => {
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ pages: [page<ArtistRef>([], 0)] })
    );
    render(ArtistsPage);
    expect(screen.getByText(/no artists yet/i)).toBeInTheDocument();
  });

  test('changing the sort dropdown calls goto with ?sort=newest', async () => {
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ pages: [page<ArtistRef>([], 0)] })
    );
    render(ArtistsPage);
    const select = screen.getByLabelText(/sort/i) as HTMLSelectElement;
    await fireEvent.change(select, { target: { value: 'newest' } });
    expect(goto).toHaveBeenCalledWith('?sort=newest', { replaceState: true });
  });

  test('pending state renders the list skeleton after the useDelayed window', () => {
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({ isPending: true })
    );
    vi.useFakeTimers();
    try {
      render(ArtistsPage);
      vi.advanceTimersByTime(200);
      flushSync(); // flush the $effect that reads delayed.value
      expect(screen.getByTestId('skeleton-list')).toBeInTheDocument();
    } finally {
      vi.useRealTimers();
    }
  });

  test('error state renders the error banner with retry', async () => {
    const refetch = vi.fn();
    (createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
      mockInfiniteQuery({
        isError: true,
        error: { code: 'server_error', message: 'db down', status: 500 },
        refetch
      })
    );
    render(ArtistsPage);
    expect(screen.getByRole('alert')).toHaveTextContent('db down');
    await fireEvent.click(screen.getByRole('button', { name: /try again/i }));
    expect(refetch).toHaveBeenCalledTimes(1);
  });
});
