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

// Mock factories for subscription plan entities
function createMockSubscriptionPlan(overrides: Record<string, any> = {}) {
  return {
    id: 'plan-uuid-1',
    code: 'STARTER',
    name: 'Starter Plan',
    description: 'Perfect for small businesses',
    planType: 'saas',
    baseMonthlyPrice: 499,
    baseAnnualPrice: 4990,
    setupFee: 0,
    maxUsers: 5,
    maxBranches: 1,
    storageGb: 10,
    apiCallsMonthly: 10000,
    includedModules: ['core', 'sales', 'inventory'],
    includedPlatforms: ['web'],
    features: { analytics: true, reports: false },
    isActive: true,
    isPublic: true,
    createdAt: new Date(),
    updatedAt: new Date(),
    deletedAt: null,
    ...overrides,
  };
}

// Mock repositories
const mockPlanRepository = {
  ...createMockRepository(),
  softDelete: jest.fn().mockResolvedValue({ affected: 1 }),
};
const mockQueryBuilder = createMockQueryBuilder();

// Mock DataSource
const mockDataSource = {
  getRepository: jest.fn(() => mockPlanRepository),
  createQueryBuilder: jest.fn(() => ({
    select: jest.fn().mockReturnThis(),
    from: jest.fn().mockReturnThis(),
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    getRawOne: jest.fn().mockResolvedValue({ count: '0' }),
  })),
};

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

// Import after mocking
import { SubscriptionPlansService } from '../services/subscription-plans.service.js';

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

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

  describe('create', () => {
    it('should create a new subscription plan successfully', async () => {
      const dto = {
        code: 'NEWPLAN',
        name: 'New Plan',
        baseMonthlyPrice: 999,
        maxUsers: 10,
      };

      const mockPlan = createMockSubscriptionPlan({ ...dto, id: 'new-plan-uuid' });
      mockPlanRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.create.mockReturnValue(mockPlan);
      mockPlanRepository.save.mockResolvedValue(mockPlan);

      const result = await service.create(dto);

      expect(mockPlanRepository.findOne).toHaveBeenCalledWith({ where: { code: 'NEWPLAN' } });
      expect(mockPlanRepository.create).toHaveBeenCalled();
      expect(mockPlanRepository.save).toHaveBeenCalled();
      expect(result.code).toBe('NEWPLAN');
    });

    it('should throw error if plan code already exists', async () => {
      const dto = {
        code: 'STARTER',
        name: 'Duplicate Plan',
        baseMonthlyPrice: 999,
      };

      mockPlanRepository.findOne.mockResolvedValue(createMockSubscriptionPlan());

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

    it('should use default values when not provided', async () => {
      const dto = {
        code: 'MINIMAL',
        name: 'Minimal Plan',
        baseMonthlyPrice: 199,
      };

      mockPlanRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.create.mockImplementation((data: any) => ({
        ...data,
        id: 'minimal-plan-uuid',
      }));
      mockPlanRepository.save.mockImplementation((plan: any) => Promise.resolve(plan));

      await service.create(dto);

      expect(mockPlanRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          planType: 'saas',
          setupFee: 0,
          maxUsers: 5,
          maxBranches: 1,
          storageGb: 10,
          apiCallsMonthly: 10000,
          includedModules: [],
          includedPlatforms: ['web'],
          features: {},
          isActive: true,
          isPublic: true,
        })
      );
    });
  });

  describe('findAll', () => {
    it('should return all plans without filters', async () => {
      const mockPlans = [
        createMockSubscriptionPlan({ id: 'plan-1', code: 'STARTER' }),
        createMockSubscriptionPlan({ id: 'plan-2', code: 'PRO' }),
      ];
      mockQueryBuilder.getMany.mockResolvedValue(mockPlans);

      const result = await service.findAll();

      expect(mockPlanRepository.createQueryBuilder).toHaveBeenCalledWith('plan');
      expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('plan.baseMonthlyPrice', 'ASC');
      expect(result).toHaveLength(2);
    });

    it('should filter by isActive', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([createMockSubscriptionPlan()]);

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

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

    it('should filter by isPublic', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);

      await service.findAll({ isPublic: false });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'plan.isPublic = :isPublic',
        { isPublic: false }
      );
    });

    it('should filter by planType', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);

      await service.findAll({ planType: 'on_premise' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'plan.planType = :planType',
        { planType: 'on_premise' }
      );
    });

    it('should apply multiple filters', async () => {
      mockQueryBuilder.getMany.mockResolvedValue([]);

      await service.findAll({ isActive: true, isPublic: true, planType: 'saas' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(3);
    });
  });

  describe('findPublicPlans', () => {
    it('should return only active and public plans', async () => {
      const publicPlans = [createMockSubscriptionPlan({ isActive: true, isPublic: true })];
      mockQueryBuilder.getMany.mockResolvedValue(publicPlans);

      const result = await service.findPublicPlans();

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'plan.isActive = :isActive',
        { isActive: true }
      );
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'plan.isPublic = :isPublic',
        { isPublic: true }
      );
      expect(result).toHaveLength(1);
    });
  });

  describe('findById', () => {
    it('should return plan by id', async () => {
      const mockPlan = createMockSubscriptionPlan();
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);

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

      expect(mockPlanRepository.findOne).toHaveBeenCalledWith({ where: { id: 'plan-uuid-1' } });
      expect(result?.id).toBe('plan-uuid-1');
    });

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

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

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

  describe('findByCode', () => {
    it('should return plan by code', async () => {
      const mockPlan = createMockSubscriptionPlan({ code: 'STARTER' });
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);

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

      expect(mockPlanRepository.findOne).toHaveBeenCalledWith({ where: { code: 'STARTER' } });
      expect(result?.code).toBe('STARTER');
    });

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

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

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

  describe('update', () => {
    it('should update plan successfully', async () => {
      const existingPlan = createMockSubscriptionPlan();
      const updateDto = { name: 'Updated Plan Name', baseMonthlyPrice: 599 };

      mockPlanRepository.findOne.mockResolvedValue(existingPlan);
      mockPlanRepository.save.mockImplementation((plan: any) => Promise.resolve(plan));

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

      expect(mockPlanRepository.findOne).toHaveBeenCalledWith({ where: { id: 'plan-uuid-1' } });
      expect(result.name).toBe('Updated Plan Name');
      expect(result.baseMonthlyPrice).toBe(599);
    });

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

      await expect(service.update('non-existent-id', { name: 'Test' }))
        .rejects.toThrow('Plan not found');
    });
  });

  describe('delete', () => {
    it('should soft delete plan with no active subscriptions', async () => {
      const mockPlan = createMockSubscriptionPlan();
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockDataSource.createQueryBuilder().getRawOne.mockResolvedValue({ count: '0' });

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

      expect(mockPlanRepository.softDelete).toHaveBeenCalledWith('plan-uuid-1');
    });

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

      await expect(service.delete('non-existent-id'))
        .rejects.toThrow('Plan not found');
    });

    it('should throw error if plan has active subscriptions', async () => {
      const mockPlan = createMockSubscriptionPlan();
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);

      // Need to reset the mock to return count > 0 for this test
      const mockQb = {
        select: jest.fn().mockReturnThis(),
        from: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        getRawOne: jest.fn().mockResolvedValue({ count: '5' }),
      };
      mockDataSource.createQueryBuilder.mockReturnValue(mockQb);

      await expect(service.delete('plan-uuid-1'))
        .rejects.toThrow('Cannot delete plan with active subscriptions');
    });
  });

  describe('setActive', () => {
    it('should activate a plan', async () => {
      const mockPlan = createMockSubscriptionPlan({ isActive: false });
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockPlanRepository.save.mockImplementation((plan: any) => Promise.resolve({
        ...plan,
        isActive: true,
      }));

      const result = await service.setActive('plan-uuid-1', true);

      expect(result.isActive).toBe(true);
    });

    it('should deactivate a plan', async () => {
      const mockPlan = createMockSubscriptionPlan({ isActive: true });
      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockPlanRepository.save.mockImplementation((plan: any) => Promise.resolve({
        ...plan,
        isActive: false,
      }));

      const result = await service.setActive('plan-uuid-1', false);

      expect(result.isActive).toBe(false);
    });
  });

  describe('comparePlans', () => {
    it('should compare two plans and return differences', async () => {
      const plan1 = createMockSubscriptionPlan({
        id: 'plan-1',
        code: 'STARTER',
        baseMonthlyPrice: 499,
        maxUsers: 5,
        includedModules: ['core', 'sales'],
      });
      const plan2 = createMockSubscriptionPlan({
        id: 'plan-2',
        code: 'PRO',
        baseMonthlyPrice: 999,
        maxUsers: 20,
        includedModules: ['core', 'sales', 'inventory', 'reports'],
      });

      mockPlanRepository.findOne
        .mockResolvedValueOnce(plan1)
        .mockResolvedValueOnce(plan2);

      const result = await service.comparePlans('plan-1', 'plan-2');

      expect(result.plan1.code).toBe('STARTER');
      expect(result.plan2.code).toBe('PRO');
      expect(result.differences.baseMonthlyPrice).toEqual({
        plan1: 499,
        plan2: 999,
      });
      expect(result.differences.maxUsers).toEqual({
        plan1: 5,
        plan2: 20,
      });
      expect(result.differences.includedModules).toBeDefined();
    });

    it('should throw error if plan1 not found', async () => {
      mockPlanRepository.findOne
        .mockResolvedValueOnce(null)
        .mockResolvedValueOnce(createMockSubscriptionPlan());

      await expect(service.comparePlans('invalid-1', 'plan-2'))
        .rejects.toThrow('One or both plans not found');
    });

    it('should throw error if plan2 not found', async () => {
      mockPlanRepository.findOne
        .mockResolvedValueOnce(createMockSubscriptionPlan())
        .mockResolvedValueOnce(null);

      await expect(service.comparePlans('plan-1', 'invalid-2'))
        .rejects.toThrow('One or both plans not found');
    });

    it('should return empty differences for identical plans', async () => {
      const plan = createMockSubscriptionPlan();
      mockPlanRepository.findOne
        .mockResolvedValueOnce(plan)
        .mockResolvedValueOnce({ ...plan, id: 'plan-2' });

      const result = await service.comparePlans('plan-1', 'plan-2');

      expect(Object.keys(result.differences)).toHaveLength(0);
    });
  });
});
