import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PartnersService } from '../partners.service';
import { Partner, PartnerStatus, PartnerType } from '../entities';
import { CreatePartnerDto, UpdatePartnerDto } from '../dto';

describe('PartnersService', () => {
  let service: PartnersService;
  let partnerRepository: Repository<Partner>;

  const mockPartner = {
    id: 'uuid-1',
    tenantId: 'tenant-1',
    code: 'PART-001',
    name: 'Test Partner',
    type: PartnerType.SUPPLIER,
    status: PartnerStatus.ACTIVE,
    taxId: 'TAX-001',
    email: 'partner@test.com',
    phone: '+1234567890',
    website: 'https://partner.com',
    address: {
      street: '123 Partner St',
      city: 'Partner City',
      state: 'PC',
      zipCode: '12345',
      country: 'US',
    },
    contact: {
      name: 'John Contact',
      email: 'john@partner.com',
      phone: '+1234567890',
      position: 'Sales Manager',
    },
    paymentTerms: {
      days: 30,
      method: 'TRANSFER',
      currency: 'USD',
    },
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PartnersService,
        {
          provide: getRepositoryToken(Partner),
          useValue: {
            findOne: jest.fn(),
            find: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            remove: jest.fn(),
            createQueryBuilder: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<PartnersService>(PartnersService);
    partnerRepository = module.get<Repository<Partner>>(getRepositoryToken(Partner));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('create', () => {
    it('should create a new partner successfully', async () => {
      const dto: CreatePartnerDto = {
        code: 'PART-001',
        name: 'Test Partner',
        type: PartnerType.SUPPLIER,
        taxId: 'TAX-001',
        email: 'partner@test.com',
        phone: '+1234567890',
        website: 'https://partner.com',
        address: {
          street: '123 Partner St',
          city: 'Partner City',
          state: 'PC',
          zipCode: '12345',
          country: 'US',
        },
        contact: {
          name: 'John Contact',
          email: 'john@partner.com',
          phone: '+1234567890',
          position: 'Sales Manager',
        },
        paymentTerms: {
          days: 30,
          method: 'TRANSFER',
          currency: 'USD',
        },
      };

      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(partnerRepository, 'create').mockReturnValue(mockPartner as any);
      jest.spyOn(partnerRepository, 'save').mockResolvedValue(mockPartner);

      const result = await service.create(dto);

      expect(partnerRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1', code: dto.code },
      });
      expect(partnerRepository.create).toHaveBeenCalled();
      expect(partnerRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockPartner);
    });

    it('should throw error if partner code already exists', async () => {
      const dto: CreatePartnerDto = {
        code: 'PART-001',
        name: 'Test Partner',
        type: PartnerType.SUPPLIER,
      };

      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);

      await expect(service.create(dto)).rejects.toThrow('Partner code already exists');
    });
  });

  describe('findById', () => {
    it('should find partner by id', async () => {
      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);

      const result = await service.findById('uuid-1');

      expect(partnerRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'uuid-1' },
      });
      expect(result).toEqual(mockPartner);
    });

    it('should return null if partner not found', async () => {
      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);

      const result = await service.findById('invalid-id');

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

  describe('findByTenant', () => {
    it('should find partners by tenant', async () => {
      const mockPartners = [mockPartner, { ...mockPartner, id: 'uuid-2' }];
      const mockQueryBuilder = {
        leftJoinAndSelect: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue(mockPartners),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      const result = await service.findByTenant('tenant-1', {
        page: 1,
        limit: 10,
      });

      expect(partnerRepository.createQueryBuilder).toHaveBeenCalledWith('partner');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('partner.tenantId = :tenantId', { tenantId: 'tenant-1' });
      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(0);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
      expect(result).toEqual(mockPartners);
    });

    it('should filter by type', async () => {
      const mockQueryBuilder = {
        leftJoinAndSelect: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockPartner]),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      await service.findByTenant('tenant-1', { type: PartnerType.SUPPLIER });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('partner.type = :type', { type: PartnerType.SUPPLIER });
    });

    it('should filter by status', async () => {
      const mockQueryBuilder = {
        leftJoinAndSelect: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockPartner]),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      await service.findByTenant('tenant-1', { status: PartnerStatus.ACTIVE });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('partner.status = :status', { status: PartnerStatus.ACTIVE });
    });

    it('should search by name or code', async () => {
      const mockQueryBuilder = {
        leftJoinAndSelect: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockPartner]),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      await service.findByTenant('tenant-1', { search: 'Test' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        '(partner.code ILIKE :search OR partner.name ILIKE :search OR partner.email ILIKE :search)',
        { search: '%Test%' }
      );
    });
  });

  describe('update', () => {
    it('should update partner successfully', async () => {
      const dto: UpdatePartnerDto = {
        name: 'Updated Partner',
        status: PartnerStatus.INACTIVE,
      };

      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
      jest.spyOn(partnerRepository, 'save').mockResolvedValue({
        ...mockPartner,
        name: 'Updated Partner',
        status: PartnerStatus.INACTIVE,
      } as any);

      const result = await service.update('uuid-1', dto);

      expect(partnerRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(partnerRepository.save).toHaveBeenCalled();
      expect(result.name).toBe('Updated Partner');
      expect(result.status).toBe(PartnerStatus.INACTIVE);
    });

    it('should throw error if partner not found', async () => {
      const dto: UpdatePartnerDto = { name: 'Updated' };

      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);

      await expect(service.update('invalid-id', dto)).rejects.toThrow('Partner not found');
    });
  });

  describe('delete', () => {
    it('should delete partner successfully', async () => {
      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
      jest.spyOn(partnerRepository, 'remove').mockResolvedValue(undefined);

      await service.delete('uuid-1');

      expect(partnerRepository.remove).toHaveBeenCalledWith(mockPartner);
    });

    it('should throw error if partner not found', async () => {
      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);

      await expect(service.delete('invalid-id')).rejects.toThrow('Partner not found');
    });
  });

  describe('updateStatus', () => {
    it('should update partner status', async () => {
      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
      jest.spyOn(partnerRepository, 'save').mockResolvedValue({
        ...mockPartner,
        status: PartnerStatus.INACTIVE,
      } as any);

      const result = await service.updateStatus('uuid-1', PartnerStatus.INACTIVE);

      expect(result.status).toBe(PartnerStatus.INACTIVE);
    });
  });

  describe('findByType', () => {
    it('should find partners by type', async () => {
      jest.spyOn(partnerRepository, 'find').mockResolvedValue([mockPartner] as any);

      const result = await service.findByType('tenant-1', PartnerType.SUPPLIER);

      expect(partnerRepository.find).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1', type: PartnerType.SUPPLIER },
        order: { name: 'ASC' },
      });
      expect(result).toEqual([mockPartner]);
    });
  });

  describe('getSuppliers', () => {
    it('should get active suppliers', async () => {
      jest.spyOn(partnerRepository, 'find').mockResolvedValue([mockPartner] as any);

      const result = await service.getSuppliers('tenant-1');

      expect(partnerRepository.find).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1', type: PartnerType.SUPPLIER, status: PartnerStatus.ACTIVE },
        order: { name: 'ASC' },
      });
      expect(result).toEqual([mockPartner]);
    });
  });

  describe('getCustomers', () => {
    it('should get active customers', async () => {
      const customerPartner = { ...mockPartner, type: PartnerType.CUSTOMER };
      jest.spyOn(partnerRepository, 'find').mockResolvedValue([customerPartner] as any);

      const result = await service.getCustomers('tenant-1');

      expect(partnerRepository.find).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1', type: PartnerType.CUSTOMER, status: PartnerStatus.ACTIVE },
        order: { name: 'ASC' },
      });
      expect(result).toEqual([customerPartner]);
    });
  });

  describe('getPartnerStats', () => {
    it('should get partner statistics', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        select: jest.fn().mockReturnThis(),
        getRawOne: jest.fn().mockResolvedValue({ total: 100, active: 80, inactive: 20 }),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      const result = await service.getPartnerStats('tenant-1');

      expect(result.totalPartners).toBe(100);
      expect(result.activePartners).toBe(80);
      expect(result.inactivePartners).toBe(20);
    });
  });

  describe('searchPartners', () => {
    it('should search partners by query', async () => {
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockPartner]),
      };

      jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      const result = await service.searchPartners('tenant-1', 'Test', 10);

      expect(partnerRepository.createQueryBuilder).toHaveBeenCalledWith('partner');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('partner.tenantId = :tenantId', { tenantId: 'tenant-1' });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        '(partner.name ILIKE :query OR partner.code ILIKE :query OR partner.email ILIKE :query)',
        { query: '%Test%' }
      );
      expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
      expect(result).toEqual([mockPartner]);
    });
  });

  describe('bulkUpdate', () => {
    it('should update multiple partners', async () => {
      const updates = [
        { id: 'uuid-1', status: PartnerStatus.INACTIVE },
        { id: 'uuid-2', status: PartnerStatus.INACTIVE },
      ];

      jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
      jest.spyOn(partnerRepository, 'save').mockResolvedValue(mockPartner as any);

      const result = await service.bulkUpdate(updates);

      expect(result).toHaveLength(2);
      expect(partnerRepository.save).toHaveBeenCalledTimes(2);
    });
  });
});
