// src/features/shopping/WatchedItemsList.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { WatchedItemsList } from './WatchedItemsList';
import type { MasterGroceryItem } from '../../types';
import { logger } from '../../services/logger.client';
import { createMockMasterGroceryItem, createMockUser } from '../../tests/utils/mockFactories';

// Mock the logger to spy on error calls
vi.mock('../../services/logger.client');

const mockUser = createMockUser({ user_id: 'user-123', email: 'test@example.com' });

const mockItems: MasterGroceryItem[] = [
  createMockMasterGroceryItem({
    master_grocery_item_id: 1,
    name: 'Apples',
    category_id: 1,
    category_name: 'Produce',
  }),
  createMockMasterGroceryItem({
    master_grocery_item_id: 2,
    name: 'Milk',
    category_id: 2,
    category_name: 'Dairy',
  }),
  createMockMasterGroceryItem({
    master_grocery_item_id: 3,
    name: 'Bread',
    category_id: 3,
    category_name: 'Bakery',
  }),
  createMockMasterGroceryItem({
    master_grocery_item_id: 4,
    name: 'Eggs',
    category_id: 2,
    category_name: 'Dairy',
  }),
];

const mockOnAddItem = vi.fn();
const mockOnRemoveItem = vi.fn();
const mockOnAddItemToList = vi.fn();

const defaultProps = {
  items: mockItems,
  onAddItem: mockOnAddItem,
  onRemoveItem: mockOnRemoveItem,
  user: mockUser,
  activeListId: 1,
  onAddItemToList: mockOnAddItemToList,
};

describe('WatchedItemsList (in shopping feature)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockOnAddItem.mockResolvedValue(undefined);
    mockOnRemoveItem.mockResolvedValue(undefined);
  });

  it('should render a login message when user is not authenticated', () => {
    render(<WatchedItemsList {...defaultProps} user={null} />);
    expect(
      screen.getByText(/please log in to create and manage your personal watchlist/i),
    ).toBeInTheDocument();
    expect(screen.queryByRole('form')).not.toBeInTheDocument();
  });

  it('should render the form and item list when user is authenticated', () => {
    render(<WatchedItemsList {...defaultProps} />);
    expect(screen.getByPlaceholderText(/add item/i)).toBeInTheDocument();
    expect(screen.getByRole('combobox', { name: /filter by category/i })).toBeInTheDocument();
    expect(screen.getByText('Apples')).toBeInTheDocument();
    expect(screen.getByText('Milk')).toBeInTheDocument();
    expect(screen.getByText('Bread')).toBeInTheDocument();
  });

  it('should allow adding a new item', async () => {
    render(<WatchedItemsList {...defaultProps} />);

    fireEvent.change(screen.getByPlaceholderText(/add item/i), { target: { value: 'Cheese' } });
    // Use getByDisplayValue to reliably select the category dropdown, which has no label.
    // Also, use the correct category name from the CATEGORIES constant.
    const categorySelect = screen.getByDisplayValue('Select a category');
    fireEvent.change(categorySelect, { target: { value: 'Dairy & Eggs' } });

    fireEvent.submit(screen.getByRole('button', { name: 'Add' }));

    await waitFor(() => {
      expect(mockOnAddItem).toHaveBeenCalledWith('Cheese', 'Dairy & Eggs');
    });

    // Check if form resets
    expect(screen.getByPlaceholderText(/add item/i)).toHaveValue('');
  });

  it('should show a loading spinner while adding an item', async () => {
    // Create a promise that we can resolve manually to control the loading state
    let resolvePromise: (value: void | PromiseLike<void>) => void;
    const mockPromise = new Promise<void>((resolve) => {
      resolvePromise = resolve;
    });
    mockOnAddItem.mockImplementation(() => mockPromise);

    render(<WatchedItemsList {...defaultProps} />);

    fireEvent.change(screen.getByPlaceholderText(/add item/i), { target: { value: 'Cheese' } });
    fireEvent.change(screen.getByDisplayValue('Select a category'), {
      target: { value: 'Dairy & Eggs' },
    });
    const addButton = screen.getByRole('button', { name: 'Add' });
    fireEvent.click(addButton);

    // The button text is replaced by the spinner, so we use the captured reference
    await waitFor(() => {
      expect(addButton).toBeDisabled();
    });
    expect(addButton.querySelector('.animate-spin')).toBeInTheDocument();

    // Resolve the promise to complete the async operation and allow the test to finish
    await act(async () => {
      resolvePromise();
      await mockPromise;
    });
  });

  it('should allow removing an item', async () => {
    render(<WatchedItemsList {...defaultProps} />);
    const removeButton = screen.getByRole('button', { name: /remove apples/i });
    fireEvent.click(removeButton);

    await waitFor(() => {
      expect(mockOnRemoveItem).toHaveBeenCalledWith(1); // ID for Apples is 1
    });
  });

  it('should filter items by category', () => {
    render(<WatchedItemsList {...defaultProps} />);
    const categoryFilter = screen.getByRole('combobox', { name: /filter by category/i });

    fireEvent.change(categoryFilter, { target: { value: 'Dairy' } });

    expect(screen.getByText('Milk')).toBeInTheDocument();
    expect(screen.queryByText('Apples')).not.toBeInTheDocument();
    expect(screen.queryByText('Bread')).not.toBeInTheDocument();
  });

  it('should sort items ascending and descending', () => {
    render(<WatchedItemsList {...defaultProps} />);
    const sortButton = screen.getByRole('button', { name: /sort items descending/i });

    const itemsAsc = screen.getAllByRole('listitem');
    expect(itemsAsc[0]).toHaveTextContent('Apples');
    expect(itemsAsc[1]).toHaveTextContent('Bread'); // This was a duplicate, fixed.
    expect(itemsAsc[2]).toHaveTextContent('Eggs');
    expect(itemsAsc[3]).toHaveTextContent('Milk');

    // Click to sort descending
    fireEvent.click(sortButton);

    const itemsDesc = screen.getAllByRole('listitem');
    expect(itemsDesc[0]).toHaveTextContent('Milk');
    expect(itemsDesc[1]).toHaveTextContent('Eggs');
    expect(itemsDesc[2]).toHaveTextContent('Bread');
    expect(itemsDesc[3]).toHaveTextContent('Apples');

    // Click again to sort ascending
    fireEvent.click(sortButton);

    const itemsAscAgain = screen.getAllByRole('listitem');
    expect(itemsAscAgain[0]).toHaveTextContent('Apples');
    expect(itemsAscAgain[1]).toHaveTextContent('Bread');
    expect(itemsAscAgain[2]).toHaveTextContent('Eggs');
    expect(itemsAscAgain[3]).toHaveTextContent('Milk');
  });

  it('should call onAddItemToList when plus icon is clicked', () => {
    render(<WatchedItemsList {...defaultProps} />);
    const addToListButton = screen.getByTitle('Add Apples to list');
    fireEvent.click(addToListButton);
    expect(mockOnAddItemToList).toHaveBeenCalledWith(1); // ID for Apples
  });

  it('should disable the add to list button if activeListId is null', () => {
    render(<WatchedItemsList {...defaultProps} activeListId={null} />);
    // Multiple buttons will have this title, so we must use `getAllByTitle`.
    const addToListButtons = screen.getAllByTitle('Select a shopping list first');
    // Assert that at least one such button exists and that they are all disabled.
    expect(addToListButtons.length).toBeGreaterThan(0);
    addToListButtons.forEach((button) => expect(button).toBeDisabled());
  });

  it('should display a message when the list is empty', () => {
    render(<WatchedItemsList {...defaultProps} items={[]} />);
    expect(screen.getByText(/your watchlist is empty/i)).toBeInTheDocument();
  });

  describe('Form Validation and Disabled States', () => {
    it('should disable the "Add" button if item name is empty or whitespace', () => {
      render(<WatchedItemsList {...defaultProps} />);
      const nameInput = screen.getByPlaceholderText(/add item/i);
      const categorySelect = screen.getByDisplayValue('Select a category');
      const addButton = screen.getByRole('button', { name: 'Add' });

      // Initially disabled
      expect(addButton).toBeDisabled();

      // With category but no name
      fireEvent.change(categorySelect, { target: { value: 'Fruits & Vegetables' } });
      expect(addButton).toBeDisabled();

      // With whitespace name
      fireEvent.change(nameInput, { target: { value: '   ' } });
      expect(addButton).toBeDisabled();

      // With valid name
      fireEvent.change(nameInput, { target: { value: 'Grapes' } });
      expect(addButton).toBeEnabled();
    });

    it('should disable the "Add" button if category is not selected', () => {
      render(<WatchedItemsList {...defaultProps} />);
      const nameInput = screen.getByPlaceholderText(/add item/i);
      const addButton = screen.getByRole('button', { name: 'Add' });

      // Initially disabled
      expect(addButton).toBeDisabled();

      // With name but no category
      fireEvent.change(nameInput, { target: { value: 'Grapes' } });
      expect(addButton).toBeDisabled();
    });

    it('should not submit if form is submitted with invalid data', () => {
      render(<WatchedItemsList {...defaultProps} />);
      const nameInput = screen.getByPlaceholderText(/add item/i);
      const form = nameInput.closest('form')!;
      const categorySelect = screen.getByDisplayValue('Select a category');
      fireEvent.change(categorySelect, { target: { value: 'Dairy & Eggs' } });

      fireEvent.change(nameInput, { target: { value: '   ' } });
      fireEvent.submit(form);
      expect(mockOnAddItem).not.toHaveBeenCalled();
    });
  });

  describe('Error Handling', () => {
    it('should reset loading state and log an error if onAddItem rejects', async () => {
      const apiError = new Error('Item already exists');
      mockOnAddItem.mockRejectedValue(apiError);
      const loggerSpy = vi.spyOn(logger, 'error');

      render(<WatchedItemsList {...defaultProps} />);

      const nameInput = screen.getByPlaceholderText(/add item/i);
      const categorySelect = screen.getByDisplayValue('Select a category');
      const addButton = screen.getByRole('button', { name: 'Add' });

      fireEvent.change(nameInput, { target: { value: 'Duplicate Item' } });
      fireEvent.change(categorySelect, { target: { value: 'Fruits & Vegetables' } });
      fireEvent.click(addButton);

      // After the promise rejects, the button should be enabled again
      await waitFor(() => expect(addButton).toBeEnabled());

      // And the error should be logged
      expect(loggerSpy).toHaveBeenCalledWith('Failed to add watched item from WatchedItemsList', {
        error: apiError,
      });
    });
  });

  describe('UI Edge Cases', () => {
    it('should display a specific message when a filter results in no items', () => {
      const { rerender } = render(<WatchedItemsList {...defaultProps} />);
      const categoryFilter = screen.getByRole('combobox', { name: /filter by category/i });

      // Select 'Produce' - 'Apples' should be visible
      fireEvent.change(categoryFilter, { target: { value: 'Produce' } });
      expect(screen.getByText('Apples')).toBeInTheDocument();

      // Rerender with the 'Produce' item removed, but keep the filter active
      const itemsWithoutProduce = mockItems.filter((item) => item.category_name !== 'Produce');
      rerender(<WatchedItemsList {...defaultProps} items={itemsWithoutProduce} />);

      // Now the message should appear for the 'Produce' category
      expect(screen.getByText('No watched items in the "Produce" category.')).toBeInTheDocument();
    });

    it('should hide the sort button if there is only one item', () => {
      render(<WatchedItemsList {...defaultProps} items={[mockItems[0]]} />);
      expect(screen.queryByRole('button', { name: /sort items/i })).not.toBeInTheDocument();
    });
  });
});
