import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockQueryBuilder, createMockPartner } from '../../../__tests__/helpers.js';

// Mock dependencies before importing service
const mockRepository = createMockRepository();
const mockQueryBuilder = createMockQueryBuilder();

jest.mock('../../../config/typeorm.js', () => ({
  AppDataSource: {
    getRepository: jest.fn(() => mockRepository),
  },
}));

jest.mock('../../../shared/utils/logger.js', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
    warn: jest.fn(),
  },
}));

// Import after mocking
import { partnersService } from '../partners.service.js';
import { NotFoundError, ValidationError } from '../../../shared/types/index.js';

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

  beforeEach(() => {
    jest.clearAllMocks();
    mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
  });

  describe('findAll', () => {
    it('should return partners with pagination', async () => {
      const mockPartners = [
        createMockPartner({ id: '1', displayName: 'Partner A' }),
        createMockPartner({ id: '2', displayName: 'Partner B' }),
      ];

      mockQueryBuilder.getCount.mockResolvedValue(2);
      mockQueryBuilder.getMany.mockResolvedValue(mockPartners);

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

      expect(result.data).toHaveLength(2);
      expect(result.total).toBe(2);
      expect(mockQueryBuilder.where).toHaveBeenCalledWith(
        'partner.tenantId = :tenantId',
        { tenantId }
      );
    });

    it('should filter by search term', async () => {
      mockQueryBuilder.getCount.mockResolvedValue(1);
      mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);

      await partnersService.findAll(tenantId, { search: 'test' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        expect.stringContaining('partner.displayName ILIKE :search'),
        { search: '%test%' }
      );
    });

    it('should filter by partner type', async () => {
      mockQueryBuilder.getCount.mockResolvedValue(1);
      mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);

      await partnersService.findAll(tenantId, { partnerType: 'customer' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'partner.partnerType = :partnerType',
        { partnerType: 'customer' }
      );
    });

    it('should filter by active status', async () => {
      mockQueryBuilder.getCount.mockResolvedValue(1);
      mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);

      await partnersService.findAll(tenantId, { isActive: true });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'partner.isActive = :isActive',
        { isActive: true }
      );
    });

    it('should apply pagination correctly', async () => {
      mockQueryBuilder.getCount.mockResolvedValue(50);
      mockQueryBuilder.getMany.mockResolvedValue([]);

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

      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
    });
  });

  describe('findById', () => {
    it('should return partner when found', async () => {
      const mockPartner = createMockPartner();
      mockRepository.findOne.mockResolvedValue(mockPartner);

      const result = await partnersService.findById('partner-uuid-1', tenantId);

      expect(result).toEqual(mockPartner);
      expect(mockRepository.findOne).toHaveBeenCalledWith({
        where: {
          id: 'partner-uuid-1',
          tenantId,
          deletedAt: expect.anything(),
        },
      });
    });

    it('should throw NotFoundError when partner not found', async () => {
      mockRepository.findOne.mockResolvedValue(null);

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

    it('should enforce tenant isolation', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(
        partnersService.findById('partner-uuid-1', 'different-tenant')
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('create', () => {
    const createDto = {
      code: 'PART-001',
      displayName: 'New Partner',
      email: 'new@partner.com',
      partnerType: 'customer' as const,
    };

    it('should create partner successfully', async () => {
      mockRepository.findOne.mockResolvedValue(null); // No existing partner
      const savedPartner = createMockPartner({ ...createDto });
      mockRepository.create.mockReturnValue(savedPartner);
      mockRepository.save.mockResolvedValue(savedPartner);

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

      expect(result).toEqual(savedPartner);
      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          tenantId,
          code: createDto.code,
          displayName: createDto.displayName,
          createdBy: userId,
        })
      );
    });

    it('should throw ValidationError when code already exists', async () => {
      mockRepository.findOne.mockResolvedValue(createMockPartner());

      await expect(
        partnersService.create(createDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should normalize email to lowercase', async () => {
      mockRepository.findOne.mockResolvedValue(null);
      const savedPartner = createMockPartner();
      mockRepository.create.mockReturnValue(savedPartner);
      mockRepository.save.mockResolvedValue(savedPartner);

      await partnersService.create(
        { ...createDto, email: 'TEST@PARTNER.COM' },
        tenantId,
        userId
      );

      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          email: 'test@partner.com',
        })
      );
    });

    it('should set default values correctly', async () => {
      mockRepository.findOne.mockResolvedValue(null);
      const savedPartner = createMockPartner();
      mockRepository.create.mockReturnValue(savedPartner);
      mockRepository.save.mockResolvedValue(savedPartner);

      await partnersService.create(
        { code: 'PART-001', displayName: 'Partner' },
        tenantId,
        userId
      );

      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          partnerType: 'customer',
          paymentTermDays: 0,
          creditLimit: 0,
          discountPercent: 0,
          isActive: true,
          isVerified: false,
        })
      );
    });
  });

  describe('update', () => {
    it('should update partner successfully', async () => {
      const existingPartner = createMockPartner();
      mockRepository.findOne.mockResolvedValue(existingPartner);
      mockRepository.save.mockResolvedValue({ ...existingPartner, displayName: 'Updated Name' });

      const result = await partnersService.update(
        'partner-uuid-1',
        { displayName: 'Updated Name' },
        tenantId,
        userId
      );

      expect(mockRepository.save).toHaveBeenCalled();
      expect(result.displayName).toBe('Updated Name');
    });

    it('should throw NotFoundError when partner not found', async () => {
      mockRepository.findOne.mockResolvedValue(null);

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

    it('should update credit limit', async () => {
      const existingPartner = createMockPartner({ creditLimit: 1000 });
      mockRepository.findOne.mockResolvedValue(existingPartner);
      mockRepository.save.mockResolvedValue({ ...existingPartner, creditLimit: 5000 });

      await partnersService.update(
        'partner-uuid-1',
        { creditLimit: 5000 },
        tenantId,
        userId
      );

      expect(existingPartner.creditLimit).toBe(5000);
    });

    it('should set updatedBy field', async () => {
      const existingPartner = createMockPartner();
      mockRepository.findOne.mockResolvedValue(existingPartner);
      mockRepository.save.mockResolvedValue(existingPartner);

      await partnersService.update(
        'partner-uuid-1',
        { displayName: 'Updated' },
        tenantId,
        userId
      );

      expect(existingPartner.updatedBy).toBe(userId);
    });
  });

  describe('delete', () => {
    it('should soft delete partner', async () => {
      const existingPartner = createMockPartner();
      mockRepository.findOne.mockResolvedValue(existingPartner);
      mockRepository.save.mockResolvedValue(existingPartner);

      await partnersService.delete('partner-uuid-1', tenantId, userId);

      expect(existingPartner.deletedAt).toBeInstanceOf(Date);
      expect(existingPartner.isActive).toBe(false);
      expect(mockRepository.save).toHaveBeenCalled();
    });

    it('should throw NotFoundError when partner not found', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(
        partnersService.delete('nonexistent-id', tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('findCustomers', () => {
    it('should return only customers', async () => {
      const mockCustomers = [createMockPartner({ partnerType: 'customer' })];
      mockQueryBuilder.getCount.mockResolvedValue(1);
      mockQueryBuilder.getMany.mockResolvedValue(mockCustomers);

      const result = await partnersService.findCustomers(tenantId, {});

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'partner.partnerType = :partnerType',
        { partnerType: 'customer' }
      );
      expect(result.data).toHaveLength(1);
    });
  });

  describe('findSuppliers', () => {
    it('should return only suppliers', async () => {
      const mockSuppliers = [createMockPartner({ partnerType: 'supplier' })];
      mockQueryBuilder.getCount.mockResolvedValue(1);
      mockQueryBuilder.getMany.mockResolvedValue(mockSuppliers);

      const result = await partnersService.findSuppliers(tenantId, {});

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'partner.partnerType = :partnerType',
        { partnerType: 'supplier' }
      );
      expect(result.data).toHaveLength(1);
    });
  });
});
