import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { makeTrack } from '$test-utils/fixtures/track';

// QueueTrackRow now renders a LikeButton, which reads createLikedIdsQuery.
// Stub the likes API so the row doesn't need a real QueryClient in context.
// The mock (and its emptyLikesMock import) must precede the component import
// below: importing QueueTrackRow transitively loads LikeButton → the mocked
// module, and the hoisted factory needs emptyLikesMock already initialized.
vi.mock('$lib/api/likes', () => emptyLikesMock());

import QueueTrackRow from './QueueTrackRow.svelte';

const playFromQueueIndex = vi.fn();
const removeFromQueue = vi.fn();
const moveQueueItem = vi.fn();

vi.mock('$lib/player/store.svelte', () => ({
  playFromQueueIndex: (...args: unknown[]) => playFromQueueIndex(...args),
  removeFromQueue: (...args: unknown[]) => removeFromQueue(...args),
  moveQueueItem: (...args: unknown[]) => moveQueueItem(...args)
}));

const sampleTrack = makeTrack({
  title: 'Song Title',
  artist_name: 'Artist Name'
});

describe('QueueTrackRow', () => {
  beforeEach(() => {
    playFromQueueIndex.mockClear();
    removeFromQueue.mockClear();
    moveQueueItem.mockClear();
  });

  it('renders track title and artist', () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    expect(screen.getByText('Song Title')).toBeInTheDocument();
    expect(screen.getByText(/Artist Name/)).toBeInTheDocument();
  });

  it('clicking the body calls playFromQueueIndex with the row index', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const body = screen.getByRole('button', { name: /play song title/i });
    await fireEvent.click(body);
    expect(playFromQueueIndex).toHaveBeenCalledWith(3);
  });

  it('clicking the remove button calls removeFromQueue with the row index', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const remove = screen.getByLabelText(/remove from queue/i);
    await fireEvent.click(remove);
    expect(removeFromQueue).toHaveBeenCalledWith(3);
  });

  it('current row shows the "Now playing" chip', () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: true } });
    expect(screen.getByText(/now playing/i)).toBeInTheDocument();
  });

  it('current row body click does NOT call playFromQueueIndex', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: true } });
    // The body button has a different aria-label when isCurrent
    const body = screen.getByLabelText(/now playing/i);
    await fireEvent.click(body);
    expect(playFromQueueIndex).not.toHaveBeenCalled();
  });

  it('ArrowDown on the drag handle calls moveQueueItem with index+1', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const handle = screen.getByLabelText(/reorder track/i);
    await fireEvent.keyDown(handle, { key: 'ArrowDown' });
    expect(moveQueueItem).toHaveBeenCalledWith(3, 4);
  });

  it('ArrowUp on the drag handle calls moveQueueItem with index-1', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const handle = screen.getByLabelText(/reorder track/i);
    await fireEvent.keyDown(handle, { key: 'ArrowUp' });
    expect(moveQueueItem).toHaveBeenCalledWith(3, 2);
  });

  it('Enter on the drag handle prevents default but does not call moveQueueItem', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const handle = screen.getByLabelText(/reorder track/i);
    await fireEvent.keyDown(handle, { key: 'Enter' });
    expect(moveQueueItem).not.toHaveBeenCalled();
  });

  it('Space on the drag handle prevents default but does not call moveQueueItem', async () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const handle = screen.getByLabelText(/reorder track/i);
    await fireEvent.keyDown(handle, { key: ' ' });
    expect(moveQueueItem).not.toHaveBeenCalled();
  });

  // --- handle placement (#2395) ---

  it('the reorder handle overlays the album art instead of taking its own column', () => {
    const { container } = render(QueueTrackRow, {
      props: { track: sampleTrack, index: 3, isCurrent: false }
    });
    const handle = screen.getByLabelText(/reorder track/i);
    const art = container.querySelector('img');
    expect(art).not.toBeNull();
    // Sharing a parent is what "overlaid" means structurally. If someone moves
    // the grip back into its own flex slot, this fails — which is the point:
    // that slot cost horizontal space in every row and is why #2395 exists.
    expect(handle.parentElement).toBe(art!.parentElement);
  });

  it('the handle is visible at rest, not hover-revealed', () => {
    render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
    const handle = screen.getByLabelText(/reorder track/i);
    // Overlaying already solved the space complaint, so there is nothing to buy
    // by hiding it — and hiding it would cost the only cue that the queue can
    // be reordered, on touch especially, where there is no hover at all.
    // Asserting the absence of `opacity-0` is stylistic and a bit brittle, but
    // it is the only handle jsdom gives us on a decision worth protecting.
    expect(handle.className).not.toMatch(/\bopacity-0\b/);
  });
});
