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

// Mock factories
function createMockUsageTracking(overrides: Record<string, any> = {}) {
  return {
    id: 'usage-uuid-1',
    tenantId: 'tenant-uuid-1',
    periodStart: new Date('2026-01-01'),
    periodEnd: new Date('2026-01-31'),
    activeUsers: 5,
    peakConcurrentUsers: 3,
    usersByProfile: { ADM: 1, VNT: 2, ALM: 2 },
    usersByPlatform: { web: 5, mobile: 2 },
    activeBranches: 2,
    storageUsedGb: 5.5,
    documentsCount: 1500,
    apiCalls: 5000,
    apiErrors: 50,
    salesCount: 200,
    salesAmount: 150000,
    invoicesGenerated: 150,
    mobileSessions: 100,
    offlineSyncs: 25,
    paymentTransactions: 180,
    totalBillableAmount: 499,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockSubscription(overrides: Record<string, any> = {}) {
  return {
    id: 'sub-uuid-1',
    tenantId: 'tenant-uuid-1',
    planId: 'plan-uuid-1',
    currentPrice: 499,
    contractedUsers: 10,
    contractedBranches: 3,
    plan: {
      id: 'plan-uuid-1',
      code: 'STARTER',
      maxUsers: 10,
      maxBranches: 3,
      storageGb: 20,
      apiCallsMonthly: 10000,
    },
    ...overrides,
  };
}

// Mock repositories
const mockUsageRepository = createMockRepository();
const mockSubscriptionRepository = createMockRepository();
const mockPlanRepository = createMockRepository();

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

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

// Import after mocking
import { UsageTrackingService } from '../services/usage-tracking.service.js';

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

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

  describe('recordUsage', () => {
    it('should create new usage record', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        activeUsers: 5,
        apiCalls: 1000,
      };

      const mockUsage = createMockUsageTracking(dto);
      mockUsageRepository.findOne.mockResolvedValueOnce(null); // No existing record
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.create.mockReturnValue(mockUsage);
      mockUsageRepository.save.mockResolvedValue(mockUsage);

      const result = await service.recordUsage(dto);

      expect(mockUsageRepository.findOne).toHaveBeenCalled();
      expect(mockUsageRepository.create).toHaveBeenCalled();
      expect(result.tenantId).toBe('tenant-uuid-1');
    });

    it('should update existing record if one exists for period', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        activeUsers: 10,
      };

      const existingUsage = createMockUsageTracking();
      mockUsageRepository.findOne
        .mockResolvedValueOnce(existingUsage) // First call - check existing
        .mockResolvedValueOnce(existingUsage); // Second call - in update
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      const result = await service.recordUsage(dto);

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

  describe('update', () => {
    it('should update usage record', async () => {
      const mockUsage = createMockUsageTracking();
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      const result = await service.update('usage-uuid-1', { apiCalls: 8000 });

      expect(result.apiCalls).toBe(8000);
    });

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

      await expect(service.update('invalid-id', { apiCalls: 100 }))
        .rejects.toThrow('Usage record not found');
    });

    it('should recalculate billable amount on update', async () => {
      const mockUsage = createMockUsageTracking();
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      await service.update('usage-uuid-1', { activeUsers: 15 }); // Exceeds limit

      expect(mockUsageRepository.save).toHaveBeenCalled();
    });
  });

  describe('incrementMetric', () => {
    it('should increment metric on existing record', async () => {
      const mockUsage = createMockUsageTracking({ apiCalls: 5000 });
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      await service.incrementMetric('tenant-uuid-1', 'apiCalls', 100);

      expect(mockUsageRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ apiCalls: 5100 })
      );
    });

    it('should create record if none exists for period', async () => {
      mockUsageRepository.findOne.mockResolvedValue(null);
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.create.mockImplementation((data: any) => ({
        ...createMockUsageTracking(),
        ...data,
        apiCalls: 0,
      }));
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      await service.incrementMetric('tenant-uuid-1', 'apiCalls', 50);

      expect(mockUsageRepository.create).toHaveBeenCalled();
    });
  });

  describe('getCurrentUsage', () => {
    it('should return current period usage', async () => {
      const mockUsage = createMockUsageTracking();
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

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

      expect(result?.tenantId).toBe('tenant-uuid-1');
    });

    it('should return null if no usage for current period', async () => {
      mockUsageRepository.findOne.mockResolvedValue(null);

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

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

  describe('getUsageHistory', () => {
    it('should return usage records within date range', async () => {
      const mockUsages = [
        createMockUsageTracking({ id: 'usage-1' }),
        createMockUsageTracking({ id: 'usage-2' }),
      ];
      mockUsageRepository.find.mockResolvedValue(mockUsages);

      const result = await service.getUsageHistory(
        'tenant-uuid-1',
        new Date('2026-01-01'),
        new Date('2026-03-31')
      );

      expect(mockUsageRepository.find).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ tenantId: 'tenant-uuid-1' }),
          order: { periodStart: 'DESC' },
        })
      );
      expect(result).toHaveLength(2);
    });
  });

  describe('getUsageSummary', () => {
    it('should return usage summary with limits', async () => {
      const mockSub = createMockSubscription();
      const mockUsage = createMockUsageTracking();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

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

      expect(result.tenantId).toBe('tenant-uuid-1');
      expect(result.currentUsers).toBe(5);
      expect(result.limits.maxUsers).toBe(10);
      expect(result.percentages.usersUsed).toBe(50);
    });

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

      await expect(service.getUsageSummary('tenant-uuid-1'))
        .rejects.toThrow('Subscription not found');
    });

    it('should handle missing current usage gracefully', async () => {
      const mockSub = createMockSubscription();
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(null);

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

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

  describe('checkLimits', () => {
    it('should return no violations when within limits', async () => {
      const mockSub = createMockSubscription();
      const mockUsage = createMockUsageTracking({
        activeUsers: 5,
        activeBranches: 2,
        storageUsedGb: 10,
        apiCalls: 5000,
      });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

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

      expect(result.exceeds).toBe(false);
      expect(result.violations).toHaveLength(0);
    });

    it('should return violations when limits exceeded', async () => {
      const mockSub = createMockSubscription();
      const mockUsage = createMockUsageTracking({
        activeUsers: 15, // Exceeds 10
        activeBranches: 5, // Exceeds 3
        storageUsedGb: 10,
        apiCalls: 5000,
      });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

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

      expect(result.exceeds).toBe(true);
      expect(result.violations.length).toBeGreaterThan(0);
      expect(result.violations.some((v: string) => v.includes('Users'))).toBe(true);
      expect(result.violations.some((v: string) => v.includes('Branches'))).toBe(true);
    });

    it('should return warnings at 80% threshold', async () => {
      const mockSub = createMockSubscription();
      const mockUsage = createMockUsageTracking({
        activeUsers: 8, // 80% of 10
        activeBranches: 2,
        storageUsedGb: 16, // 80% of 20
        apiCalls: 8000, // 80% of 10000
      });
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSub);
      mockUsageRepository.findOne.mockResolvedValue(mockUsage);

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

      expect(result.exceeds).toBe(false);
      expect(result.warnings.length).toBeGreaterThan(0);
      expect(result.warnings.some((w: string) => w.includes('Users'))).toBe(true);
      expect(result.warnings.some((w: string) => w.includes('Storage'))).toBe(true);
    });
  });

  describe('getUsageReport', () => {
    it('should generate usage report with totals and averages', async () => {
      const mockUsages = [
        createMockUsageTracking({
          activeUsers: 5,
          activeBranches: 2,
          storageUsedGb: 5,
          apiCalls: 5000,
          salesCount: 100,
          salesAmount: 50000,
        }),
        createMockUsageTracking({
          activeUsers: 7,
          activeBranches: 3,
          storageUsedGb: 6,
          apiCalls: 6000,
          salesCount: 150,
          salesAmount: 75000,
        }),
      ];
      mockUsageRepository.find.mockResolvedValue(mockUsages);

      const result = await service.getUsageReport(
        'tenant-uuid-1',
        new Date('2026-01-01'),
        new Date('2026-02-28')
      );

      expect(result.tenantId).toBe('tenant-uuid-1');
      expect(result.data).toHaveLength(2);
      expect(result.totals.apiCalls).toBe(11000);
      expect(result.totals.salesCount).toBe(250);
      expect(result.totals.salesAmount).toBe(125000);
      expect(result.averages.activeUsers).toBe(6);
      expect(result.averages.activeBranches).toBe(3);
    });

    it('should handle empty usage data', async () => {
      mockUsageRepository.find.mockResolvedValue([]);

      const result = await service.getUsageReport(
        'tenant-uuid-1',
        new Date('2026-01-01'),
        new Date('2026-02-28')
      );

      expect(result.data).toHaveLength(0);
      expect(result.totals.apiCalls).toBe(0);
      expect(result.averages.activeUsers).toBe(0);
    });
  });

  describe('calculateBillableAmount (via recordUsage)', () => {
    it('should calculate base price for usage within limits', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        activeUsers: 5,
        activeBranches: 2,
        storageUsedGb: 10,
        apiCalls: 5000,
      };

      mockUsageRepository.findOne.mockResolvedValue(null);
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.create.mockImplementation((data: any) => data);
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      const result = await service.recordUsage(dto);

      expect(result.totalBillableAmount).toBe(499); // Base price, no overages
    });

    it('should add overage charges when limits exceeded', async () => {
      const dto = {
        tenantId: 'tenant-uuid-1',
        periodStart: new Date('2026-01-01'),
        periodEnd: new Date('2026-01-31'),
        activeUsers: 15, // 5 extra users at $10 each = $50
        activeBranches: 5, // 2 extra branches at $20 each = $40
        storageUsedGb: 25, // 5 extra GB at $0.50 each = $2.50
        apiCalls: 15000, // 5000 extra at $0.001 each = $5
      };

      mockUsageRepository.findOne.mockResolvedValue(null);
      mockSubscriptionRepository.findOne.mockResolvedValue(createMockSubscription());
      mockUsageRepository.create.mockImplementation((data: any) => data);
      mockUsageRepository.save.mockImplementation((usage: any) => Promise.resolve(usage));

      const result = await service.recordUsage(dto);

      // Base: 499 + Extra users: 50 + Extra branches: 40 + Extra storage: 2.5 + Extra API: 5 = 596.5
      expect(result.totalBillableAmount).toBe(596.5);
    });
  });
});
