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

// Mock factories for billing entities
function createMockCoupon(overrides: Record<string, any> = {}) {
  return {
    id: 'coupon-uuid-1',
    code: 'SAVE20',
    name: '20% Discount',
    description: 'Get 20% off your subscription',
    discountType: 'percentage',
    discountValue: 20,
    currency: 'MXN',
    applicablePlans: [],
    minAmount: 0,
    durationPeriod: 'once',
    durationMonths: null,
    maxRedemptions: 100,
    currentRedemptions: 10,
    validFrom: new Date('2024-01-01'),
    validUntil: new Date('2030-12-31'),
    isActive: true,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockCouponRedemption(overrides: Record<string, any> = {}) {
  return {
    id: 'redemption-uuid-1',
    couponId: 'coupon-uuid-1',
    tenantId: 'tenant-uuid-1',
    subscriptionId: 'subscription-uuid-1',
    discountAmount: 200,
    expiresAt: null,
    createdAt: new Date(),
    ...overrides,
  };
}

// Mock repositories
const mockCouponRepository = createMockRepository();
const mockRedemptionRepository = createMockRepository();
const mockSubscriptionRepository = createMockRepository();
const mockQueryBuilder = createMockQueryBuilder();

// Mock transaction manager
const mockManager = {
  save: jest.fn().mockResolvedValue({}),
};

// Mock DataSource with transaction
const mockDataSource = {
  getRepository: jest.fn((entity: any) => {
    const entityName = entity.name || entity;
    if (entityName === 'Coupon') return mockCouponRepository;
    if (entityName === 'CouponRedemption') return mockRedemptionRepository;
    if (entityName === 'TenantSubscription') return mockSubscriptionRepository;
    return mockCouponRepository;
  }),
  transaction: jest.fn((callback: (manager: any) => Promise<void>) => callback(mockManager)),
};

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

// Import after mocking
import { CouponsService } from '../services/coupons.service.js';

describe('CouponsService', () => {
  let service: CouponsService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new CouponsService(mockDataSource as any);
    mockCouponRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
  });

  describe('create', () => {
    it('should create a new coupon successfully', async () => {
      const dto = {
        code: 'NEWCODE',
        name: 'New Discount',
        discountType: 'percentage' as const,
        discountValue: 15,
        validFrom: new Date(),
        validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
      };

      const mockCoupon = createMockCoupon({ ...dto, id: 'new-coupon-uuid', code: 'NEWCODE' });
      mockCouponRepository.findOne.mockResolvedValue(null);
      mockCouponRepository.create.mockReturnValue(mockCoupon);
      mockCouponRepository.save.mockResolvedValue(mockCoupon);

      const result = await service.create(dto);

      expect(result.code).toBe('NEWCODE');
      expect(mockCouponRepository.create).toHaveBeenCalled();
      expect(mockCouponRepository.save).toHaveBeenCalled();
    });

    it('should throw error if coupon code already exists', async () => {
      const dto = {
        code: 'EXISTING',
        name: 'Existing Discount',
        discountType: 'percentage' as const,
        discountValue: 10,
        validFrom: new Date(),
        validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
      };

      mockCouponRepository.findOne.mockResolvedValue(createMockCoupon({ code: 'EXISTING' }));

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

  describe('findByCode', () => {
    it('should find a coupon by code', async () => {
      const mockCoupon = createMockCoupon({ code: 'TESTCODE' });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);

      const result = await service.findByCode('TESTCODE');

      expect(result).toBeDefined();
      expect(result?.code).toBe('TESTCODE');
      expect(mockCouponRepository.findOne).toHaveBeenCalledWith({
        where: { code: 'TESTCODE' },
      });
    });

    it('should return null if coupon not found', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      const result = await service.findByCode('NOTFOUND');

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

  describe('validateCoupon', () => {
    it('should validate an active coupon successfully', async () => {
      const mockCoupon = createMockCoupon({
        code: 'VALID',
        isActive: true,
        validFrom: new Date('2023-01-01'),
        validUntil: new Date('2030-12-31'),
        maxRedemptions: 100,
        currentRedemptions: 10,
        applicablePlans: [],
        minAmount: 0,
      });

      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockRedemptionRepository.findOne.mockResolvedValue(null);

      const result = await service.validateCoupon('VALID', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Cupón válido');
    });

    it('should reject inactive coupon', async () => {
      const mockCoupon = createMockCoupon({ isActive: false });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);

      const result = await service.validateCoupon('INACTIVE', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón inactivo');
    });

    it('should reject coupon not yet valid', async () => {
      const mockCoupon = createMockCoupon({
        isActive: true,
        validFrom: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Future date
      });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);

      const result = await service.validateCoupon('FUTURE', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón aún no válido');
    });

    it('should reject expired coupon', async () => {
      const mockCoupon = createMockCoupon({
        isActive: true,
        validFrom: new Date('2020-01-01'),
        validUntil: new Date('2020-12-31'), // Past date
      });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);

      const result = await service.validateCoupon('EXPIRED', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón expirado');
    });

    it('should reject coupon exceeding max redemptions', async () => {
      const mockCoupon = createMockCoupon({
        isActive: true,
        validFrom: new Date('2023-01-01'),
        validUntil: new Date('2030-12-31'),
        maxRedemptions: 10,
        currentRedemptions: 10,
      });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);

      const result = await service.validateCoupon('MAXED', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón agotado');
    });

    it('should reject if tenant already redeemed', async () => {
      const mockCoupon = createMockCoupon({
        isActive: true,
        validFrom: new Date('2023-01-01'),
        validUntil: new Date('2030-12-31'),
        maxRedemptions: 100,
        currentRedemptions: 10,
      });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockRedemptionRepository.findOne.mockResolvedValue(createMockCouponRedemption());

      const result = await service.validateCoupon('ONCEONLY', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón ya utilizado');
    });

    it('should reject if coupon not found', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      const result = await service.validateCoupon('NOTFOUND', 'tenant-uuid-1', 'plan-uuid-1', 1000);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Cupón no encontrado');
    });
  });

  describe('applyCoupon', () => {
    it('should apply percentage discount correctly', async () => {
      const mockCoupon = createMockCoupon({
        id: 'coupon-uuid-1',
        discountType: 'percentage',
        discountValue: 20,
        isActive: true,
        validFrom: new Date('2023-01-01'),
        validUntil: new Date('2030-12-31'),
        maxRedemptions: 100,
        currentRedemptions: 10,
        applicablePlans: [],
        minAmount: 0,
      });

      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockRedemptionRepository.findOne.mockResolvedValue(null); // No existing redemption
      mockRedemptionRepository.create.mockReturnValue(createMockCouponRedemption({ discountAmount: 200 }));

      const result = await service.applyCoupon('SAVE20', 'tenant-uuid-1', 'subscription-uuid-1', 1000);

      expect(result.discountAmount).toBe(200); // 20% of 1000
      expect(mockManager.save).toHaveBeenCalled();
    });

    it('should apply fixed discount correctly', async () => {
      const mockCoupon = createMockCoupon({
        id: 'coupon-uuid-1',
        discountType: 'fixed',
        discountValue: 150,
        isActive: true,
        validFrom: new Date('2023-01-01'),
        validUntil: new Date('2030-12-31'),
        maxRedemptions: 100,
        currentRedemptions: 10,
        applicablePlans: [],
        minAmount: 0,
      });

      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockRedemptionRepository.findOne.mockResolvedValue(null);
      mockRedemptionRepository.create.mockReturnValue(createMockCouponRedemption({ discountAmount: 150 }));

      const result = await service.applyCoupon('FIXED150', 'tenant-uuid-1', 'subscription-uuid-1', 1000);

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

    it('should throw error if coupon is invalid', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      await expect(
        service.applyCoupon('INVALID', 'tenant-uuid-1', 'subscription-uuid-1', 1000)
      ).rejects.toThrow('Cupón no encontrado');
    });
  });

  describe('findAll', () => {
    it('should return all coupons', async () => {
      const mockCoupons = [
        createMockCoupon({ code: 'CODE1' }),
        createMockCoupon({ code: 'CODE2' }),
      ];

      mockQueryBuilder.getMany.mockResolvedValue(mockCoupons);

      const result = await service.findAll();

      expect(result).toHaveLength(2);
    });

    it('should filter by active status', async () => {
      const mockCoupons = [createMockCoupon({ isActive: true })];
      mockQueryBuilder.getMany.mockResolvedValue(mockCoupons);

      await service.findAll({ isActive: true });

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

  describe('getStats', () => {
    it('should return coupon statistics', async () => {
      const mockCoupon = createMockCoupon({
        maxRedemptions: 100,
        currentRedemptions: 25,
      });
      const mockRedemptions = [
        createMockCouponRedemption({ discountAmount: 200 }),
        createMockCouponRedemption({ discountAmount: 300 }),
      ];

      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockRedemptionRepository.find.mockResolvedValue(mockRedemptions);

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

      expect(result.totalRedemptions).toBe(2);
      expect(result.totalDiscountGiven).toBe(500);
    });

    it('should throw error if coupon not found', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      await expect(service.getStats('nonexistent')).rejects.toThrow('Coupon not found');
    });
  });

  describe('deactivate', () => {
    it('should deactivate a coupon', async () => {
      const mockCoupon = createMockCoupon({ isActive: true });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockCouponRepository.save.mockResolvedValue({ ...mockCoupon, isActive: false });

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

      expect(result.isActive).toBe(false);
      expect(mockCouponRepository.save).toHaveBeenCalled();
    });

    it('should throw error if coupon not found', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      await expect(service.deactivate('nonexistent')).rejects.toThrow('Coupon not found');
    });
  });

  describe('update', () => {
    it('should update coupon properties', async () => {
      const mockCoupon = createMockCoupon({ name: 'Old Name' });
      mockCouponRepository.findOne.mockResolvedValue(mockCoupon);
      mockCouponRepository.save.mockResolvedValue({ ...mockCoupon, name: 'New Name' });

      const result = await service.update('coupon-uuid-1', { name: 'New Name' });

      expect(result.name).toBe('New Name');
    });

    it('should throw error if coupon not found', async () => {
      mockCouponRepository.findOne.mockResolvedValue(null);

      await expect(service.update('nonexistent', { name: 'New' })).rejects.toThrow('Coupon not found');
    });
  });

  describe('getActiveRedemptions', () => {
    it('should return active redemptions for tenant', async () => {
      const mockRedemptions = [
        createMockCouponRedemption({ expiresAt: null }),
        createMockCouponRedemption({ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }),
      ];

      mockRedemptionRepository.find.mockResolvedValue(mockRedemptions);

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

      expect(result).toHaveLength(2);
    });
  });
});
