// src/features/shopping/ShoppingList.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { ShoppingListComponent } from './ShoppingList'; // This path is now relative to the new folder
import type { ShoppingList } from '../../types';
import * as aiApiClient from '../../services/aiApiClient';
import {
  createMockShoppingList,
  createMockShoppingListItem,
  createMockUser,
} from '../../tests/utils/mockFactories';
// The logger and aiApiClient are now mocked globally.
// Mock the AI API client (relative to new location)
// We will spy on the function directly in the test instead of mocking the whole module.

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

const mockLists: ShoppingList[] = [
  createMockShoppingList({
    shopping_list_id: 1,
    name: 'Weekly Groceries',
    user_id: 'user-123',
    items: [
      createMockShoppingListItem({
        shopping_list_item_id: 101,
        shopping_list_id: 1,
        master_item_id: 1,
        custom_item_name: null,
        master_item: { name: 'Apples' },
      }),
      createMockShoppingListItem({
        shopping_list_item_id: 102,
        shopping_list_id: 1,
        master_item_id: null,
        custom_item_name: 'Special Bread',
        master_item: null,
      }),
      createMockShoppingListItem({
        shopping_list_item_id: 103,
        shopping_list_id: 1,
        master_item_id: 2,
        is_purchased: true,
        custom_item_name: null,
        master_item: { name: 'Milk' },
      }),
      createMockShoppingListItem({
        shopping_list_item_id: 104,
        shopping_list_id: 1,
        master_item_id: null,
        custom_item_name: null,
        master_item: null,
      }), // Item with no name
    ],
  }),
  createMockShoppingList({
    shopping_list_id: 2,
    name: 'Party Supplies',
    user_id: 'user-123',
    items: [],
  }),
];

describe('ShoppingListComponent (in shopping feature)', () => {
  const mockOnSelectList = vi.fn();
  const mockOnCreateList = vi.fn();
  const mockOnDeleteList = vi.fn();
  const mockOnAddItem = vi.fn();
  const mockOnUpdateItem = vi.fn();
  const mockOnRemoveItem = vi.fn();

  const defaultProps = {
    user: mockUser,
    lists: mockLists,
    activeListId: 1,
    onSelectList: mockOnSelectList,
    onCreateList: mockOnCreateList,
    onDeleteList: mockOnDeleteList,
    onAddItem: mockOnAddItem,
    onUpdateItem: mockOnUpdateItem,
    onRemoveItem: mockOnRemoveItem,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    // Mock browser APIs
    window.prompt = vi.fn();
    window.confirm = vi.fn();
    window.AudioContext = vi.fn().mockImplementation(() => ({
      decodeAudioData: vi.fn().mockResolvedValue({}),
      createBufferSource: vi.fn().mockReturnValue({
        connect: vi.fn(),
        start: vi.fn(),
        buffer: {},
      }),
      close: vi.fn(),
      sampleRate: 44100,
    }));
  });

  // Restore all mocks after each test to ensure test isolation.
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should render a login message when user is not authenticated', () => {
    render(<ShoppingListComponent {...defaultProps} user={null} />);
    expect(screen.getByText(/please log in to manage your shopping lists/i)).toBeInTheDocument();
  });

  it('should render correctly when authenticated with an active list', () => {
    render(<ShoppingListComponent {...defaultProps} />);
    expect(screen.getByRole('heading', { name: /shopping list/i })).toBeInTheDocument();
    expect(screen.getByRole('combobox')).toHaveValue('1');
    expect(screen.getByText('Apples')).toBeInTheDocument();
    expect(screen.getByText('Special Bread')).toBeInTheDocument();
    expect(screen.getByText('Milk')).toBeInTheDocument(); // Purchased item
    expect(screen.getByRole('heading', { name: /purchased/i })).toBeInTheDocument();
  });

  it('should display a message if there are no lists', () => {
    render(<ShoppingListComponent {...defaultProps} lists={[]} activeListId={null} />);
    expect(screen.getByText(/no shopping lists found/i)).toBeInTheDocument();
  });

  it('should call onSelectList when changing the list in the dropdown', () => {
    render(<ShoppingListComponent {...defaultProps} />);
    fireEvent.change(screen.getByRole('combobox'), { target: { value: '2' } });
    expect(mockOnSelectList).toHaveBeenCalledWith(2);
  });

  it('should call onCreateList when creating a new list', async () => {
    (window.prompt as Mock).mockReturnValue('New List Name');
    render(<ShoppingListComponent {...defaultProps} />);
    fireEvent.click(screen.getByRole('button', { name: /new list/i }));
    await waitFor(() => {
      expect(mockOnCreateList).toHaveBeenCalledWith('New List Name');
    });
  });

  it('should not call onCreateList if prompt is cancelled', async () => {
    (window.prompt as Mock).mockReturnValue(null);
    render(<ShoppingListComponent {...defaultProps} />);
    fireEvent.click(screen.getByRole('button', { name: /new list/i }));
    await waitFor(() => {
      expect(mockOnCreateList).not.toHaveBeenCalled();
    });
  });

  it('should call onDeleteList when deleting a list after confirmation', async () => {
    (window.confirm as Mock).mockReturnValue(true);
    render(<ShoppingListComponent {...defaultProps} />);
    fireEvent.click(screen.getByRole('button', { name: /delete list/i }));
    await waitFor(() => {
      expect(window.confirm).toHaveBeenCalled();
      expect(mockOnDeleteList).toHaveBeenCalledWith(1);
    });
  });

  it('should not call onDeleteList if deletion is not confirmed', async () => {
    (window.confirm as Mock).mockReturnValue(false);
    render(<ShoppingListComponent {...defaultProps} />);
    fireEvent.click(screen.getByRole('button', { name: /delete list/i }));
    await waitFor(() => {
      expect(window.confirm).toHaveBeenCalled();
      expect(mockOnDeleteList).not.toHaveBeenCalled();
    });
  });

  it('should call onAddItem when adding a custom item', async () => {
    render(<ShoppingListComponent {...defaultProps} />);
    const input = screen.getByPlaceholderText(/add a custom item/i);
    const addButton = screen.getByRole('button', { name: 'Add' });

    fireEvent.change(input, { target: { value: 'New Custom Item' } });
    fireEvent.click(addButton);

    // Assert that the callback was called with the correct value.
    expect(mockOnAddItem).toHaveBeenCalledWith({ customItemName: 'New Custom Item' });

    // Also, verify that the input field was cleared after submission.
    await waitFor(() => {
      expect(input).toHaveValue('');
    });
  });

  it('should call onUpdateItem when toggling an item checkbox', () => {
    render(<ShoppingListComponent {...defaultProps} />);
    const checkboxes = screen.getAllByRole('checkbox');
    const appleCheckbox = checkboxes[0]; // 'Apples' is the first item

    fireEvent.click(appleCheckbox);
    expect(mockOnUpdateItem).toHaveBeenCalledWith(101, { is_purchased: true });
  });

  it('should call onRemoveItem when clicking the remove button', () => {
    render(<ShoppingListComponent {...defaultProps} />);
    // Items are in a div, not listitem. We find the button near the item text.
    const appleItem = screen.getByText('Apples').closest('div');
    const removeButton = appleItem!.querySelector('button')!;

    fireEvent.click(removeButton);
    expect(mockOnRemoveItem).toHaveBeenCalledWith(101);
  });

  it('should call generateSpeechFromText when "Read aloud" is clicked', async () => {
    const generateSpeechSpy = vi.spyOn(aiApiClient, 'generateSpeechFromText').mockResolvedValue({
      json: () => Promise.resolve('base64-audio-string'),
    } as Response);

    render(<ShoppingListComponent {...defaultProps} />);
    const readAloudButton = screen.getByTitle(/read list aloud/i);

    fireEvent.click(readAloudButton);

    await waitFor(() => {
      expect(generateSpeechSpy).toHaveBeenCalledWith(
        'Here is your shopping list: Apples, Special Bread',
      );
    });
  });

  it('should show an alert if reading aloud fails', async () => {
    const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
    vi.spyOn(aiApiClient, 'generateSpeechFromText').mockRejectedValue(new Error('AI API is down'));

    render(<ShoppingListComponent {...defaultProps} />);
    const readAloudButton = screen.getByTitle(/read list aloud/i);

    fireEvent.click(readAloudButton);

    await waitFor(() => {
      expect(alertSpy).toHaveBeenCalledWith('Could not read list aloud: AI API is down');
    });

    alertSpy.mockRestore();
  });

  it('should show a generic alert if reading aloud fails with a non-Error object', async () => {
    const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
    vi.spyOn(aiApiClient, 'generateSpeechFromText').mockRejectedValue('A string error');

    render(<ShoppingListComponent {...defaultProps} />);
    const readAloudButton = screen.getByTitle(/read list aloud/i);

    fireEvent.click(readAloudButton);

    await waitFor(() => {
      expect(alertSpy).toHaveBeenCalledWith(
        'Could not read list aloud: An unknown error occurred while generating audio.',
      );
    });

    alertSpy.mockRestore();
  });

  it('should handle interactions with purchased items', () => {
    render(<ShoppingListComponent {...defaultProps} />);

    // Find the purchased item 'Milk'
    const milkItem = screen.getByText('Milk').closest('div');
    expect(milkItem).toBeInTheDocument();

    // Test un-checking the item
    const checkbox = milkItem!.querySelector('input[type="checkbox"]');
    expect(checkbox).toBeChecked();
    fireEvent.click(checkbox!);
    expect(mockOnUpdateItem).toHaveBeenCalledWith(103, { is_purchased: false });

    // Test removing the item
    const removeButton = milkItem!.querySelector('button');
    expect(removeButton).toBeInTheDocument();
    fireEvent.click(removeButton!);
    expect(mockOnRemoveItem).toHaveBeenCalledWith(103);
  });

  describe('Loading States and Disabled States', () => {
    it('should disable the "Add" button for custom items when input is empty or whitespace', () => {
      render(<ShoppingListComponent {...defaultProps} />);
      const input = screen.getByPlaceholderText(/add a custom item/i);
      const addButton = screen.getByRole('button', { name: 'Add' });

      expect(addButton).toBeDisabled();

      fireEvent.change(input, { target: { value: '   ' } });
      expect(addButton).toBeDisabled();

      fireEvent.change(input, { target: { value: 'Something' } });
      expect(addButton).toBeEnabled();
    });

    it('should show a loading spinner while adding a custom item', async () => {
      let resolvePromise: (value: void | PromiseLike<void>) => void;
      const mockPromise = new Promise<void>((resolve) => {
        resolvePromise = resolve;
      });
      mockOnAddItem.mockReturnValue(mockPromise);

      render(<ShoppingListComponent {...defaultProps} />);
      const input = screen.getByPlaceholderText(/add a custom item/i);
      const addButton = screen.getByRole('button', { name: 'Add' });

      fireEvent.change(input, { target: { value: 'Loading Item' } });
      fireEvent.click(addButton);

      await waitFor(() => {
        expect(addButton).toBeDisabled();
        expect(addButton.querySelector('.animate-spin')).toBeInTheDocument();
      });

      // Resolve promise to avoid test warnings
      await act(async () => {
        resolvePromise();
        await mockPromise;
      });
    });

    it('should show a loading spinner while creating a new list', async () => {
      let resolvePromise: (value: void | PromiseLike<void>) => void;
      const mockPromise = new Promise<void>((resolve) => {
        resolvePromise = resolve;
      });
      mockOnCreateList.mockReturnValue(mockPromise);
      (window.prompt as Mock).mockReturnValue('New List');

      render(<ShoppingListComponent {...defaultProps} />);
      const newListButton = screen.getByRole('button', { name: /new list/i });
      fireEvent.click(newListButton);

      await waitFor(() => {
        expect(newListButton).toBeDisabled();
      });

      // Resolve promise to avoid test warnings
      await act(async () => {
        resolvePromise();
        await mockPromise;
      });
    });

    it('should show a loading spinner while reading the list aloud', async () => {
      let resolvePromise: (value: Response | PromiseLike<Response>) => void;
      const mockPromise = new Promise<Response>((resolve) => {
        resolvePromise = resolve;
      });
      vi.spyOn(aiApiClient, 'generateSpeechFromText').mockReturnValue(mockPromise);

      render(<ShoppingListComponent {...defaultProps} />);
      const readAloudButton = screen.getByTitle(/read list aloud/i);
      fireEvent.click(readAloudButton);

      await waitFor(() => {
        expect(readAloudButton).toBeDisabled();
        expect(readAloudButton.querySelector('.animate-spin')).toBeInTheDocument();
      });

      // Resolve promise to avoid test warnings
      await act(async () => {
        resolvePromise({ json: () => Promise.resolve('audio') } as Response);
        await mockPromise;
      });
    });

    it('should disable the "Read aloud" button if there are no items to read', () => {
      const listWithOnlyPurchasedItems: ShoppingList[] = [
        createMockShoppingList({
          shopping_list_id: 1,
          name: 'Weekly Groceries',
          user_id: 'user-123',
          items: [mockLists[0].items[2]], // Only the purchased 'Milk' item
        }),
      ];
      render(<ShoppingListComponent {...defaultProps} lists={listWithOnlyPurchasedItems} />);
      expect(screen.getByTitle(/read list aloud/i)).toBeDisabled();
    });
  });

  describe('UI Edge Cases', () => {
    it('should not call onCreateList if the prompt returns an empty or whitespace string', async () => {
      (window.prompt as Mock).mockReturnValue('   ');
      render(<ShoppingListComponent {...defaultProps} />);
      fireEvent.click(screen.getByRole('button', { name: /new list/i }));
      await waitFor(() => {
        expect(mockOnCreateList).not.toHaveBeenCalled();
      });
    });

    it('should display a message for an active list with no items', () => {
      render(<ShoppingListComponent {...defaultProps} activeListId={2} />); // Party Supplies list is empty
      expect(screen.getByText('This list is empty.')).toBeInTheDocument();
    });

    it('should render an item gracefully if it has no custom name or master item name', () => {
      render(<ShoppingListComponent {...defaultProps} />);
      // The item with ID 104 has no name. We find its checkbox to confirm it rendered.
      const checkboxes = screen.getAllByRole('checkbox');
      // Apples, Special Bread, Nameless Item, Milk (purchased)
      expect(checkboxes).toHaveLength(4);
      const namelessItemCheckbox = checkboxes[2];
      // The span next to it should be empty
      expect(namelessItemCheckbox.nextElementSibling).toHaveTextContent('');
    });
  });
});
