import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';

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

vi.mock('$lib/api/admin/tracks', () => ({
  removeTrack: vi.fn()
}));

import RemoveTrackPopover from './RemoveTrackPopover.svelte';
import { removeTrack } from '$lib/api/admin/tracks';

const track: TrackRef = {
  id: 't1',
  title: 'Roygbiv',
  album_id: 'a1',
  album_title: 'Geogaddi',
  artist_id: 'ar1',
  artist_name: 'Boards of Canada',
  duration_sec: 240,
  stream_url: '/api/tracks/t1/stream'
};

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

describe('RemoveTrackPopover', () => {
  test('renders the title and subtext', () => {
    render(RemoveTrackPopover, { props: { track, onClose: vi.fn() } });
    expect(screen.getByText(/remove "roygbiv" from your library/i)).toBeInTheDocument();
    expect(screen.getByText(/this deletes the file from disk/i)).toBeInTheDocument();
  });

  test('renders the Lidarr unmonitor checkbox', () => {
    render(RemoveTrackPopover, { props: { track, onClose: vi.fn() } });
    const cb = screen.getByRole('checkbox', { name: /also stop lidarr/i });
    expect(cb).toBeInTheDocument();
    expect((cb as HTMLInputElement).checked).toBe(false);
  });

  test('Cancel button calls onClose without firing removeTrack', async () => {
    const onClose = vi.fn();
    render(RemoveTrackPopover, { props: { track, onClose } });
    await fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
    expect(removeTrack).not.toHaveBeenCalled();
    expect(onClose).toHaveBeenCalled();
  });

  test('Remove without checkbox calls removeTrack with unmonitor=false', async () => {
    (removeTrack as ReturnType<typeof vi.fn>).mockResolvedValue({ deleted_track_id: 't1' });
    const onClose = vi.fn();
    render(RemoveTrackPopover, { props: { track, onClose } });
    await fireEvent.click(screen.getByRole('button', { name: /^remove$/i }));
    // confirm() chains 3-5 awaited invalidateQueries calls before onClose,
    // each a separate microtask. waitFor polls until the side effects land.
    await waitFor(() => expect(onClose).toHaveBeenCalled());
    expect(removeTrack).toHaveBeenCalledWith('t1', { unmonitor: false });
  });

  test('Remove with checkbox checked calls removeTrack with unmonitor=true', async () => {
    (removeTrack as ReturnType<typeof vi.fn>).mockResolvedValue({ deleted_track_id: 't1' });
    const onClose = vi.fn();
    render(RemoveTrackPopover, { props: { track, onClose } });
    await fireEvent.click(screen.getByRole('checkbox', { name: /also stop lidarr/i }));
    await fireEvent.click(screen.getByRole('button', { name: /^remove$/i }));
    await waitFor(() => expect(onClose).toHaveBeenCalled());
    expect(removeTrack).toHaveBeenCalledWith('t1', { unmonitor: true });
  });

  test('failed remove surfaces error via copyForCode and does not close', async () => {
    (removeTrack as ReturnType<typeof vi.fn>).mockRejectedValue({ code: 'not_found' });
    const onClose = vi.fn();
    render(RemoveTrackPopover, { props: { track, onClose } });
    await fireEvent.click(screen.getByRole('button', { name: /^remove$/i }));
    await waitFor(() => expect(removeTrack).toHaveBeenCalled());
    // The catch path runs synchronously after the rejected await; one more
    // microtask boundary is enough.
    await Promise.resolve();
    expect(onClose).not.toHaveBeenCalled();
  });

  test('success invalidates album, artist and home queries', async () => {
    (removeTrack as ReturnType<typeof vi.fn>).mockResolvedValue({
      deleted_track_id: 't1',
      deleted_album_id: 'a1',
      deleted_artist_id: 'ar1'
    });
    render(RemoveTrackPopover, { props: { track, onClose: vi.fn() } });
    await fireEvent.click(screen.getByRole('button', { name: /^remove$/i }));
    // Wait for all three baseline invalidations (album, artist, home) plus
    // the two cascade ones (deleted_album_id, deleted_artist_id) to land.
    await waitFor(() => expect(invalidateMock.mock.calls.length).toBeGreaterThanOrEqual(5));
    const keys = invalidateMock.mock.calls.map((c) => c[0].queryKey);
    const flat = keys.map((k) => Array.isArray(k) ? k.join('|') : String(k));
    expect(flat.some((k) => k.startsWith('album|a1'))).toBe(true);
    expect(flat.some((k) => k.startsWith('artist|ar1'))).toBe(true);
    expect(flat.some((k) => k === 'home')).toBe(true);
  });
});
