import { describe, it, expect, vi, beforeEach } from 'vitest';

const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
  mockGet: vi.fn(),
  mockPost: vi.fn(),
  mockPatch: vi.fn(),
  mockDelete: vi.fn(),
}));

vi.mock('../../src/services/api-client', () => ({
  apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
}));

import {
  listProducts,
  getProduct,
  lookupBarcode,
  createProduct,
  updateProduct,
  deleteProduct,
  smartAddProduct,
  importProducts,
} from '../../src/services/products';

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

describe('products service', () => {
  it('listProducts with no query', async () => {
    mockGet.mockResolvedValue({ data: [] });
    await listProducts('hh1');
    expect(mockGet).toHaveBeenCalledWith('/households/hh1/products');
  });

  it('listProducts builds query string', async () => {
    mockGet.mockResolvedValue({ data: [] });
    await listProducts('hh1', {
      q: 'chicken',
      category: 'meat',
      tags: 'organic',
      barcode: '1234',
      cursor: 'cur1',
      limit: 10,
    });
    const url = mockGet.mock.calls[0][0] as string;
    expect(url).toContain('q=chicken');
    expect(url).toContain('category=meat');
    expect(url).toContain('tags=organic');
    expect(url).toContain('barcode=1234');
    expect(url).toContain('cursor=cur1');
    expect(url).toContain('limit=10');
  });

  it('getProduct calls GET with correct path', async () => {
    mockGet.mockResolvedValue({ _id: 'p1' });
    await getProduct('hh1', 'p1');
    expect(mockGet).toHaveBeenCalledWith('/households/hh1/products/p1');
  });

  it('lookupBarcode calls GET barcode endpoint', async () => {
    mockGet.mockResolvedValue({ _id: 'p1' });
    await lookupBarcode('hh1', '1234567890');
    expect(mockGet).toHaveBeenCalledWith('/households/hh1/products/barcode/1234567890');
  });

  it('createProduct calls POST', async () => {
    mockPost.mockResolvedValue({ _id: 'p1' });
    const data = {
      name: 'Chicken',
      category: 'meat' as never,
      servingSize: 100,
      servingUnit: 'g' as never,
      nutrition: { calories: 165, protein: 31, carbs: 0, fat: 3.6 },
      tags: [],
      source: 'manual' as never,
    };
    await createProduct('hh1', data);
    expect(mockPost).toHaveBeenCalledWith('/households/hh1/products', data);
  });

  it('updateProduct calls PATCH', async () => {
    mockPatch.mockResolvedValue({ _id: 'p1' });
    await updateProduct('hh1', 'p1', { name: 'Updated' });
    expect(mockPatch).toHaveBeenCalledWith('/households/hh1/products/p1', { name: 'Updated' });
  });

  it('deleteProduct calls DELETE', async () => {
    mockDelete.mockResolvedValue(undefined);
    await deleteProduct('hh1', 'p1');
    expect(mockDelete).toHaveBeenCalledWith('/households/hh1/products/p1');
  });

  it('smartAddProduct calls POST smart-add', async () => {
    mockPost.mockResolvedValue({ available: false, message: 'LLM not configured' });
    const result = await smartAddProduct('hh1', 'chicken breast');
    expect(mockPost).toHaveBeenCalledWith('/households/hh1/products/smart-add', {
      text: 'chicken breast',
    });
    expect(result.available).toBe(false);
  });

  describe('importProducts', () => {
    const mockFetch = vi.fn();

    beforeEach(() => {
      vi.stubGlobal('fetch', mockFetch);
    });

    it('uploads file via fetch and returns result', async () => {
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ imported: 3, skippedDuplicates: 1, errors: [] }),
      });

      const file = new File(['data'], 'products.csv', { type: 'text/csv' });
      const result = await importProducts('hh1', file);

      expect(result.imported).toBe(3);
      expect(result.skippedDuplicates).toBe(1);
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('/households/hh1/products/import'),
        expect.objectContaining({ method: 'POST' }),
      );
    });

    it('throws on non-ok response with JSON body', async () => {
      mockFetch.mockResolvedValue({
        ok: false,
        status: 400,
        statusText: 'Bad Request',
        json: () => Promise.resolve({ message: 'File too large' }),
      });

      const file = new File(['data'], 'products.csv', { type: 'text/csv' });
      await expect(importProducts('hh1', file)).rejects.toThrow('File too large');
    });

    it('throws with status on non-ok response without JSON', async () => {
      mockFetch.mockResolvedValue({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
        json: () => Promise.reject(new Error('not json')),
      });

      const file = new File(['data'], 'products.csv', { type: 'text/csv' });
      await expect(importProducts('hh1', file)).rejects.toThrow(
        'Import failed: 500 Internal Server Error',
      );
    });

    it('throws with status when JSON body has no message', async () => {
      mockFetch.mockResolvedValue({
        ok: false,
        status: 422,
        statusText: 'Unprocessable Entity',
        json: () => Promise.resolve({}),
      });

      const file = new File(['data'], 'products.csv', { type: 'text/csv' });
      await expect(importProducts('hh1', file)).rejects.toThrow('Import failed: 422');
    });
  });
});
