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 { mockListRecipes, mockDeleteRecipe } = vi.hoisted(() => ({
  mockListRecipes: vi.fn(),
  mockDeleteRecipe: vi.fn(),
}));

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

vi.mock('@/services/recipes', () => ({
  listRecipes: mockListRecipes,
  deleteRecipe: mockDeleteRecipe,
  createRecipe: vi.fn(),
  updateRecipe: vi.fn(),
  getRecipe: vi.fn(),
  scaleRecipe: vi.fn(),
  importRecipeFromText: vi.fn(),
  importRecipeFromUrl: vi.fn(),
  listRecipesByProduct: vi.fn(),
}));

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

import RecipesPage from '../../../../src/app/(dashboard)/recipes/page';

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

const SAMPLE_RECIPE = {
  _id: 'r1',
  householdId: 'hh1',
  name: 'Spaghetti Bolognese',
  description: 'Classic Italian pasta',
  servings: 4,
  prepTime: 15,
  cookTime: 30,
  totalTime: 45,
  cuisine: 'Italian',
  tags: ['pasta', 'comfort'],
  isFavorite: true,
  ingredients: [
    {
      productId: 'p1',
      productName: 'Spaghetti',
      quantity: 400,
      unit: 'g',
      isOptional: false,
    },
  ],
  steps: [{ order: 1, instruction: 'Boil water' }],
  perServingNutrition: {
    calories: 450,
    protein: 25,
    carbs: 55,
    fat: 12,
    fiber: 3,
    sugar: 5,
    sodium: 400,
  },
  totalNutrition: {
    calories: 1800,
    protein: 100,
    carbs: 220,
    fat: 48,
  },
  warnings: ['high_calories'],
  createdBy: 'u1',
  createdAt: '2026-01-01T00:00:00.000Z',
  updatedAt: '2026-01-01T00:00:00.000Z',
};

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

    render(<RecipesPage />);

    expect(screen.getByText('Recipes')).toBeInTheDocument();
    expect(screen.queryByText('Search recipes...')).not.toBeInTheDocument();
  });

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

    render(<RecipesPage />);

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

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

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('Spaghetti Bolognese')).toBeInTheDocument();
    });
  });

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

    render(<RecipesPage />);

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

  it('shows nutrition info on recipe card', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('450 kcal')).toBeInTheDocument();
      expect(screen.getByText('4 servings')).toBeInTheDocument();
    });
  });

  it('shows time on recipe card', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('45 min')).toBeInTheDocument();
    });
  });

  it('shows warning labels', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('High cal')).toBeInTheDocument();
    });
  });

  it('shows tags on recipe card', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('pasta')).toBeInTheDocument();
      expect(screen.getByText('comfort')).toBeInTheDocument();
    });
  });

  it('shows favorite star', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      // Star character is rendered for favorites
      expect(screen.getByText('Spaghetti Bolognese').closest('a')).toBeInTheDocument();
    });
  });

  it('shows edit link', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

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

  it('handles delete with confirmation', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });
    mockDeleteRecipe.mockResolvedValue(undefined);
    vi.spyOn(window, 'confirm').mockReturnValue(true);

    render(<RecipesPage />);

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

    fireEvent.click(screen.getByText('Delete'));

    await waitFor(() => {
      expect(mockDeleteRecipe).toHaveBeenCalledWith('hh1', 'r1');
    });
  });

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

    render(<RecipesPage />);

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

    fireEvent.click(screen.getByText('Delete'));

    expect(mockDeleteRecipe).not.toHaveBeenCalled();
  });

  it('shows error when fetch fails', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockRejectedValue(new Error('Network error'));

    render(<RecipesPage />);

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

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

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByPlaceholderText('Cuisine...')).toBeInTheDocument();
    });

    fireEvent.change(screen.getByPlaceholderText('Cuisine...'), {
      target: { value: 'Thai' },
    });

    await waitFor(() => {
      expect(screen.getByText(/No recipes match your filters/)).toBeInTheDocument();
    });
  });

  it('shows recipe without totalTime using prep+cook', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [{ ...SAMPLE_RECIPE, totalTime: undefined }],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('45 min')).toBeInTheDocument();
    });
  });

  it('formats hours correctly', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [{ ...SAMPLE_RECIPE, totalTime: 90 }],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('1h 30m')).toBeInTheDocument();
    });
  });

  it('formats exact hours', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [{ ...SAMPLE_RECIPE, totalTime: 120 }],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('2h')).toBeInTheDocument();
    });
  });

  it('handles delete error', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });
    mockDeleteRecipe.mockRejectedValue(new Error('Delete failed'));
    vi.spyOn(window, 'confirm').mockReturnValue(true);

    render(<RecipesPage />);

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

    fireEvent.click(screen.getByText('Delete'));

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

  it('handles non-Error delete failure', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [SAMPLE_RECIPE],
      pagination: { cursor: null, hasMore: false },
    });
    mockDeleteRecipe.mockRejectedValue('string error');
    vi.spyOn(window, 'confirm').mockReturnValue(true);

    render(<RecipesPage />);

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

    fireEvent.click(screen.getByText('Delete'));

    await waitFor(() => {
      expect(screen.getByText('Failed to delete recipe')).toBeInTheDocument();
    });
  });

  it('shows + New Recipe link', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('+ New Recipe')).toBeInTheDocument();
    });
  });

  it('recipe with no warnings renders without warning badges', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [{ ...SAMPLE_RECIPE, warnings: [], tags: [] }],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByText('Spaghetti Bolognese')).toBeInTheDocument();
      expect(screen.queryByText('High cal')).not.toBeInTheDocument();
    });
  });

  it('toggles favorites checkbox', async () => {
    mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
    mockListRecipes.mockResolvedValue({
      data: [],
      pagination: { cursor: null, hasMore: false },
    });

    render(<RecipesPage />);

    await waitFor(() => {
      expect(screen.getByLabelText('Favorites')).toBeInTheDocument();
    });

    fireEvent.click(screen.getByLabelText('Favorites'));

    await waitFor(() => {
      expect(mockListRecipes).toHaveBeenCalledWith(
        'hh1',
        expect.objectContaining({ isFavorite: true }),
      );
    });
  });
});
