import { Test, TestingModule } from '@nestjs/testing';
import { DataSource, Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { SubscriptionsService } from '../services/subscriptions.service';
import { TenantSubscription, SubscriptionPlan, BillingCycle, SubscriptionStatus } from '../entities';
import { CreateTenantSubscriptionDto, UpdateTenantSubscriptionDto, CancelSubscriptionDto, ChangePlanDto } from '../dto';

describe('SubscriptionsService', () => {
  let service: SubscriptionsService;
  let subscriptionRepository: Repository<TenantSubscription>;
  let planRepository: Repository<SubscriptionPlan>;
  let dataSource: DataSource;

  const mockSubscription = {
    id: 'uuid-1',
    tenantId: 'tenant-1',
    planId: 'plan-1',
    status: SubscriptionStatus.ACTIVE,
    billingCycle: BillingCycle.MONTHLY,
    currentPeriodStart: new Date('2024-01-01'),
    currentPeriodEnd: new Date('2024-02-01'),
    trialEnd: null,
    cancelledAt: null,
    paymentMethodId: 'pm-1',
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockPlan = {
    id: 'plan-1',
    name: 'Basic Plan',
    description: 'Basic subscription plan',
    price: 9.99,
    billingCycle: BillingCycle.MONTHLY,
    features: ['feature1', 'feature2'],
    isActive: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SubscriptionsService,
        {
          provide: DataSource,
          useValue: {
            getRepository: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<SubscriptionsService>(SubscriptionsService);
    dataSource = module.get<DataSource>(DataSource);
    subscriptionRepository = module.get<Repository<TenantSubscription>>(
      getRepositoryToken(TenantSubscription),
    );
    planRepository = module.get<Repository<SubscriptionPlan>>(
      getRepositoryToken(SubscriptionPlan),
    );
  });

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

  describe('create', () => {
    it('should create a new subscription successfully', async () => {
      const dto: CreateTenantSubscriptionDto = {
        tenantId: 'tenant-1',
        planId: 'plan-1',
        billingCycle: BillingCycle.MONTHLY,
        paymentMethodId: 'pm-1',
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(planRepository, 'findOne').mockResolvedValue(mockPlan as any);
      jest.spyOn(subscriptionRepository, 'create').mockReturnValue(mockSubscription as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(mockSubscription);

      const result = await service.create(dto);

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: dto.tenantId },
      });
      expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: dto.planId } });
      expect(subscriptionRepository.create).toHaveBeenCalled();
      expect(subscriptionRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockSubscription);
    });

    it('should throw error if tenant already has subscription', async () => {
      const dto: CreateTenantSubscriptionDto = {
        tenantId: 'tenant-1',
        planId: 'plan-1',
        billingCycle: BillingCycle.MONTHLY,
        paymentMethodId: 'pm-1',
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);

      await expect(service.create(dto)).rejects.toThrow('Tenant already has a subscription');
    });

    it('should throw error if plan not found', async () => {
      const dto: CreateTenantSubscriptionDto = {
        tenantId: 'tenant-1',
        planId: 'invalid-plan',
        billingCycle: BillingCycle.MONTHLY,
        paymentMethodId: 'pm-1',
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
      jest.spyOn(planRepository, 'findOne').mockResolvedValue(null);

      await expect(service.create(dto)).rejects.toThrow('Plan not found');
    });
  });

  describe('findByTenant', () => {
    it('should find subscription by tenant id', async () => {
      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);

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

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-1' },
      });
      expect(result).toEqual(mockSubscription);
    });

    it('should return null if no subscription found', async () => {
      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);

      const result = await service.findByTenant('invalid-tenant');

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

  describe('update', () => {
    it('should update subscription successfully', async () => {
      const dto: UpdateTenantSubscriptionDto = {
        paymentMethodId: 'pm-2',
      };

      const updatedSubscription = { ...mockSubscription, paymentMethodId: 'pm-2' };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(updatedSubscription as any);

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

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(subscriptionRepository.save).toHaveBeenCalled();
      expect(result).toEqual(updatedSubscription);
    });

    it('should throw error if subscription not found', async () => {
      const dto: UpdateTenantSubscriptionDto = {
        paymentMethodId: 'pm-2',
      };

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

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

  describe('cancel', () => {
    it('should cancel subscription successfully', async () => {
      const dto: CancelSubscriptionDto = {
        reason: 'Customer request',
        effectiveImmediately: false,
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
        ...mockSubscription,
        status: SubscriptionStatus.CANCELLED,
        cancelledAt: new Date(),
      } as any);

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

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(subscriptionRepository.save).toHaveBeenCalled();
      expect(result.status).toBe(SubscriptionStatus.CANCELLED);
      expect(result.cancelledAt).toBeDefined();
    });

    it('should cancel subscription immediately if requested', async () => {
      const dto: CancelSubscriptionDto = {
        reason: 'Customer request',
        effectiveImmediately: true,
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
        ...mockSubscription,
        status: SubscriptionStatus.CANCELLED,
        cancelledAt: new Date(),
        currentPeriodEnd: new Date(),
      } as any);

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

      expect(result.status).toBe(SubscriptionStatus.CANCELLED);
      expect(result.cancelledAt).toBeDefined();
    });
  });

  describe('changePlan', () => {
    it('should change subscription plan successfully', async () => {
      const newPlan = { ...mockPlan, id: 'plan-2', price: 19.99 };
      const dto: ChangePlanDto = {
        newPlanId: 'plan-2',
        billingCycle: BillingCycle.YEARLY,
        prorate: true,
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(planRepository, 'findOne').mockResolvedValue(newPlan as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
        ...mockSubscription,
        planId: 'plan-2',
        billingCycle: BillingCycle.YEARLY,
      } as any);

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

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: 'plan-2' } });
      expect(subscriptionRepository.save).toHaveBeenCalled();
      expect(result.planId).toBe('plan-2');
      expect(result.billingCycle).toBe(BillingCycle.YEARLY);
    });

    it('should throw error if new plan not found', async () => {
      const dto: ChangePlanDto = {
        newPlanId: 'invalid-plan',
        billingCycle: BillingCycle.MONTHLY,
        prorate: false,
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(planRepository, 'findOne').mockResolvedValue(null);

      await expect(service.changePlan('uuid-1', dto)).rejects.toThrow('New plan not found');
    });
  });

  describe('getUsage', () => {
    it('should get subscription usage', async () => {
      const mockUsage = {
        currentUsage: 850,
        limits: {
          apiCalls: 1000,
          storage: 5368709120, // 5GB in bytes
          users: 10,
        },
        periodStart: new Date('2024-01-01'),
        periodEnd: new Date('2024-02-01'),
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
      jest.spyOn(dataSource, 'query').mockResolvedValue([{ current_usage: 850 }]);

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

      expect(result.currentUsage).toBe(850);
      expect(result.limits).toBeDefined();
    });
  });

  describe('reactivate', () => {
    it('should reactivate cancelled subscription', async () => {
      const cancelledSubscription = {
        ...mockSubscription,
        status: SubscriptionStatus.CANCELLED,
        cancelledAt: new Date(),
      };

      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(cancelledSubscription as any);
      jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
        ...cancelledSubscription,
        status: SubscriptionStatus.ACTIVE,
        cancelledAt: null,
      } as any);

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

      expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(subscriptionRepository.save).toHaveBeenCalled();
      expect(result.status).toBe(SubscriptionStatus.ACTIVE);
      expect(result.cancelledAt).toBeNull();
    });

    it('should throw error if subscription is not cancelled', async () => {
      jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);

      await expect(service.reactivate('uuid-1')).rejects.toThrow('Cannot reactivate active subscription');
    });
  });
});
