// src/hooks/mutations/useUpdateShoppingListItemMutation.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { useUpdateShoppingListItemMutation } from './useUpdateShoppingListItemMutation';
import * as apiClient from '../../services/apiClient';
import * as notificationService from '../../services/notificationService';

vi.mock('../../services/apiClient');
vi.mock('../../services/notificationService');

const mockedApiClient = vi.mocked(apiClient);
const mockedNotifications = vi.mocked(notificationService);

describe('useUpdateShoppingListItemMutation', () => {
  let queryClient: QueryClient;

  const wrapper = ({ children }: { children: ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  beforeEach(() => {
    vi.resetAllMocks();
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false },
      },
    });
  });

  it('should update a shopping list item successfully', async () => {
    const mockResponse = { id: 42, quantity: 3 };
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockResponse),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { quantity: 3 } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { quantity: 3 });
    expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Shopping list item updated');
  });

  it('should update is_purchased status', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 42, is_purchased: true }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { is_purchased: true } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { is_purchased: true });
  });

  it('should update custom_item_name', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 42, custom_item_name: 'Organic Milk' }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { custom_item_name: 'Organic Milk' } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, {
      custom_item_name: 'Organic Milk',
    });
  });

  it('should update notes', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 42, notes: 'Get the 2% variety' }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { notes: 'Get the 2% variety' } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, {
      notes: 'Get the 2% variety',
    });
  });

  it('should update multiple fields at once', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 42, quantity: 2, notes: 'Important' }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { quantity: 2, notes: 'Important' } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, {
      quantity: 2,
      notes: 'Important',
    });
  });

  it('should invalidate shopping-lists query on success', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 42 }),
    } as Response);

    const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { quantity: 5 } });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
  });

  it('should handle API error with error message', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: false,
      status: 404,
      json: () => Promise.resolve({ message: 'Item not found' }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 999, updates: { quantity: 1 } });

    await waitFor(() => expect(result.current.isError).toBe(true));

    expect(result.current.error?.message).toBe('Item not found');
    expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Item not found');
  });

  it('should handle API error when json parse fails', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: false,
      status: 500,
      json: () => Promise.reject(new Error('Parse error')),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 42, updates: { quantity: 1 } });

    await waitFor(() => expect(result.current.isError).toBe(true));

    expect(result.current.error?.message).toBe('Request failed with status 500');
  });

  it('should handle API error with empty message in response', async () => {
    mockedApiClient.updateShoppingListItem.mockResolvedValue({
      ok: false,
      status: 400,
      json: () => Promise.resolve({ message: '' }),
    } as Response);

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 99, updates: { notes: 'test' } });

    await waitFor(() => expect(result.current.isError).toBe(true));

    expect(result.current.error?.message).toBe('Failed to update shopping list item');
  });

  it('should use fallback error message when error has no message', async () => {
    mockedApiClient.updateShoppingListItem.mockRejectedValue(new Error(''));

    const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });

    result.current.mutate({ itemId: 77, updates: { is_purchased: true } });

    await waitFor(() => expect(result.current.isError).toBe(true));

    expect(mockedNotifications.notifyError).toHaveBeenCalledWith(
      'Failed to update shopping list item',
    );
  });
});
