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

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

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

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

vi.mock('next/navigation', () => ({
  useRouter: () => ({ push: mockPush, back: mockBack }),
  useParams: () => ({ id: 'r1' }),
}));

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

import { RecipeEditor } from '../../../../src/app/(dashboard)/recipes/RecipeEditor';

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

describe('RecipeEditor', () => {
  it('renders create form by default', () => {
    render(<RecipeEditor householdId="hh1" />);

    expect(screen.getByPlaceholderText('Recipe name')).toBeInTheDocument();
    expect(screen.getByText('Create recipe')).toBeInTheDocument();
    expect(screen.getByText('Ingredients')).toBeInTheDocument();
    expect(screen.getByText('Instructions')).toBeInTheDocument();
  });

  it('renders edit form when existing recipe provided', () => {
    const existing = {
      _id: 'r1',
      name: 'Pasta',
      description: 'Good pasta',
      servings: 2,
      prepTime: 10,
      cookTime: 20,
      cuisine: 'Italian',
      tags: ['pasta', 'quick'],
      isFavorite: true,
      ingredients: [
        {
          productId: 'p1',
          productName: 'Spaghetti',
          quantity: 200,
          unit: 'g',
          originalQuantity: 200,
          originalUnit: 'g',
          isOptional: false,
        },
      ],
      steps: [{ order: 1, instruction: 'Boil water', duration: 5, tip: 'Use salted water' }],
      perServingNutrition: { calories: 300, protein: 10, carbs: 40, fat: 8 },
      totalNutrition: { calories: 600, protein: 20, carbs: 80, fat: 16 },
      warnings: [],
      createdBy: 'u1',
      createdAt: '2026-01-01T00:00:00.000Z',
      updatedAt: '2026-01-01T00:00:00.000Z',
      householdId: 'hh1',
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    render(<RecipeEditor householdId="hh1" existing={existing as any} />);

    expect(screen.getByDisplayValue('Pasta')).toBeInTheDocument();
    expect(screen.getByDisplayValue('Good pasta')).toBeInTheDocument();
    expect(screen.getByText('Save changes')).toBeInTheDocument();
  });

  it('submits create form', async () => {
    mockCreateRecipe.mockResolvedValue({ _id: 'new1' });

    render(<RecipeEditor householdId="hh1" />);

    fireEvent.change(screen.getByPlaceholderText('Recipe name'), {
      target: { value: 'New Recipe' },
    });
    fireEvent.change(screen.getByPlaceholderText('Product name'), {
      target: { value: 'Flour' },
    });

    fireEvent.click(screen.getByText('Create recipe'));

    await waitFor(() => {
      expect(mockCreateRecipe).toHaveBeenCalledWith(
        'hh1',
        expect.objectContaining({ name: 'New Recipe' }),
      );
      expect(mockPush).toHaveBeenCalledWith('/recipes/new1');
    });
  });

  it('submits update form', async () => {
    mockUpdateRecipe.mockResolvedValue({ _id: 'r1' });
    const existing = {
      _id: 'r1',
      name: 'Old Name',
      description: '',
      servings: 4,
      tags: [],
      isFavorite: false,
      ingredients: [
        {
          productId: 'p1',
          productName: 'Test',
          quantity: 100,
          unit: 'g',
          isOptional: false,
        },
      ],
      steps: [{ order: 1, instruction: 'Do thing' }],
      perServingNutrition: { calories: 100, protein: 5, carbs: 10, fat: 3 },
      totalNutrition: { calories: 400, protein: 20, carbs: 40, fat: 12 },
      warnings: [],
      createdBy: 'u1',
      createdAt: '2026-01-01T00:00:00.000Z',
      updatedAt: '2026-01-01T00:00:00.000Z',
      householdId: 'hh1',
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    render(<RecipeEditor householdId="hh1" existing={existing as any} />);

    fireEvent.change(screen.getByDisplayValue('Old Name'), {
      target: { value: 'New Name' },
    });

    fireEvent.click(screen.getByText('Save changes'));

    await waitFor(() => {
      expect(mockUpdateRecipe).toHaveBeenCalledWith(
        'hh1',
        'r1',
        expect.objectContaining({ name: 'New Name' }),
      );
      expect(mockPush).toHaveBeenCalledWith('/recipes/r1');
    });
  });

  it('shows error when create fails', async () => {
    mockCreateRecipe.mockRejectedValue(new Error('Server error'));

    render(<RecipeEditor householdId="hh1" />);

    fireEvent.change(screen.getByPlaceholderText('Recipe name'), {
      target: { value: 'Test' },
    });
    fireEvent.change(screen.getByPlaceholderText('Product name'), {
      target: { value: 'Item' },
    });

    fireEvent.click(screen.getByText('Create recipe'));

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

  it('shows fallback error for non-Error failure', async () => {
    mockCreateRecipe.mockRejectedValue('unexpected');

    render(<RecipeEditor householdId="hh1" />);

    fireEvent.change(screen.getByPlaceholderText('Recipe name'), {
      target: { value: 'Test' },
    });
    fireEvent.change(screen.getByPlaceholderText('Product name'), {
      target: { value: 'Item' },
    });

    fireEvent.click(screen.getByText('Create recipe'));

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

  it('can add and remove ingredients', () => {
    render(<RecipeEditor householdId="hh1" />);

    fireEvent.click(screen.getByText('+ Add ingredient'));

    const nameInputs = screen.getAllByPlaceholderText('Product name');
    expect(nameInputs).toHaveLength(2);
  });

  it('can add and remove steps', () => {
    render(<RecipeEditor householdId="hh1" />);

    fireEvent.click(screen.getByText('+ Add step'));

    const stepInputs = screen.getAllByPlaceholderText('Instruction...');
    expect(stepInputs).toHaveLength(2);
  });

  it('cancel button calls router.back', () => {
    render(<RecipeEditor householdId="hh1" />);

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

    expect(mockBack).toHaveBeenCalled();
  });

  it('handles updating ingredient optional properties and removing ingredients', () => {
    render(<RecipeEditor householdId="hh1" />);

    fireEvent.click(screen.getByText('+ Add ingredient'));
    
    const prepInputs = screen.getAllByPlaceholderText(/Preparation/i);
    fireEvent.change(prepInputs[0], { target: { value: 'Diced' } });
    
    const removeBtns = screen.getAllByRole('button', { name: '×' });
    fireEvent.click(removeBtns[0]);

    const remainingPrepInputs = screen.getAllByPlaceholderText(/Preparation/i);
    expect(remainingPrepInputs).toHaveLength(1);
  });

  it('handles step metadata and removing steps', () => {
    render(<RecipeEditor householdId="hh1" />);

    fireEvent.click(screen.getByText('+ Add step'));

    const durationInputs = screen.getAllByPlaceholderText(/Duration/i);
    const tipInputs = screen.getAllByPlaceholderText(/Tip/i);
    
    fireEvent.change(durationInputs[0], { target: { value: '15' } });
    fireEvent.change(tipInputs[0], { target: { value: "Don't burn it" } });

    const removeBtns = screen.getAllByRole('button', { name: '×' });
    fireEvent.click(removeBtns[removeBtns.length - 1]);

    const remainingDurationInputs = screen.getAllByPlaceholderText(/Duration/i);
    expect(remainingDurationInputs).toHaveLength(1);
  });
});
