import { jest, describe, it, expect, beforeEach } from '@jest/globals';

// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();

jest.mock('../../../config/database.js', () => ({
  query: (...args: any[]) => mockQuery(...args),
  queryOne: (...args: any[]) => mockQueryOne(...args),
  getClient: jest.fn(),
}));

// Import after mocking
import { pricelistsService } from '../pricelists.service.js';
import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js';

// Helper to create mock pricelist
function createMockPricelist(overrides: Record<string, any> = {}) {
  return {
    id: 'pricelist-uuid-1',
    tenant_id: 'test-tenant-uuid',
    company_id: 'company-uuid-1',
    company_name: 'Test Company',
    name: 'Standard Pricelist',
    currency_id: 'currency-uuid-1',
    currency_code: 'MXN',
    active: true,
    items: [],
    created_at: new Date(),
    ...overrides,
  };
}

// Helper to create mock pricelist item
function createMockPricelistItem(overrides: Record<string, any> = {}) {
  return {
    id: 'pricelist-item-uuid-1',
    pricelist_id: 'pricelist-uuid-1',
    product_id: 'product-uuid-1',
    product_name: 'Test Product',
    product_category_id: null,
    category_name: null,
    price: 100,
    min_quantity: 1,
    valid_from: null,
    valid_to: null,
    active: true,
    ...overrides,
  };
}

describe('PricelistsService', () => {
  const tenantId = 'test-tenant-uuid';
  const userId = 'test-user-uuid';

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('should return pricelists with pagination', async () => {
      const mockPricelists = [
        createMockPricelist({ id: '1', name: 'Standard' }),
        createMockPricelist({ id: '2', name: 'VIP' }),
      ];

      mockQueryOne.mockResolvedValue({ count: '2' });
      mockQuery.mockResolvedValue(mockPricelists);

      const result = await pricelistsService.findAll(tenantId, { page: 1, limit: 20 });

      expect(result.data).toHaveLength(2);
      expect(result.total).toBe(2);
    });

    it('should filter by company_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await pricelistsService.findAll(tenantId, { company_id: 'company-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('p.company_id = $'),
        expect.arrayContaining([tenantId, 'company-uuid'])
      );
    });

    it('should filter by active status', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await pricelistsService.findAll(tenantId, { active: true });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('p.active = $'),
        expect.arrayContaining([tenantId, true])
      );
    });

    it('should filter by inactive status', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await pricelistsService.findAll(tenantId, { active: false });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('p.active = $'),
        expect.arrayContaining([tenantId, false])
      );
    });

    it('should apply pagination correctly', async () => {
      mockQueryOne.mockResolvedValue({ count: '50' });
      mockQuery.mockResolvedValue([]);

      await pricelistsService.findAll(tenantId, { page: 3, limit: 10 });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([10, 20]) // limit=10, offset=20 (page 3)
      );
    });

    it('should use default pagination when not provided', async () => {
      mockQueryOne.mockResolvedValue({ count: '50' });
      mockQuery.mockResolvedValue([]);

      await pricelistsService.findAll(tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([20, 0]) // default limit=20, offset=0
      );
    });
  });

  describe('findById', () => {
    it('should return pricelist with items when found', async () => {
      const mockPricelist = createMockPricelist();
      const mockItems = [createMockPricelistItem()];

      mockQueryOne.mockResolvedValue(mockPricelist);
      mockQuery.mockResolvedValue(mockItems);

      const result = await pricelistsService.findById('pricelist-uuid-1', tenantId);

      expect(result).toEqual({ ...mockPricelist, items: mockItems });
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('WHERE p.id = $1 AND p.tenant_id = $2'),
        ['pricelist-uuid-1', tenantId]
      );
    });

    it('should throw NotFoundError when pricelist not found', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        pricelistsService.findById('nonexistent-id', tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should return empty items array when no items exist', async () => {
      const mockPricelist = createMockPricelist();
      mockQueryOne.mockResolvedValue(mockPricelist);
      mockQuery.mockResolvedValue([]);

      const result = await pricelistsService.findById('pricelist-uuid-1', tenantId);

      expect(result.items).toEqual([]);
    });
  });

  describe('create', () => {
    const createDto = {
      name: 'New Pricelist',
      currency_id: 'currency-uuid',
    };

    it('should create pricelist successfully', async () => {
      mockQueryOne
        .mockResolvedValueOnce(null) // unique name check
        .mockResolvedValueOnce(createMockPricelist({ name: 'New Pricelist' })); // INSERT

      const result = await pricelistsService.create(createDto, tenantId, userId);

      expect(result.name).toBe('New Pricelist');
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO sales.pricelists'),
        expect.any(Array)
      );
    });

    it('should create pricelist with company_id', async () => {
      const dtoWithCompany = { ...createDto, company_id: 'company-uuid' };
      mockQueryOne
        .mockResolvedValueOnce(null) // unique name check
        .mockResolvedValueOnce(createMockPricelist({ name: 'New Pricelist', company_id: 'company-uuid' }));

      await pricelistsService.create(dtoWithCompany, tenantId, userId);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO sales.pricelists'),
        expect.arrayContaining(['company-uuid'])
      );
    });

    it('should throw ConflictError when name already exists', async () => {
      mockQueryOne.mockResolvedValueOnce({ id: 'existing-id' }); // name exists

      await expect(
        pricelistsService.create(createDto, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });
  });

  describe('update', () => {
    it('should update pricelist name', async () => {
      const existingPricelist = createMockPricelist();
      mockQueryOne
        .mockResolvedValueOnce(existingPricelist) // findById
        .mockResolvedValueOnce(null) // unique name check
        .mockResolvedValueOnce(existingPricelist); // findById after update
      mockQuery
        .mockResolvedValueOnce([]) // items for findById
        .mockResolvedValueOnce(undefined) // UPDATE
        .mockResolvedValueOnce([]); // items for findById after update

      await pricelistsService.update(
        'pricelist-uuid-1',
        { name: 'Updated Pricelist' },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE sales.pricelists SET'),
        expect.any(Array)
      );
    });

    it('should update pricelist currency_id', async () => {
      const existingPricelist = createMockPricelist();
      mockQueryOne
        .mockResolvedValueOnce(existingPricelist)
        .mockResolvedValueOnce(existingPricelist);
      mockQuery
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(undefined)
        .mockResolvedValueOnce([]);

      await pricelistsService.update(
        'pricelist-uuid-1',
        { currency_id: 'new-currency-uuid' },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('currency_id'),
        expect.arrayContaining(['new-currency-uuid'])
      );
    });

    it('should update pricelist active status', async () => {
      const existingPricelist = createMockPricelist();
      mockQueryOne
        .mockResolvedValueOnce(existingPricelist)
        .mockResolvedValueOnce(existingPricelist);
      mockQuery
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(undefined)
        .mockResolvedValueOnce([]);

      await pricelistsService.update(
        'pricelist-uuid-1',
        { active: false },
        tenantId,
        userId
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('active'),
        expect.arrayContaining([false])
      );
    });

    it('should throw ConflictError when updating to existing name', async () => {
      const existingPricelist = createMockPricelist();
      mockQueryOne
        .mockResolvedValueOnce(existingPricelist) // findById
        .mockResolvedValueOnce({ id: 'other-pricelist' }); // name conflict
      mockQuery.mockResolvedValue([]);

      await expect(
        pricelistsService.update('pricelist-uuid-1', { name: 'Existing Name' }, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should throw NotFoundError when pricelist not found', async () => {
      mockQueryOne.mockResolvedValueOnce(null);

      await expect(
        pricelistsService.update('nonexistent-id', { name: 'Test' }, tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('addItem', () => {
    const itemDto = {
      product_id: 'product-uuid',
      price: 150,
      min_quantity: 10,
    };

    it('should add item with product_id', async () => {
      const pricelist = createMockPricelist();
      const newItem = createMockPricelistItem({ product_id: 'product-uuid', price: 150 });

      mockQueryOne
        .mockResolvedValueOnce(pricelist) // findById
        .mockResolvedValueOnce(newItem); // INSERT
      mockQuery.mockResolvedValue([]); // items for findById

      const result = await pricelistsService.addItem('pricelist-uuid-1', itemDto, tenantId, userId);

      expect(result.product_id).toBe('product-uuid');
      expect(result.price).toBe(150);
    });

    it('should add item with product_category_id', async () => {
      const categoryItemDto = {
        product_category_id: 'category-uuid',
        price: 200,
      };
      const pricelist = createMockPricelist();
      const newItem = createMockPricelistItem({ product_category_id: 'category-uuid', price: 200 });

      mockQueryOne
        .mockResolvedValueOnce(pricelist)
        .mockResolvedValueOnce(newItem);
      mockQuery.mockResolvedValue([]);

      const result = await pricelistsService.addItem('pricelist-uuid-1', categoryItemDto, tenantId, userId);

      expect(result.price).toBe(200);
    });

    it('should add item with validity dates', async () => {
      const itemWithDates = {
        product_id: 'product-uuid',
        price: 100,
        valid_from: '2024-01-01',
        valid_to: '2024-12-31',
      };
      const pricelist = createMockPricelist();
      const newItem = createMockPricelistItem(itemWithDates);

      mockQueryOne
        .mockResolvedValueOnce(pricelist)
        .mockResolvedValueOnce(newItem);
      mockQuery.mockResolvedValue([]);

      await pricelistsService.addItem('pricelist-uuid-1', itemWithDates, tenantId, userId);

      expect(mockQueryOne).toHaveBeenLastCalledWith(
        expect.stringContaining('INSERT INTO sales.pricelist_items'),
        expect.arrayContaining(['2024-01-01', '2024-12-31'])
      );
    });

    it('should throw ValidationError when neither product nor category specified', async () => {
      const invalidDto = { price: 100 };
      const pricelist = createMockPricelist();

      mockQueryOne.mockResolvedValueOnce(pricelist);
      mockQuery.mockResolvedValue([]);

      await expect(
        pricelistsService.addItem('pricelist-uuid-1', invalidDto as any, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when both product and category specified', async () => {
      const invalidDto = {
        product_id: 'product-uuid',
        product_category_id: 'category-uuid',
        price: 100,
      };
      const pricelist = createMockPricelist();

      mockQueryOne.mockResolvedValueOnce(pricelist);
      mockQuery.mockResolvedValue([]);

      await expect(
        pricelistsService.addItem('pricelist-uuid-1', invalidDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw NotFoundError when pricelist not found', async () => {
      mockQueryOne.mockResolvedValueOnce(null);

      await expect(
        pricelistsService.addItem('nonexistent-id', itemDto, tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should use default min_quantity of 1', async () => {
      const itemWithoutMinQty = {
        product_id: 'product-uuid',
        price: 100,
      };
      const pricelist = createMockPricelist();
      const newItem = createMockPricelistItem();

      mockQueryOne
        .mockResolvedValueOnce(pricelist)
        .mockResolvedValueOnce(newItem);
      mockQuery.mockResolvedValue([]);

      await pricelistsService.addItem('pricelist-uuid-1', itemWithoutMinQty, tenantId, userId);

      expect(mockQueryOne).toHaveBeenLastCalledWith(
        expect.stringContaining('INSERT INTO sales.pricelist_items'),
        expect.arrayContaining([1]) // default min_quantity
      );
    });
  });

  describe('removeItem', () => {
    it('should remove item from pricelist', async () => {
      const pricelist = createMockPricelist();
      mockQueryOne.mockResolvedValue(pricelist);
      mockQuery
        .mockResolvedValueOnce([createMockPricelistItem()]) // items for findById
        .mockResolvedValueOnce(undefined); // DELETE

      await pricelistsService.removeItem('pricelist-uuid-1', 'item-uuid', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM sales.pricelist_items'),
        ['item-uuid', 'pricelist-uuid-1']
      );
    });

    it('should throw NotFoundError when pricelist not found', async () => {
      mockQueryOne.mockResolvedValueOnce(null);

      await expect(
        pricelistsService.removeItem('nonexistent-id', 'item-uuid', tenantId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('getProductPrice', () => {
    it('should return product-specific price', async () => {
      mockQueryOne.mockResolvedValue({ price: 150 });

      const result = await pricelistsService.getProductPrice(
        'product-uuid',
        'pricelist-uuid',
        5
      );

      expect(result).toBe(150);
    });

    it('should return category price when no product-specific price', async () => {
      mockQueryOne.mockResolvedValue({ price: 120 });

      const result = await pricelistsService.getProductPrice(
        'product-uuid',
        'pricelist-uuid',
        1
      );

      expect(result).toBe(120);
    });

    it('should return null when no price found', async () => {
      mockQueryOne.mockResolvedValue(null);

      const result = await pricelistsService.getProductPrice(
        'product-uuid',
        'pricelist-uuid',
        1
      );

      expect(result).toBeNull();
    });

    it('should use default quantity of 1', async () => {
      mockQueryOne.mockResolvedValue({ price: 100 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid');

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('min_quantity <= $3'),
        ['pricelist-uuid', 'product-uuid', 1]
      );
    });

    it('should filter by min_quantity', async () => {
      mockQueryOne.mockResolvedValue({ price: 80 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid', 100);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('min_quantity <= $3'),
        expect.arrayContaining([100])
      );
    });

    it('should filter by valid dates', async () => {
      mockQueryOne.mockResolvedValue({ price: 100 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid', 1);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('valid_from IS NULL OR valid_from <= CURRENT_DATE'),
        expect.any(Array)
      );
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('valid_to IS NULL OR valid_to >= CURRENT_DATE'),
        expect.any(Array)
      );
    });

    it('should only return active items', async () => {
      mockQueryOne.mockResolvedValue({ price: 100 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid', 1);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('active = true'),
        expect.any(Array)
      );
    });

    it('should prioritize product-specific over category prices', async () => {
      mockQueryOne.mockResolvedValue({ price: 100 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid', 1);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('ORDER BY product_id NULLS LAST'),
        expect.any(Array)
      );
    });

    it('should prioritize higher min_quantity', async () => {
      mockQueryOne.mockResolvedValue({ price: 100 });

      await pricelistsService.getProductPrice('product-uuid', 'pricelist-uuid', 1);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('min_quantity DESC'),
        expect.any(Array)
      );
    });
  });
});
