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

const { mockUseApi } = vi.hoisted(() => ({
  mockUseApi: vi.fn(),
}));

const { mockListProducts, mockDeleteProduct, mockCreateProduct, mockUpdateProduct } = vi.hoisted(() => ({
  mockListProducts: vi.fn(),
  mockDeleteProduct: vi.fn(),
  mockCreateProduct: vi.fn(),
  mockUpdateProduct: vi.fn(),
}));

vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi }));

vi.mock('@/services/products', () => ({
  listProducts: mockListProducts,
  deleteProduct: mockDeleteProduct,
  createProduct: mockCreateProduct,
  updateProduct: mockUpdateProduct,
}));

vi.mock('next/link', () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return { default: (props: any) => props.children };
});

import ProductsPage from '../../../../src/app/(dashboard)/products/page';

beforeEach(() => vi.clearAllMocks());

const SAMPLE_PRODUCT = {
  _id: 'p1',
  householdId: 'hh1',
  name: 'Chicken Breast',
  brand: 'Tyson',
  category: 'meat',
  servingSize: 100,
  servingUnit: 'g',
  nutrition: { calories: 165, protein: 31, carbs: 0, fat: 3.6 },
  tags: [],
  source: 'manual',
  createdBy: 'u1',
  createdAt: '2026-05-01T00:00:00.000Z',
  updatedAt: '2026-05-01T00:00:00.000Z',
};

describe('ProductsPage', () => {
  it('shows loading skeleton when session is loading', () => {
    mockUseApi.mockReturnValue({ householdId: null, isLoading: true });

    render(<ProductsPage />);

    expect(screen.getByText('Product Library')).toBeInTheDocument();
    expect(screen.queryByText('All categories')).not.toBeInTheDocument();
  });

  it('shows household prompt when no householdId', () => {
    mockUseApi.mockReturnValue({ householdId: null, isLoading: false });

    render(<ProductsPage />);

    expect(screen.getByText(/create or join a household/)).toBeInTheDocument();
  });

  it('renders products when household exists', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);

    await waitFor(() => {
      expect(screen.getByText('Chicken Breast')).toBeInTheDocument();
    });
    expect(screen.getByText('Tyson')).toBeInTheDocument();
    expect(screen.getByText('165 kcal')).toBeInTheDocument();
  });

  it('shows empty state when no products', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);

    await waitFor(() => {
      expect(screen.getByText(/No products yet/)).toBeInTheDocument();
    });
  });

  it('shows error message on fetch failure', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockRejectedValue(new Error('Network error'));

    render(<ProductsPage />);

    await waitFor(() => {
      expect(screen.getByText('Network error')).toBeInTheDocument();
    });
  });

  it('filters by search input with debounce', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);

    await waitFor(() => expect(mockListProducts).toHaveBeenCalledTimes(1));

    const searchInput = screen.getByPlaceholderText('Search products...');
    fireEvent.change(searchInput, { target: { value: 'chicken' } });

    await waitFor(
      () => {
        expect(mockListProducts).toHaveBeenCalledWith(
          'hh1',
          expect.objectContaining({ q: 'chicken' }),
        );
      },
      { timeout: 500 },
    );
  });

  it('filters by category dropdown', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);

    await waitFor(() => expect(mockListProducts).toHaveBeenCalledTimes(1));

    const categorySelect = screen.getByRole('combobox');
    fireEvent.change(categorySelect, { target: { value: 'meat' } });

    await waitFor(() => {
      expect(mockListProducts).toHaveBeenCalledWith(
        'hh1',
        expect.objectContaining({ category: 'meat' }),
      );
    });
  });

  it('deletes product on confirm', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });
    mockDeleteProduct.mockResolvedValue(undefined);
    vi.spyOn(window, 'confirm').mockReturnValue(true);

    render(<ProductsPage />);

    await waitFor(() => expect(screen.getByText('Chicken Breast')).toBeInTheDocument());

    const deleteBtn = screen.getByRole('button', { name: /delete product/i });
    fireEvent.click(deleteBtn);

    await waitFor(() => {
      expect(mockDeleteProduct).toHaveBeenCalledWith('hh1', 'p1');
    });
    expect(screen.queryByText('Chicken Breast')).not.toBeInTheDocument();
  });

  it('does not delete when confirm is cancelled', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });
    vi.spyOn(window, 'confirm').mockReturnValue(false);

    render(<ProductsPage />);

    await waitFor(() => expect(screen.getByText('Chicken Breast')).toBeInTheDocument());

    const deleteBtn = screen.getByRole('button', { name: /delete product/i });
    fireEvent.click(deleteBtn);

    expect(mockDeleteProduct).not.toHaveBeenCalled();
    expect(screen.getByText('Chicken Breast')).toBeInTheDocument();
  });

  it('shows error when delete fails', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });
    mockDeleteProduct.mockRejectedValue(new Error('Delete failed'));
    vi.spyOn(window, 'confirm').mockReturnValue(true);

    render(<ProductsPage />);

    await waitFor(() => expect(screen.getByText('Chicken Breast')).toBeInTheDocument());

    const deleteBtn = screen.getByRole('button', { name: /delete product/i });
    fireEvent.click(deleteBtn);

    await waitFor(() => {
      expect(screen.getByText('Delete failed')).toBeInTheDocument();
    });
  });

  it('shows macro breakdown in product card', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);

    await waitFor(() => expect(screen.getByText('Chicken Breast')).toBeInTheDocument());

    expect(screen.getByText('P: 31g')).toBeInTheDocument();
    expect(screen.getByText('C: 0g')).toBeInTheDocument();
    expect(screen.getByText('F: 3.6g')).toBeInTheDocument();
  });

  it('toggles import dialog open and close', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);
    await waitFor(() => expect(screen.getByText('Import')).toBeInTheDocument());

    fireEvent.click(screen.getByText('Import'));
    expect(screen.getByText('Import Products')).toBeInTheDocument();

    // Use button name or text inside the dialog container to close
    const cancelBtns = screen.getAllByRole('button', { name: /Cancel/i });
    fireEvent.click(cancelBtns[cancelBtns.length - 1]);
  });

  it('toggles add product modal open and close', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);
    await waitFor(() => expect(screen.getByText('Add Product')).toBeInTheDocument());

    fireEvent.click(screen.getByText('Add Product'));
    const headings = screen.getAllByRole('heading');
    expect(headings.some(h => h.textContent === 'Add Product')).toBe(true);

    const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
    fireEvent.click(cancelBtn);
  });

  it('opens and closes edit product modal', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });

    render(<ProductsPage />);
    await screen.findByText('Chicken Breast');

    const editBtn = screen.getByRole('button', { name: /edit product/i });
    fireEvent.click(editBtn);

    const headings = screen.getAllByRole('heading');
    expect(headings.some(h => h.textContent === 'Edit Product')).toBe(true);

    const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
    fireEvent.click(cancelBtn);
  });

  it('submits add product modal successfully', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });
    mockCreateProduct.mockResolvedValue({ _id: 'pnew' });

    render(<ProductsPage />);
    await screen.findByText('Add Product');
    fireEvent.click(screen.getByText('Add Product'));

    const nameInput = screen.getByPlaceholderText('Product name');
    fireEvent.change(nameInput, { target: { value: 'Fresh Banana' } });

    // Fill mandatory numeric inputs to satisfy form validation
    const numInputs = screen.getAllByRole('spinbutton');
    numInputs.forEach(input => {
      fireEvent.change(input, { target: { value: '100' } });
    });

    const saveBtn = screen.getByRole('button', { name: /^Save$/i });
    fireEvent.click(saveBtn);

    await waitFor(() => {
      expect(mockCreateProduct).toHaveBeenCalledWith('hh1', expect.objectContaining({ name: 'Fresh Banana' }));
    });
  });

  it('submits edit product modal successfully', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListProducts.mockResolvedValue({
      data: [SAMPLE_PRODUCT],
      pagination: { cursor: null, hasMore: false },
    });
    mockUpdateProduct.mockResolvedValue({ _id: 'p1' });

    render(<ProductsPage />);
    await screen.findByText('Chicken Breast');

    const editBtn = screen.getByRole('button', { name: /edit product/i });
    fireEvent.click(editBtn);

    const nameInput = screen.getByPlaceholderText('Product name');
    fireEvent.change(nameInput, { target: { value: 'Updated Chicken' } });

    const saveBtn = screen.getByRole('button', { name: /^Save$/i });
    fireEvent.click(saveBtn);

    await waitFor(() => {
      expect(mockUpdateProduct).toHaveBeenCalledWith('hh1', 'p1', expect.objectContaining({ name: 'Updated Chicken' }));
    });
  });
});
