import { BadRequestException } from '@nestjs/common';
import { UserAiQuotaService } from './user-ai-quota.service';

describe('UserAiQuotaService', () => {
  let service: UserAiQuotaService;
  let mockSettingsFindUnique: jest.Mock;
  let mockUsageFindUnique: jest.Mock;
  let mockUpsert: jest.Mock;

  const today = () => {
    const d = new Date();
    return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
  };

  beforeEach(() => {
    mockSettingsFindUnique = jest.fn();
    mockUsageFindUnique = jest.fn();
    mockUpsert = jest.fn();
    const mockPrisma = {
      userAiSettings: { findUnique: mockSettingsFindUnique },
      userAiUsageDaily: {
        findUnique: mockUsageFindUnique,
        upsert: mockUpsert,
      },
    } as any;
    service = new UserAiQuotaService(mockPrisma);
  });

  // ═══════════════════════════════════════════════════════════════════
  // checkQuota
  // ═══════════════════════════════════════════════════════════════════

  describe('checkQuota', () => {
    it('passes when under both limits', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyAiJobs: 10, maxDailyTokenBudget: 50000 });
      mockUsageFindUnique.mockResolvedValue({ jobCount: 3, totalTokens: 10000 });

      await expect(service.checkQuota('u1', 'platform_key')).resolves.toBeUndefined();
    });

    it('passes with defaults when settings is null', async () => {
      mockSettingsFindUnique.mockResolvedValue(null);
      mockUsageFindUnique.mockResolvedValue({ jobCount: 5, totalTokens: 50000 });

      await expect(service.checkQuota('u1', 'platform_key')).resolves.toBeUndefined();
    });

    it('passes when usage is null (first call of day)', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyAiJobs: 10, maxDailyTokenBudget: 50000 });
      mockUsageFindUnique.mockResolvedValue(null); // no usage → defaults to 0

      await expect(service.checkQuota('u1', 'platform_key')).resolves.toBeUndefined();
    });

    it('throws DAILY_JOB_LIMIT_EXCEEDED when jobCount equals maxJobs', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyAiJobs: 10 });
      mockUsageFindUnique.mockResolvedValue({ jobCount: 10, totalTokens: 0 });

      await expect(service.checkQuota('u1', 'platform_key')).rejects.toMatchObject({
        response: expect.objectContaining({ errorCode: 'DAILY_JOB_LIMIT_EXCEEDED' }),
      });
    });

    it('throws DAILY_JOB_LIMIT_EXCEEDED when jobCount exceeds maxJobs', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyAiJobs: 5 });
      mockUsageFindUnique.mockResolvedValue({ jobCount: 8, totalTokens: 0 });

      await expect(service.checkQuota('u1', 'platform_key')).rejects.toThrow(BadRequestException);
    });

    it('throws DAILY_TOKEN_BUDGET_EXCEEDED when totalTokens >= maxTokens', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyTokenBudget: 10000 });
      mockUsageFindUnique.mockResolvedValue({ jobCount: 0, totalTokens: 10000 });

      await expect(service.checkQuota('u1', 'user_deepseek_key')).rejects.toMatchObject({
        response: expect.objectContaining({ errorCode: 'DAILY_TOKEN_BUDGET_EXCEEDED' }),
      });
    });

    it('queries usage with correct apiKeyMode', async () => {
      mockSettingsFindUnique.mockResolvedValue({ maxDailyAiJobs: 10 });
      mockUsageFindUnique.mockResolvedValue(null);

      await service.checkQuota('u1', 'platform_key');

      expect(mockUsageFindUnique).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            userId_localDate_apiKeyMode: expect.objectContaining({ apiKeyMode: 'platform_key' }),
          }),
        }),
      );
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // incrementJobCount
  // ═══════════════════════════════════════════════════════════════════

  describe('incrementJobCount', () => {
    it('upserts with jobCount increment for today', async () => {
      const localDate = today();
      mockUpsert.mockResolvedValue({});

      await service.incrementJobCount('u1', 'platform_key');

      expect(mockUpsert).toHaveBeenCalledWith({
        where: { userId_localDate_apiKeyMode: { userId: 'u1', localDate, apiKeyMode: 'platform_key' } },
        create: { userId: 'u1', localDate, apiKeyMode: 'platform_key', jobCount: 1 },
        update: { jobCount: { increment: 1 } },
      });
    });

    it('passes correct apiKeyMode for user key', async () => {
      mockUpsert.mockResolvedValue({});
      await service.incrementJobCount('u2', 'user_deepseek_key');
      expect(mockUpsert.mock.calls[0][0].where.userId_localDate_apiKeyMode.apiKeyMode).toBe('user_deepseek_key');
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // recordTokenUsage
  // ═══════════════════════════════════════════════════════════════════

  describe('recordTokenUsage', () => {
    it('upserts with all token fields', async () => {
      const localDate = today();
      mockUpsert.mockResolvedValue({});

      await service.recordTokenUsage('u1', 'platform_key', 100, 50, 150, 0.002);

      expect(mockUpsert).toHaveBeenCalledWith({
        where: { userId_localDate_apiKeyMode: { userId: 'u1', localDate, apiKeyMode: 'platform_key' } },
        create: {
          userId: 'u1', localDate, apiKeyMode: 'platform_key',
          inputTokens: 100, outputTokens: 50, totalTokens: 150,
          costEstimate: 0.002, jobCount: 0,
        },
        update: {
          inputTokens: { increment: 100 },
          outputTokens: { increment: 50 },
          totalTokens: { increment: 150 },
          costEstimate: { increment: 0.002 },
        },
      });
    });

    it('defaults costEstimate to 0 when undefined', async () => {
      mockUpsert.mockResolvedValue({});
      await service.recordTokenUsage('u1', 'platform_key', 100, 50, 150);
      const call = mockUpsert.mock.calls[0][0];
      expect(call.create.costEstimate).toBe(0);
      expect(call.update.costEstimate).toEqual({ increment: 0 });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // getUsage
  // ═══════════════════════════════════════════════════════════════════

  describe('getUsage', () => {
    it('returns both platform and user key usage', async () => {
      mockUsageFindUnique
        .mockResolvedValueOnce({ jobCount: 5, totalTokens: 30000 })
        .mockResolvedValueOnce({ jobCount: 2, totalTokens: 10000 });

      const result = await service.getUsage('u1');

      expect(result).toEqual({
        platformKey: { jobCount: 5, totalTokens: 30000 },
        userDeepseekKey: { jobCount: 2, totalTokens: 10000 },
      });
    });

    it('returns default empty objects when no records', async () => {
      mockUsageFindUnique.mockResolvedValue(null);

      const result = await service.getUsage('u1');

      expect(result).toEqual({
        platformKey: { jobCount: 0, totalTokens: 0 },
        userDeepseekKey: { jobCount: 0, totalTokens: 0 },
      });
    });

    it('returns default for one mode when missing', async () => {
      mockUsageFindUnique
        .mockResolvedValueOnce({ jobCount: 3, totalTokens: 5000 })
        .mockResolvedValueOnce(null);

      const result = await service.getUsage('u1');

      expect(result.platformKey).toEqual({ jobCount: 3, totalTokens: 5000 });
      expect(result.userDeepseekKey).toEqual({ jobCount: 0, totalTokens: 0 });
    });

    it('queries both modes in parallel', async () => {
      mockUsageFindUnique.mockResolvedValue({ jobCount: 1, totalTokens: 100 });

      await service.getUsage('u1');

      const localDate = today();
      expect(mockUsageFindUnique).toHaveBeenCalledWith({
        where: { userId_localDate_apiKeyMode: { userId: 'u1', localDate, apiKeyMode: 'platform_key' } },
      });
      expect(mockUsageFindUnique).toHaveBeenCalledWith({
        where: { userId_localDate_apiKeyMode: { userId: 'u1', localDate, apiKeyMode: 'user_deepseek_key' } },
      });
    });
  });
});
