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

// Mock factories
function createMockSubscriptionPlan(overrides: Record<string, any> = {}) {
  return {
    id: 'plan-uuid-1',
    code: 'STARTER',
    name: 'Starter Plan',
    baseMonthlyPrice: 499,
    baseAnnualPrice: 4990,
    maxUsers: 5,
    maxBranches: 1,
    ...overrides,
  };
}

function createMockSubscription(overrides: Record<string, any> = {}) {
  return {
    id: 'sub-uuid-1',
    tenantId: 'tenant-uuid-1',
    planId: 'plan-uuid-1',
    billingCycle: 'monthly',
    currentPeriodStart: new Date('2026-01-01'),
    currentPeriodEnd: new Date('2026-02-01'),
    status: 'active',
    trialStart: null,
    trialEnd: null,
    billingEmail: 'billing@example.com',
    billingName: 'Test Company',
    billingAddress: {},
    taxId: 'RFC123456',
    paymentMethodId: null,
    paymentProvider: null,
    currentPrice: 499,
    discountPercent: 0,
    discountReason: null,
    contractedUsers: 5,
    contractedBranches: 1,
    autoRenew: true,
    nextInvoiceDate: new Date('2026-02-01'),
    cancelAtPeriodEnd: false,
    cancelledAt: null,
    cancellationReason: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    plan: createMockSubscriptionPlan(),
    ...overrides,
  };
}

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

// Mock DataSource
const mockDataSource = {
  getRepository: jest.fn((entity: any) => {
    const entityName = entity.name || entity;
    if (entityName === 'TenantSubscription') return mockSubscriptionRepository;
    if (entityName === 'SubscriptionPlan') return mockPlanRepository;
    return mockSubscriptionRepository;
  }),
};

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

// Import after mocking
import { SubscriptionsService } from '../services/subscriptions.service.js';

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

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

  describe('create', () => {
    it('should create a new subscription successfully', async () => {
      const dto = {
        tenantId: 'tenant-uuid-new',
        planId: 'plan-uuid-1',
        billingEmail: 'test@example.com',
        currentPrice: 499,
      };

      const mockPlan = createMockSubscriptionPlan();
      const mockSub = createMockSubscription({ tenantId: dto.tenantId });

      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockSubscriptionRepository.create.mockReturnValue(mockSub);
      mockSubscriptionRepository.save.mockResolvedValue(mockSub);

      const result = await service.create(dto);

      expect(mockSubscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-uuid-new' },
      });
      expect(mockPlanRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'plan-uuid-1' },
      });
      expect(result.tenantId).toBe('tenant-uuid-new');
    });

    it('should throw error if tenant already has subscription', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        planId: 'plan-uuid-1',
        currentPrice: 499,
      };

      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());

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

    it('should throw error if plan not found', async () => {
      const dto = {
        tenantId: 'tenant-uuid-new',
        planId: 'invalid-plan',
        currentPrice: 499,
      };

      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(null);

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

    it('should create subscription with trial', async () => {
      const dto = {
        tenantId: 'tenant-uuid-new',
        planId: 'plan-uuid-1',
        currentPrice: 499,
        startWithTrial: true,
        trialDays: 14,
      };

      const mockPlan = createMockSubscriptionPlan();
      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockSubscriptionRepository.create.mockImplementation((data: any) => ({
        ...data,
        id: 'new-sub-id',
      }));
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.create(dto);

      expect(mockSubscriptionRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          status: 'trial',
        })
      );
      expect(result.trialStart).toBeDefined();
      expect(result.trialEnd).toBeDefined();
    });
  });

  describe('findByTenantId', () => {
    it('should return subscription with plan relation', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);

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

      expect(mockSubscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId: 'tenant-uuid-1' },
        relations: ['plan'],
      });
      expect(result?.tenantId).toBe('tenant-uuid-1');
    });

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

      const result = await service.findByTenantId('non-existent');

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

  describe('findById', () => {
    it('should return subscription by id', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);

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

      expect(mockSubscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'sub-uuid-1' },
        relations: ['plan'],
      });
      expect(result?.id).toBe('sub-uuid-1');
    });
  });

  describe('update', () => {
    it('should update subscription successfully', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.update('sub-uuid-1', {
        billingEmail: 'new@example.com',
      });

      expect(result.billingEmail).toBe('new@example.com');
    });

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

      await expect(service.update('invalid-id', { billingEmail: 'test@example.com' }))
        .rejects.toThrow('Subscription not found');
    });

    it('should validate plan when changing plan', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockPlanRepository.findOne.mockResolvedValue(null);

      await expect(service.update('sub-uuid-1', { planId: 'new-plan-id' }))
        .rejects.toThrow('Plan not found');
    });
  });

  describe('cancel', () => {
    it('should cancel at period end by default', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.cancel('sub-uuid-1', { reason: 'Too expensive' });

      expect(result.cancelAtPeriodEnd).toBe(true);
      expect(result.autoRenew).toBe(false);
      expect(result.cancellationReason).toBe('Too expensive');
      expect(result.status).toBe('active'); // Still active until period end
    });

    it('should cancel immediately when specified', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.cancel('sub-uuid-1', {
        reason: 'Closing business',
        cancelImmediately: true,
      });

      expect(result.status).toBe('cancelled');
    });

    it('should throw error if already cancelled', async () => {
      const mockSub = createMockSubscription({ status: 'cancelled' });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);

      await expect(service.cancel('sub-uuid-1', {}))
        .rejects.toThrow('Subscription is already cancelled');
    });

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

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

  describe('reactivate', () => {
    it('should reactivate cancelled subscription', async () => {
      const mockSub = createMockSubscription({ status: 'cancelled', cancelAtPeriodEnd: false });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('active');
      expect(result.cancelAtPeriodEnd).toBe(false);
      expect(result.autoRenew).toBe(true);
    });

    it('should reactivate subscription pending cancellation', async () => {
      const mockSub = createMockSubscription({ status: 'active', cancelAtPeriodEnd: true });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.cancelAtPeriodEnd).toBe(false);
      expect(result.autoRenew).toBe(true);
    });

    it('should throw error if not cancelled', async () => {
      const mockSub = createMockSubscription({ status: 'active', cancelAtPeriodEnd: false });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);

      await expect(service.reactivate('sub-uuid-1'))
        .rejects.toThrow('Subscription is not cancelled');
    });
  });

  describe('changePlan', () => {
    it('should change to new plan', async () => {
      const mockSub = createMockSubscription();
      const newPlan = createMockSubscriptionPlan({
        id: 'plan-uuid-2',
        code: 'PRO',
        baseMonthlyPrice: 999,
        maxUsers: 20,
        maxBranches: 5,
      });

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockPlanRepository.findOne.mockResolvedValue(newPlan);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.changePlan('sub-uuid-1', { newPlanId: 'plan-uuid-2' });

      expect(result.planId).toBe('plan-uuid-2');
      expect(result.currentPrice).toBe(999);
      expect(result.contractedUsers).toBe(20);
      expect(result.contractedBranches).toBe(5);
    });

    it('should throw error if new plan not found', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockPlanRepository.findOne.mockResolvedValue(null);

      await expect(service.changePlan('sub-uuid-1', { newPlanId: 'invalid-plan' }))
        .rejects.toThrow('New plan not found');
    });

    it('should apply existing discount to new plan price', async () => {
      const mockSub = createMockSubscription({ discountPercent: 20 });
      const newPlan = createMockSubscriptionPlan({
        id: 'plan-uuid-2',
        baseMonthlyPrice: 1000,
      });

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockPlanRepository.findOne.mockResolvedValue(newPlan);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.changePlan('sub-uuid-1', { newPlanId: 'plan-uuid-2' });

      expect(result.currentPrice).toBe(800); // 1000 - 20%
    });
  });

  describe('setPaymentMethod', () => {
    it('should set payment method', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

      const result = await service.setPaymentMethod('sub-uuid-1', {
        paymentMethodId: 'pm_123',
        paymentProvider: 'stripe',
      });

      expect(result.paymentMethodId).toBe('pm_123');
      expect(result.paymentProvider).toBe('stripe');
    });
  });

  describe('renew', () => {
    it('should renew subscription and advance period', async () => {
      const mockSub = createMockSubscription({
        currentPeriodStart: new Date('2026-01-01'),
        currentPeriodEnd: new Date('2026-02-01'),
      });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.currentPeriodStart.getTime()).toBe(new Date('2026-02-01').getTime());
    });

    it('should cancel if cancelAtPeriodEnd is true', async () => {
      const mockSub = createMockSubscription({ cancelAtPeriodEnd: true });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('cancelled');
    });

    it('should throw error if autoRenew is disabled', async () => {
      const mockSub = createMockSubscription({ autoRenew: false });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);

      await expect(service.renew('sub-uuid-1'))
        .rejects.toThrow('Subscription auto-renew is disabled');
    });

    it('should transition from trial to active', async () => {
      const mockSub = createMockSubscription({ status: 'trial' });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('active');
    });
  });

  describe('status updates', () => {
    it('should mark as past due', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('past_due');
    });

    it('should suspend subscription', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('suspended');
    });

    it('should activate subscription', async () => {
      const mockSub = createMockSubscription({ status: 'suspended' });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockSubscriptionRepository.save.mockImplementation((sub: any) => Promise.resolve(sub));

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

      expect(result.status).toBe('active');
    });
  });

  describe('findExpiringSoon', () => {
    it('should find subscriptions expiring within days', async () => {
      const mockSubs = [createMockSubscription()];
      mockQueryBuilder.getMany.mockResolvedValue(mockSubs);

      const result = await service.findExpiringSoon(7);

      expect(mockSubscriptionRepository.createQueryBuilder).toHaveBeenCalledWith('sub');
      expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledWith('sub.plan', 'plan');
      expect(result).toHaveLength(1);
    });
  });

  describe('findTrialsEndingSoon', () => {
    it('should find trials ending within days', async () => {
      const mockSubs = [createMockSubscription({ status: 'trial' })];
      mockQueryBuilder.getMany.mockResolvedValue(mockSubs);

      const result = await service.findTrialsEndingSoon(3);

      expect(mockQueryBuilder.where).toHaveBeenCalledWith("sub.status = 'trial'");
      expect(result).toHaveLength(1);
    });
  });

  describe('getStats', () => {
    it('should return subscription statistics', async () => {
      const mockSubs = [
        createMockSubscription({ status: 'active', currentPrice: 499, plan: { code: 'STARTER' } }),
        createMockSubscription({ status: 'active', currentPrice: 999, plan: { code: 'PRO' } }),
        createMockSubscription({ status: 'trial', currentPrice: 499, plan: { code: 'STARTER' } }),
        createMockSubscription({ status: 'cancelled', currentPrice: 499, plan: { code: 'STARTER' } }),
      ];
      mockSubscriptionRepository.find.mockResolvedValue(mockSubs);

      const result = await service.getStats();

      expect(result.total).toBe(4);
      expect(result.byStatus.active).toBe(2);
      expect(result.byStatus.trial).toBe(1);
      expect(result.byStatus.cancelled).toBe(1);
      expect(result.byPlan['STARTER']).toBe(3);
      expect(result.byPlan['PRO']).toBe(1);
      expect(result.totalMRR).toBe(499 + 999 + 499); // Active and trial subscriptions
      expect(result.totalARR).toBe(result.totalMRR * 12);
    });
  });
});
