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

// Mock factories for billing entities
function createMockPlanLimit(overrides: Record<string, any> = {}) {
  return {
    id: 'limit-uuid-1',
    planId: 'plan-uuid-1',
    limitKey: 'users',
    limitName: 'Active Users',
    limitValue: 10,
    limitType: 'monthly',
    allowOverage: false,
    overageUnitPrice: 0,
    overageCurrency: 'MXN',
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockSubscriptionPlan(overrides: Record<string, any> = {}) {
  return {
    id: 'plan-uuid-1',
    code: 'PRO',
    name: 'Professional Plan',
    description: 'Professional subscription plan',
    monthlyPrice: 499,
    annualPrice: 4990,
    currency: 'MXN',
    isActive: true,
    displayOrder: 2,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockSubscription(overrides: Record<string, any> = {}) {
  return {
    id: 'subscription-uuid-1',
    tenantId: 'tenant-uuid-1',
    planId: 'plan-uuid-1',
    status: 'active',
    currentPrice: 499,
    billingCycle: 'monthly',
    currentPeriodStart: new Date(),
    currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockUsageTracking(overrides: Record<string, any> = {}) {
  return {
    id: 'usage-uuid-1',
    tenantId: 'tenant-uuid-1',
    periodStart: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
    periodEnd: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0),
    activeUsers: 5,
    storageUsedGb: 2.5,
    apiCalls: 1000,
    activeBranches: 2,
    documentsCount: 150,
    invoicesGenerated: 50,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

// Mock repositories with extended methods
const mockLimitRepository = {
  ...createMockRepository(),
  remove: jest.fn(),
};
const mockPlanRepository = createMockRepository();
const mockSubscriptionRepository = createMockRepository();
const mockUsageRepository = createMockRepository();

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

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

// Import after mocking
import { PlanLimitsService } from '../services/plan-limits.service.js';

describe('PlanLimitsService', () => {
  let service: PlanLimitsService;
  const tenantId = 'tenant-uuid-1';

  beforeEach(() => {
    jest.clearAllMocks();
    service = new PlanLimitsService(mockDataSource as any);
  });

  describe('create', () => {
    it('should create a new plan limit successfully', async () => {
      const dto = {
        planId: 'plan-uuid-1',
        limitKey: 'storage_gb',
        limitName: 'Storage (GB)',
        limitValue: 50,
        limitType: 'fixed' as const,
      };

      const mockPlan = createMockSubscriptionPlan();
      const mockLimit = createMockPlanLimit({ ...dto, id: 'new-limit-uuid' });

      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockLimitRepository.findOne.mockResolvedValue(null);
      mockLimitRepository.create.mockReturnValue(mockLimit);
      mockLimitRepository.save.mockResolvedValue(mockLimit);

      const result = await service.create(dto);

      expect(result.limitKey).toBe('storage_gb');
      expect(result.limitValue).toBe(50);
      expect(mockLimitRepository.create).toHaveBeenCalled();
      expect(mockLimitRepository.save).toHaveBeenCalled();
    });

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

      const dto = {
        planId: 'nonexistent-plan',
        limitKey: 'users',
        limitName: 'Users',
        limitValue: 10,
      };

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

    it('should throw error if limit key already exists for plan', async () => {
      const mockPlan = createMockSubscriptionPlan();
      const existingLimit = createMockPlanLimit({ limitKey: 'users' });

      mockPlanRepository.findOne.mockResolvedValue(mockPlan);
      mockLimitRepository.findOne.mockResolvedValue(existingLimit);

      const dto = {
        planId: 'plan-uuid-1',
        limitKey: 'users',
        limitName: 'Users',
        limitValue: 10,
      };

      await expect(service.create(dto)).rejects.toThrow('Limit users already exists for this plan');
    });
  });

  describe('findByPlan', () => {
    it('should return all limits for a plan', async () => {
      const mockLimits = [
        createMockPlanLimit({ limitKey: 'users', limitValue: 10 }),
        createMockPlanLimit({ limitKey: 'storage_gb', limitValue: 50 }),
        createMockPlanLimit({ limitKey: 'api_calls', limitValue: 10000 }),
      ];

      mockLimitRepository.find.mockResolvedValue(mockLimits);

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

      expect(result).toHaveLength(3);
      expect(mockLimitRepository.find).toHaveBeenCalledWith({
        where: { planId: 'plan-uuid-1' },
        order: { limitKey: 'ASC' },
      });
    });
  });

  describe('findByKey', () => {
    it('should find a specific limit by key', async () => {
      const mockLimit = createMockPlanLimit({ limitKey: 'users' });
      mockLimitRepository.findOne.mockResolvedValue(mockLimit);

      const result = await service.findByKey('plan-uuid-1', 'users');

      expect(result).toBeDefined();
      expect(result?.limitKey).toBe('users');
    });

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

      const result = await service.findByKey('plan-uuid-1', 'nonexistent');

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

  describe('update', () => {
    it('should update a plan limit', async () => {
      const mockLimit = createMockPlanLimit({ limitValue: 10 });
      mockLimitRepository.findOne.mockResolvedValue(mockLimit);
      mockLimitRepository.save.mockResolvedValue({ ...mockLimit, limitValue: 20 });

      const result = await service.update('limit-uuid-1', { limitValue: 20 });

      expect(result.limitValue).toBe(20);
    });

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

      await expect(service.update('nonexistent', { limitValue: 20 })).rejects.toThrow('Limit not found');
    });
  });

  describe('delete', () => {
    it('should delete a plan limit', async () => {
      const mockLimit = createMockPlanLimit();
      mockLimitRepository.findOne.mockResolvedValue(mockLimit);
      mockLimitRepository.remove.mockResolvedValue(mockLimit);

      await expect(service.delete('limit-uuid-1')).resolves.not.toThrow();
      expect(mockLimitRepository.remove).toHaveBeenCalledWith(mockLimit);
    });

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

      await expect(service.delete('nonexistent')).rejects.toThrow('Limit not found');
    });
  });

  describe('getTenantLimits', () => {
    it('should return limits for tenant with active subscription', async () => {
      const mockSubscription = createMockSubscription({ planId: 'pro-plan' });
      const mockLimits = [
        createMockPlanLimit({ limitKey: 'users', limitValue: 25 }),
        createMockPlanLimit({ limitKey: 'storage_gb', limitValue: 100 }),
      ];

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.getTenantLimits(tenantId);

      expect(result).toHaveLength(2);
      expect(mockSubscriptionRepository.findOne).toHaveBeenCalledWith({
        where: { tenantId, status: 'active' },
      });
    });

    it('should return free plan limits if no active subscription', async () => {
      const mockFreePlan = createMockSubscriptionPlan({ id: 'free-plan', code: 'FREE' });
      const mockLimits = [createMockPlanLimit({ limitKey: 'users', limitValue: 3 })];

      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(mockFreePlan);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.getTenantLimits(tenantId);

      expect(result).toHaveLength(1);
      expect(result[0].limitValue).toBe(3);
    });

    it('should return empty array if no subscription and no free plan', async () => {
      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(null);

      const result = await service.getTenantLimits(tenantId);

      expect(result).toEqual([]);
    });
  });

  describe('getTenantLimit', () => {
    it('should return specific limit value for tenant', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [createMockPlanLimit({ limitKey: 'users', limitValue: 10 })];

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.getTenantLimit(tenantId, 'users');

      expect(result).toBe(10);
    });

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

      const result = await service.getTenantLimit(tenantId, 'nonexistent');

      expect(result).toBe(0);
    });
  });

  describe('checkUsage', () => {
    it('should allow usage within limits', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [createMockPlanLimit({ limitKey: 'users', limitValue: 10, allowOverage: false })];

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.checkUsage(tenantId, 'users', 5, 1);

      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(4);
      expect(result.message).toBe('Dentro del límite');
    });

    it('should reject usage exceeding limits when overage not allowed', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [createMockPlanLimit({ limitKey: 'users', limitValue: 10, allowOverage: false })];

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.checkUsage(tenantId, 'users', 10, 1);

      expect(result.allowed).toBe(false);
      expect(result.remaining).toBe(0);
      expect(result.message).toContain('Límite alcanzado');
    });

    it('should allow overage when configured', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [
        createMockPlanLimit({
          limitKey: 'users',
          limitValue: 10,
          allowOverage: true,
          overageUnitPrice: 50,
        }),
      ];

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);

      const result = await service.checkUsage(tenantId, 'users', 10, 2);

      expect(result.allowed).toBe(true);
      expect(result.overageAllowed).toBe(true);
      expect(result.overageUnits).toBe(2);
      expect(result.overageCost).toBe(100); // 2 * 50
    });

    it('should allow unlimited when no limit defined', async () => {
      mockSubscriptionRepository.findOne.mockResolvedValue(null);
      mockPlanRepository.findOne.mockResolvedValue(null);

      const result = await service.checkUsage(tenantId, 'nonexistent', 1000, 100);

      expect(result.allowed).toBe(true);
      expect(result.limit).toBe(-1);
      expect(result.remaining).toBe(-1);
    });
  });

  describe('getCurrentUsage', () => {
    it('should return current usage for a limit key', async () => {
      const mockUsage = createMockUsageTracking({ activeUsers: 7 });
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

      const result = await service.getCurrentUsage(tenantId, 'users');

      expect(result).toBe(7);
    });

    it('should return 0 if no usage record found', async () => {
      mockUsageRepository.findOne.mockResolvedValue(null);

      const result = await service.getCurrentUsage(tenantId, 'users');

      expect(result).toBe(0);
    });

    it('should return correct value for different limit keys', async () => {
      const mockUsage = createMockUsageTracking({
        activeUsers: 5,
        storageUsedGb: 10,
        apiCalls: 5000,
      });
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

      expect(await service.getCurrentUsage(tenantId, 'users')).toBe(5);
      expect(await service.getCurrentUsage(tenantId, 'storage_gb')).toBe(10);
      expect(await service.getCurrentUsage(tenantId, 'api_calls')).toBe(5000);
    });
  });

  describe('validateAllLimits', () => {
    it('should return valid when all limits OK', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [
        createMockPlanLimit({ limitKey: 'users', limitValue: 10 }),
        createMockPlanLimit({ limitKey: 'storage_gb', limitValue: 50 }),
      ];
      const mockUsage = createMockUsageTracking({ activeUsers: 5, storageUsedGb: 20 });

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

      const result = await service.validateAllLimits(tenantId);

      expect(result.valid).toBe(true);
      expect(result.violations).toHaveLength(0);
    });

    it('should return violations when limits exceeded', async () => {
      const mockSubscription = createMockSubscription();
      const mockLimits = [
        createMockPlanLimit({ limitKey: 'users', limitValue: 5, allowOverage: false }),
      ];
      const mockUsage = createMockUsageTracking({ activeUsers: 10 });

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockLimitRepository.find.mockResolvedValue(mockLimits);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

      const result = await service.validateAllLimits(tenantId);

      expect(result.valid).toBe(false);
      expect(result.violations).toHaveLength(1);
      expect(result.violations[0].limitKey).toBe('users');
    });
  });

  describe('copyLimitsFromPlan', () => {
    it('should copy all limits from source to target plan', async () => {
      const sourceLimits = [
        createMockPlanLimit({ id: 'limit-1', limitKey: 'users', limitValue: 10 }),
        createMockPlanLimit({ id: 'limit-2', limitKey: 'storage_gb', limitValue: 50 }),
      ];
      const targetPlan = createMockSubscriptionPlan({ id: 'target-plan' });

      mockLimitRepository.find.mockResolvedValue(sourceLimits);
      mockPlanRepository.findOne.mockResolvedValue(targetPlan);
      mockLimitRepository.findOne.mockResolvedValue(null); // No existing limits
      mockLimitRepository.create.mockImplementation((data) => data as any);
      mockLimitRepository.save.mockImplementation((data) => Promise.resolve({ ...data, id: 'new-limit' }));

      const result = await service.copyLimitsFromPlan('source-plan', 'target-plan');

      expect(result).toHaveLength(2);
      expect(mockLimitRepository.create).toHaveBeenCalledTimes(2);
    });
  });
});
