import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MediaBrowseScreen } from './MediaBrowseScreen';

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json' }, status });
}

const items = [
  { artwork: '', collectionType: 'Movie', id: 1, mediaType: 'Movie', title: 'Blade Runner' },
  { artwork: '', collectionType: 'Movie', id: 2, mediaType: 'Movie', title: 'Alien' }
];

function mockFetch() {
  return vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
    const url = input.toString();
    if (url.startsWith('/api/v1/library/browse')) {
      return Promise.resolve(jsonResponse({ page: items, totalCount: items.length }));
    }
    if (url === '/api/v1/collections') {
      return Promise.resolve(jsonResponse([{ collectionType: 'Collection', id: 7, name: 'Favorites', useCustomPlaybackOrder: false }]));
    }
    if (url === '/api/v1/playlists/groups') {
      return Promise.resolve(jsonResponse([]));
    }
    if (url === '/api/v1/schedules') {
      return Promise.resolve(jsonResponse([]));
    }
    return Promise.resolve(new Response(null, { status: 204 }));
  });
}

describe('MediaBrowseScreen', () => {
  beforeEach(() => {
    window.localStorage.clear();
    window.history.replaceState(null, '', '/app/media?kind=movies');
    vi.restoreAllMocks();
  });

  afterEach(() => {
    cleanup();
  });

  it('renders a per-tile Add-to menu on every card', async () => {
    mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());
    expect(screen.getAllByRole('button', { name: 'Add to…' })).toHaveLength(items.length);
  });

  it('supports select mode and adds the selected items to a collection', async () => {
    const fetchSpy = mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());

    // No selection bar until we enter select mode and pick something.
    expect(screen.queryByText(/selected/)).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    fireEvent.click(screen.getByText('Blade Runner'));
    expect(screen.getByText('1 selected')).toBeInTheDocument();

    fireEvent.click(screen.getByText('Alien'));
    expect(screen.getByText('2 selected')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: /Add to collection/ }));
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());

    // The first existing collection (Favorites, id 7) is preselected — just submit.
    await waitFor(() => expect(screen.getByRole('option', { name: 'Favorites' })).toBeTruthy());
    fireEvent.click(screen.getAllByRole('button', { name: /Add to collection/ }).at(-1)!);

    await waitFor(() => {
      const addCall = fetchSpy.mock.calls.find(
        ([url, init]) => String(url) === '/api/v1/collections/7/items' && (init as RequestInit | undefined)?.method === 'POST'
      );
      expect(addCall).toBeTruthy();
      const body = JSON.parse(String((addCall![1] as RequestInit).body));
      expect(body.movieIds).toEqual([1, 2]);
    });
  });

  it('shows a success toast after a bulk add', async () => {
    mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());
    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    fireEvent.click(screen.getByText('Blade Runner'));
    fireEvent.click(screen.getByRole('button', { name: /Add to collection/ }));

    await waitFor(() => expect(screen.getByRole('option', { name: 'Favorites' })).toBeTruthy());
    fireEvent.click(screen.getAllByRole('button', { name: /Add to collection/ }).at(-1)!);

    await waitFor(() => expect(screen.getByText('Added to “Favorites”')).toBeInTheDocument());
    // Selection cleared alongside the toast.
    expect(screen.queryByText(/selected/)).toBeNull();
  });

  it('hides the per-tile Add-to menu while select mode is on', async () => {
    mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());
    expect(screen.getAllByRole('button', { name: 'Add to…' })).toHaveLength(items.length);

    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    expect(screen.queryByRole('button', { name: 'Add to…' })).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: 'Done' }));
    expect(screen.getAllByRole('button', { name: 'Add to…' })).toHaveLength(items.length);
  });

  it('offers a Seasons kind that browses TelevisionSeason items', async () => {
    const fetchSpy = mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());
    expect(screen.getByRole('option', { name: 'Seasons' })).toBeTruthy();

    fireEvent.change(screen.getByRole('combobox'), { target: { value: 'seasons' } });

    await waitFor(() => {
      const seasonCall = fetchSpy.mock.calls.find(([url]) =>
        String(url).startsWith('/api/v1/library/browse') && String(url).includes('mediaType=TelevisionSeason')
      );
      expect(seasonCall).toBeTruthy();
    });
  });

  it('gates old-kind cards and shows a refreshing cue during a kind-change refetch (issue #221)', async () => {
    function deferred<T>() {
      let resolve!: (value: T) => void;
      const promise = new Promise<T>((r) => {
        resolve = r;
      });
      return { promise, resolve };
    }

    const browseDeferreds: Array<{ resolve: (body: unknown) => void }> = [];
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        const d = deferred<Response>();
        browseDeferreds.push({ resolve: (body) => d.resolve(jsonResponse(body)) });
        return d.promise;
      }
      if (url === '/api/v1/collections') {
        return Promise.resolve(jsonResponse([{ collectionType: 'Collection', id: 7, name: 'Favorites', useCustomPlaybackOrder: false }]));
      }
      if (url === '/api/v1/playlists/groups') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(new Response(null, { status: 204 }));
    });

    render(<MediaBrowseScreen />);
    browseDeferreds[0].resolve({ page: items, totalCount: items.length });
    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());
    expect(screen.getAllByRole('button', { name: 'Add to…' })).toHaveLength(items.length);

    // Change kind: the refetch for the new kind is held pending while old-kind cards stay rendered.
    fireEvent.change(screen.getByRole('combobox'), { target: { value: 'shows' } });
    await waitFor(() => expect(browseDeferreds.length).toBe(2));

    // Refreshing window: cue visible, mutation surfaces gated, old-kind cards still visible but inert.
    await waitFor(() => expect(screen.getByText('Refreshing…')).toBeInTheDocument());
    expect(screen.getByText('Blade Runner')).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: 'Add to…' })).toBeNull();
    expect(screen.getByRole('button', { name: 'Select' })).toBeDisabled();

    // New-kind results resolve: controls re-enable, cue gone.
    browseDeferreds[1].resolve({ page: items, totalCount: items.length });
    await waitFor(() => expect(screen.queryByText('Refreshing…')).not.toBeInTheDocument());
    expect(screen.getAllByRole('button', { name: 'Add to…' })).toHaveLength(items.length);
  });

  it('keeps a select-mode card inert (no select, no navigate) while refreshing (#222 review)', async () => {
    function deferred<T>() {
      let resolve!: (value: T) => void;
      const promise = new Promise<T>((r) => {
        resolve = r;
      });
      return { promise, resolve };
    }

    const browseDeferreds: Array<{ resolve: (body: unknown) => void }> = [];
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        const d = deferred<Response>();
        browseDeferreds.push({ resolve: (body) => d.resolve(jsonResponse(body)) });
        return d.promise;
      }
      if (url === '/api/v1/collections') {
        return Promise.resolve(jsonResponse([{ collectionType: 'Collection', id: 7, name: 'Favorites', useCustomPlaybackOrder: false }]));
      }
      if (url === '/api/v1/playlists/groups') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(new Response(null, { status: 204 }));
    });

    render(<MediaBrowseScreen />);
    browseDeferreds[0].resolve({ page: items, totalCount: items.length });
    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());

    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    fireEvent.change(screen.getByRole('combobox'), { target: { value: 'shows' } });
    await waitFor(() => expect(browseDeferreds.length).toBe(2));
    await waitFor(() => expect(screen.getByText('Refreshing…')).toBeInTheDocument());

    const pushState = vi.spyOn(window.history, 'pushState');
    fireEvent.click(screen.getByText('Blade Runner'));

    // Neither selection nor navigation fires — the card is fully inert during select+refresh.
    expect(screen.queryByText(/selected/)).toBeNull();
    expect(pushState).not.toHaveBeenCalled();
  });

  it('select toggle: entering select mode is blocked while refreshing, exiting (Done) is allowed (#222 review)', async () => {
    function deferred<T>() {
      let resolve!: (value: T) => void;
      const promise = new Promise<T>((r) => {
        resolve = r;
      });
      return { promise, resolve };
    }

    const browseDeferreds: Array<{ resolve: (body: unknown) => void }> = [];
    vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
      const url = input.toString();
      if (url.startsWith('/api/v1/library/browse')) {
        const d = deferred<Response>();
        browseDeferreds.push({ resolve: (body) => d.resolve(jsonResponse(body)) });
        return d.promise;
      }
      if (url === '/api/v1/collections') {
        return Promise.resolve(jsonResponse([{ collectionType: 'Collection', id: 7, name: 'Favorites', useCustomPlaybackOrder: false }]));
      }
      if (url === '/api/v1/playlists/groups') {
        return Promise.resolve(jsonResponse([]));
      }
      return Promise.resolve(new Response(null, { status: 204 }));
    });

    render(<MediaBrowseScreen />);
    browseDeferreds[0].resolve({ page: items, totalCount: items.length });
    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());

    // Not in select mode; trigger a refetch — entering select mode must be blocked.
    fireEvent.change(screen.getByRole('combobox'), { target: { value: 'shows' } });
    await waitFor(() => expect(browseDeferreds.length).toBe(2));
    await waitFor(() => expect(screen.getByText('Refreshing…')).toBeInTheDocument());
    expect(screen.getByRole('button', { name: 'Select' })).toBeDisabled();

    // Settle, enter select mode, then trigger another refetch — exiting (Done) must stay enabled.
    browseDeferreds[1].resolve({ page: items, totalCount: items.length });
    await waitFor(() => expect(screen.queryByText('Refreshing…')).not.toBeInTheDocument());

    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    expect(screen.getByRole('button', { name: 'Done' })).toBeTruthy();

    fireEvent.change(screen.getByRole('combobox'), { target: { value: 'movies' } });
    await waitFor(() => expect(browseDeferreds.length).toBe(3));
    await waitFor(() => expect(screen.getByText('Refreshing…')).toBeInTheDocument());

    const doneToggle = screen.getByRole('button', { name: 'Done' });
    expect(doneToggle).not.toBeDisabled();
    fireEvent.click(doneToggle);
    expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
  });

  it('selects every loaded item with Select all on page', async () => {
    mockFetch();
    render(<MediaBrowseScreen />);

    await waitFor(() => expect(screen.getByText('Blade Runner')).toBeInTheDocument());

    fireEvent.click(screen.getByRole('button', { name: 'Select' }));
    fireEvent.click(screen.getByText('Blade Runner'));
    fireEvent.click(screen.getByRole('button', { name: 'Select all on page' }));
    expect(screen.getByText('2 selected')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: 'Clear selection' }));
    expect(screen.queryByText(/selected/)).toBeNull();
  });
});
