import { describe, it, expect, vi, beforeEach } from 'vitest';
import { BarcodeService } from '../../../src/modules/products/barcode.service.js';

vi.mock('undici', () => ({
  request: vi.fn(),
}));

import { request as undiciRequest } from 'undici';

const mockRequest = undiciRequest as ReturnType<typeof vi.fn>;

function makeMockRepo() {
  return {
    findByBarcode: vi.fn(),
    create: vi.fn(),
  };
}

describe('BarcodeService', () => {
  let service: BarcodeService;
  let mockRepo: ReturnType<typeof makeMockRepo>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockRepo = makeMockRepo();
    service = new BarcodeService({
      productsRepository: mockRepo as unknown as ConstructorParameters<
        typeof BarcodeService
      >[0]['productsRepository'],
    });
  });

  it('returns cached product from local DB', async () => {
    const existing = { _id: 'p1', name: 'Test Product', barcode: '1234567890123' };
    mockRepo.findByBarcode.mockResolvedValue(existing);

    const result = await service.lookup('hh1', '1234567890123', 'u1');

    expect(result.found).toBe(true);
    if (result.found) {
      expect(result.cached).toBe(true);
      expect(result.product).toEqual(existing);
    }
    expect(mockRequest).not.toHaveBeenCalled();
  });

  it('calls Open Food Facts when not found locally and caches result', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    const savedProduct = { _id: 'p2', name: 'Nutella', barcode: '3017620422003' };
    mockRepo.create.mockResolvedValue(savedProduct);

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Nutella',
            brands: 'Ferrero',
            categories_tags: ['en:snacks'],
            serving_quantity: 15,
            nutriments: {
              'energy-kcal_serving': 80,
              proteins_serving: 0.9,
              carbohydrates_serving: 8.5,
              fat_serving: 4.7,
              fiber_serving: 0.5,
              sugars_serving: 8.2,
              'saturated-fat_serving': 1.6,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '3017620422003', 'u1');

    expect(result.found).toBe(true);
    if (result.found) {
      expect(result.cached).toBe(false);
    }
    expect(mockRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({
        householdId: 'hh1',
        name: 'Nutella',
        brand: 'Ferrero',
        barcode: '3017620422003',
        category: 'snacks',
        servingSize: 15,
        servingUnit: 'g',
        source: 'barcode_lookup',
        createdBy: 'u1',
      }),
    );
  });

  it('returns found:false when OFF returns 404', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRequest.mockResolvedValue({
      statusCode: 404,
      body: { json: vi.fn().mockResolvedValue({}) },
    });

    const result = await service.lookup('hh1', '0000000000000', 'u1');

    expect(result.found).toBe(false);
    expect(mockRepo.create).not.toHaveBeenCalled();
  });

  it('returns found:false when OFF returns status 0', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({ status: 0, product: { product_name: 'X' } }),
      },
    });

    const result = await service.lookup('hh1', '0000000000000', 'u1');

    expect(result.found).toBe(false);
  });

  it('returns found:false when OFF product has no product_name', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({ status: 1, product: {} }),
      },
    });

    const result = await service.lookup('hh1', '0000000000000', 'u1');

    expect(result.found).toBe(false);
  });

  it('returns found:false when network request throws', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRequest.mockRejectedValue(new Error('Connection timeout'));

    const result = await service.lookup('hh1', '0000000000000', 'u1');

    expect(result.found).toBe(false);
  });

  it('falls back to per-100g nutrition when no serving data', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    const savedProduct = { _id: 'p3', name: 'Plain Rice', barcode: '1111111111111' };
    mockRepo.create.mockResolvedValue(savedProduct);

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Plain Rice',
            categories_tags: ['en:cereals'],
            nutriments: {
              'energy-kcal_100g': 130,
              proteins_100g: 2.7,
              carbohydrates_100g: 28,
              fat_100g: 0.3,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '1111111111111', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({
        servingSize: 100,
        nutrition: expect.objectContaining({
          calories: 130,
          protein: 2.7,
          carbs: 28,
          fat: 0.3,
        }),
      }),
    );
  });

  it('maps category from OFF categories_tags', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p4', name: 'Milk' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Milk',
            categories_tags: ['en:dairies'],
            nutriments: {
              'energy-kcal_100g': 60,
              proteins_100g: 3.3,
              carbohydrates_100g: 4.7,
              fat_100g: 3.2,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '2222222222222', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ category: 'dairy' }));
  });

  it('parses serving_size string when serving_quantity is absent', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p5', name: 'Yogurt' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Yogurt',
            serving_size: '125 g',
            nutriments: {
              'energy-kcal_serving': 110,
              proteins_serving: 5,
              carbohydrates_serving: 15,
              fat_serving: 3,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '3333333333333', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ servingSize: 125 }));
  });

  it('converts sodium and cholesterol from grams to milligrams', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p6', name: 'Soup' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Soup',
            serving_quantity: 250,
            nutriments: {
              'energy-kcal_serving': 90,
              proteins_serving: 4,
              carbohydrates_serving: 12,
              fat_serving: 2,
              sodium_serving: 0.8,
              cholesterol_serving: 0.015,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '4444444444444', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({
        nutrition: expect.objectContaining({
          sodium: 800,
          cholesterol: 15,
        }),
      }),
    );
  });

  it('handles brand with multiple comma-separated values by taking first', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p7', name: 'Multi Brand' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Multi Brand',
            brands: 'BrandA, BrandB, BrandC',
            nutriments: {
              'energy-kcal_100g': 100,
              proteins_100g: 5,
              carbohydrates_100g: 20,
              fat_100g: 2,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '5555555555555', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ brand: 'BrandA' }));
  });

  it('returns found:false when OFF product field is missing', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({ status: 1 }),
      },
    });

    const result = await service.lookup('hh1', '6666666666666', 'u1');

    expect(result.found).toBe(false);
  });

  it('defaults category to OTHER when no matching tags', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p8', name: 'Unknown' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Unknown',
            categories_tags: ['en:unknown-stuff'],
            nutriments: {
              'energy-kcal_100g': 50,
              proteins_100g: 1,
              carbohydrates_100g: 10,
              fat_100g: 0.5,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '7777777777777', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ category: 'other' }));
  });

  it('handles serving_size with no numeric value', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p9', name: 'Weird Serving' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Weird Serving',
            serving_size: 'one portion',
            nutriments: {
              'energy-kcal_serving': 100,
              proteins_serving: 5,
              carbohydrates_serving: 10,
              fat_serving: 3,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '8888888888888', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ servingSize: 100 }));
  });

  it('defaults nutrition to zeros when nutriments is undefined', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p10', name: 'No Nutrition' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'No Nutrition',
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '9999999999999', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({
        nutrition: { calories: 0, protein: 0, carbs: 0, fat: 0 },
      }),
    );
  });

  it('defaults serving size to 100 when serving_quantity is negative', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p11', name: 'Negative QTY' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Negative QTY',
            serving_quantity: -1,
            nutriments: {
              'energy-kcal_100g': 50,
              proteins_100g: 2,
              carbohydrates_100g: 8,
              fat_100g: 1,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '1010101010101', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ servingSize: 100 }));
  });

  it('uses serving fallbacks when _serving nutriments are missing', async () => {
    mockRepo.findByBarcode.mockResolvedValue(null);
    mockRepo.create.mockResolvedValue({ _id: 'p12', name: 'Partial Nutrients' });

    mockRequest.mockResolvedValue({
      statusCode: 200,
      body: {
        json: vi.fn().mockResolvedValue({
          status: 1,
          product: {
            product_name: 'Partial Nutrients',
            serving_quantity: 50,
            nutriments: {
              'energy-kcal_100g': 200,
              proteins_100g: 10,
              carbohydrates_100g: 30,
              fat_100g: 5,
              fiber_100g: 3,
              sugars_100g: 12,
              sodium_100g: 0.4,
              'saturated-fat_100g': 1.5,
              cholesterol_100g: 0.02,
            },
          },
        }),
      },
    });

    const result = await service.lookup('hh1', '1212121212121', 'u1');

    expect(result.found).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({
        servingSize: 50,
        nutrition: expect.objectContaining({
          calories: 200,
          protein: 10,
          carbs: 30,
          fat: 5,
          fiber: 3,
          sugar: 12,
          sodium: 400,
          saturatedFat: 1.5,
          cholesterol: 20,
        }),
      }),
    );
  });
});
