/**
 * Component: Requests Hooks Tests
 * Documentation: documentation/frontend/components.md
 */

// @vitest-environment jsdom

import React from 'react';
import { act, render, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const useAuthMock = vi.hoisted(() => vi.fn());
const useSWRMock = vi.hoisted(() => vi.fn());
const mutateMock = vi.hoisted(() => vi.fn());
const fetchWithAuthMock = vi.hoisted(() => vi.fn());

vi.mock('@/contexts/AuthContext', () => ({
  useAuth: () => useAuthMock(),
}));

vi.mock('swr', () => ({
  default: useSWRMock,
  mutate: mutateMock,
}));

vi.mock('@/lib/utils/api', () => ({
  fetchWithAuth: fetchWithAuthMock,
}));

const renderHookValue = <T,>(hook: () => T) => {
  let value: T;
  function Probe() {
    value = hook();
    return null;
  }
  render(<Probe />);
  return value!;
};

const renderHook = <T,>(hook: () => T) => {
  const result = { current: undefined as T };
  function Probe() {
    result.current = hook();
    return null;
  }
  render(<Probe />);
  return result;
};

const makeResponse = (body: any, ok = true) => ({
  ok,
  json: async () => body,
});

describe('useRequests hooks', () => {
  beforeEach(() => {
    useAuthMock.mockReset();
    useSWRMock.mockReset();
    mutateMock.mockReset();
    fetchWithAuthMock.mockReset();
    vi.resetModules();
  });

  it('builds request list endpoints when authenticated', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    useSWRMock.mockReturnValue({ data: { requests: [] }, error: null, isLoading: false });

    const { useRequests } = await import('@/lib/hooks/useRequests');

    renderHookValue(() => useRequests('pending', 25, true));

    expect(useSWRMock).toHaveBeenCalledWith(
      '/api/requests?status=pending&limit=25&myOnly=true',
      expect.any(Function),
      expect.objectContaining({ refreshInterval: 5000 })
    );
  });

  it('skips request list endpoints when unauthenticated', async () => {
    useAuthMock.mockReturnValue({ accessToken: null });
    useSWRMock.mockReturnValue({ data: null, error: null, isLoading: false });

    const { useRequests } = await import('@/lib/hooks/useRequests');

    renderHookValue(() => useRequests());

    expect(useSWRMock).toHaveBeenCalledWith(
      null,
      expect.any(Function),
      expect.objectContaining({ refreshInterval: 5000 })
    );
  });

  it('builds request detail endpoints when authenticated', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    useSWRMock.mockReturnValue({ data: { request: { id: 'req-1' } }, error: null, isLoading: false });

    const { useRequest } = await import('@/lib/hooks/useRequests');

    renderHookValue(() => useRequest('req-1'));

    expect(useSWRMock).toHaveBeenCalledWith(
      '/api/requests/req-1',
      expect.any(Function),
      expect.objectContaining({ refreshInterval: 3000 })
    );
  });

  it('creates requests and triggers revalidation', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-1' } }));

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useCreateRequest());

    await act(async () => {
      const result = await hook.createRequest({ asin: 'a1', title: 'Book', author: 'Author' } as any);
      expect(result.id).toBe('req-1');
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests',
      expect.objectContaining({ method: 'POST' })
    );
    expect(mutateMock).toHaveBeenCalled();
  });

  it('adds skipAutoSearch query params when creating requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-10' } }));

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useCreateRequest());

    await act(async () => {
      await result.current.createRequest(
        { asin: 'a10', title: 'Book', author: 'Author' } as any,
        { skipAutoSearch: true }
      );
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests?skipAutoSearch=true',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('throws when creating a request without authentication', async () => {
    useAuthMock.mockReturnValue({ accessToken: null });

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useCreateRequest());

    await expect(
      result.current.createRequest({ asin: 'a1', title: 'Book', author: 'Author' } as any)
    ).rejects.toThrow('Not authenticated');
  });

  it('surfaces specific create request errors', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ error: 'AlreadyAvailable' }, false));

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useCreateRequest());

    await act(async () => {
      await expect(
        hook.createRequest({ asin: 'a1', title: 'Book', author: 'Author' } as any)
      ).rejects.toThrow('already in your Plex library');
    });
  });

  it('surfaces being processed errors when creating requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ error: 'BeingProcessed' }, false));

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useCreateRequest());

    await act(async () => {
      await expect(
        result.current.createRequest({ asin: 'a2', title: 'Book', author: 'Author' } as any)
      ).rejects.toThrow('being processed');
    });

    await waitFor(() => {
      expect(result.current.error).toContain('being processed');
    });
  });

  it('surfaces API error messages when creating requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ message: 'Backend refused' }, false));

    const { useCreateRequest } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useCreateRequest());

    await act(async () => {
      await expect(
        result.current.createRequest({ asin: 'a3', title: 'Book', author: 'Author' } as any)
      ).rejects.toThrow('Backend refused');
    });

    await waitFor(() => {
      expect(result.current.error).toBe('Backend refused');
    });
  });

  it('cancels requests via the API', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-2' } }));

    const { useCancelRequest } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useCancelRequest());

    await act(async () => {
      await hook.cancelRequest('req-2');
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests/req-2',
      expect.objectContaining({ method: 'PATCH' })
    );
  });

  it('triggers manual search for requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-3' } }));

    const { useManualSearch } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useManualSearch());

    await act(async () => {
      await hook.triggerManualSearch('req-3');
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests/req-3/manual-search',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('captures API errors when triggering manual search', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ message: 'Manual search failed' }, false));

    const { useManualSearch } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useManualSearch());

    await act(async () => {
      await expect(result.current.triggerManualSearch('req-3')).rejects.toThrow('Manual search failed');
    });

    await waitFor(() => {
      expect(result.current.error).toBe('Manual search failed');
    });
  });

  it('searches torrents interactively for a request', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ results: [{ guid: 't1' }] }));

    const { useInteractiveSearch } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useInteractiveSearch());

    await act(async () => {
      const results = await hook.searchTorrents('req-4', 'Custom');
      expect(results).toHaveLength(1);
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests/req-4/interactive-search',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('reports interactive search errors', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ message: 'Search failed' }, false));

    const { useInteractiveSearch } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useInteractiveSearch());

    await act(async () => {
      await expect(result.current.searchTorrents('req-4')).rejects.toThrow('Search failed');
    });

    await waitFor(() => {
      expect(result.current.error).toBe('Search failed');
    });
  });

  it('selects torrents for existing requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-5' } }));

    const { useSelectTorrent } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useSelectTorrent());

    await act(async () => {
      await hook.selectTorrent('req-5', { title: 'Torrent' });
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/requests/req-5/select-torrent',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('searches torrents for new requests', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ results: [{ guid: 't2' }] }));

    const { useSearchTorrents } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useSearchTorrents());

    await act(async () => {
      const results = await hook.searchTorrents('Title', 'Author', 'asin');
      expect(results).toHaveLength(1);
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/audiobooks/search-torrents',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('requests torrents with audiobook payloads', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ request: { id: 'req-6' } }));

    const { useRequestWithTorrent } = await import('@/lib/hooks/useRequests');
    const hook = renderHookValue(() => useRequestWithTorrent());

    await act(async () => {
      await hook.requestWithTorrent({ asin: 'a1', title: 'Book', author: 'Author' } as any, { title: 'Torrent' });
    });

    expect(fetchWithAuthMock).toHaveBeenCalledWith(
      '/api/audiobooks/request-with-torrent',
      expect.objectContaining({ method: 'POST' })
    );
  });

  it('surfaces being processed errors when requesting with torrents', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ error: 'BeingProcessed' }, false));

    const { useRequestWithTorrent } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useRequestWithTorrent());

    await act(async () => {
      await expect(
        result.current.requestWithTorrent(
          { asin: 'a4', title: 'Book', author: 'Author' } as any,
          { title: 'Torrent' }
        )
      ).rejects.toThrow('being processed');
    });

    await waitFor(() => {
      expect(result.current.error).toContain('being processed');
    });
  });

  it('surfaces already available errors when requesting with torrents', async () => {
    useAuthMock.mockReturnValue({ accessToken: 'token' });
    fetchWithAuthMock.mockResolvedValueOnce(makeResponse({ error: 'AlreadyAvailable' }, false));

    const { useRequestWithTorrent } = await import('@/lib/hooks/useRequests');
    const result = renderHook(() => useRequestWithTorrent());

    await act(async () => {
      await expect(
        result.current.requestWithTorrent(
          { asin: 'a5', title: 'Book', author: 'Author' } as any,
          { title: 'Torrent' }
        )
      ).rejects.toThrow('already in your Plex library');
    });

    await waitFor(() => {
      expect(result.current.error).toContain('already in your Plex library');
    });
  });
});
