import { SubscriptionEntity } from '../../domain/entities/subscription.entity';
import { type ISubscriptionRepository } from '../../domain/repositories/subscription.repository';
import { MeterUsageCommand } from '../commands/meter-usage/meter-usage.command';
import { MeterUsageHandler } from '../commands/meter-usage/meter-usage.handler';

function createActiveSubscription(): SubscriptionEntity {
  const sub = SubscriptionEntity.createNew(
    'sub-1', 'user-1', 'plan-1', 'AGENT_PRO',
    new Date('2026-01-01'), new Date('2026-02-01'),
  );
  sub.clearDomainEvents();
  return sub;
}

describe('MeterUsageHandler', () => {
  let handler: MeterUsageHandler;
  let mockRepo: { [K in keyof ISubscriptionRepository]: ReturnType<typeof vi.fn> };
  let mockPrisma: any;
  let mockCache: { getOrSet: ReturnType<typeof vi.fn>; invalidate: ReturnType<typeof vi.fn>; invalidateByPrefix: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockRepo = {
      findById: vi.fn(),
      findByUserId: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
    };

    mockPrisma = {
      usageRecord: {
        upsert: vi.fn(),
      },
    };

    mockCache = {
      getOrSet: vi.fn(),
      invalidate: vi.fn().mockResolvedValue(undefined),
      invalidateByPrefix: vi.fn().mockResolvedValue(undefined),
    };

    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };

    handler = new MeterUsageHandler(
      mockRepo as any,
      mockPrisma,
      mockCache as any,
      mockLogger as any,
    );
  });

  it('creates new usage record via atomic upsert when none exists', async () => {
    const subscription = createActiveSubscription();
    mockRepo.findByUserId.mockResolvedValue(subscription);
    mockPrisma.usageRecord.upsert.mockResolvedValue({
      id: 'usage-1',
      metric: 'listings_created',
      count: 3,
      periodStart: subscription.currentPeriodStart,
      periodEnd: subscription.currentPeriodEnd,
    });

    const command = new MeterUsageCommand('user-1', 'listings_created', 3);
    const result = await handler.execute(command);

    expect(result.usageRecordId).toBe('usage-1');
    expect(result.metric).toBe('listings_created');
    expect(result.count).toBe(3);
    expect(mockPrisma.usageRecord.upsert).toHaveBeenCalledWith({
      where: {
        subscriptionId_metric_periodStart_periodEnd: {
          subscriptionId: subscription.id,
          metric: 'listings_created',
          periodStart: subscription.currentPeriodStart,
          periodEnd: subscription.currentPeriodEnd,
        },
      },
      update: { count: { increment: 3 } },
      create: {
        subscriptionId: subscription.id,
        metric: 'listings_created',
        count: 3,
        periodStart: subscription.currentPeriodStart,
        periodEnd: subscription.currentPeriodEnd,
      },
    });
  });

  it('increments existing usage record via atomic upsert', async () => {
    const subscription = createActiveSubscription();
    mockRepo.findByUserId.mockResolvedValue(subscription);
    mockPrisma.usageRecord.upsert.mockResolvedValue({
      id: 'usage-1',
      metric: 'listings_created',
      count: 8,
      periodStart: subscription.currentPeriodStart,
      periodEnd: subscription.currentPeriodEnd,
    });

    const command = new MeterUsageCommand('user-1', 'listings_created', 3);
    const result = await handler.execute(command);

    expect(result.count).toBe(8);
    expect(mockPrisma.usageRecord.upsert).toHaveBeenCalledTimes(1);
  });

  it('invalidates quota cache after metering usage', async () => {
    const subscription = createActiveSubscription();
    mockRepo.findByUserId.mockResolvedValue(subscription);
    mockPrisma.usageRecord.upsert.mockResolvedValue({
      id: 'usage-1',
      metric: 'listings_created',
      count: 1,
      periodStart: subscription.currentPeriodStart,
      periodEnd: subscription.currentPeriodEnd,
    });

    const command = new MeterUsageCommand('user-1', 'listings_created', 1);
    await handler.execute(command);

    expect(mockCache.invalidate).toHaveBeenCalledWith(
      expect.stringContaining('user-1'),
    );
    expect(mockCache.invalidate).toHaveBeenCalledWith(
      expect.stringContaining('listings_created'),
    );
  });

  it('throws ValidationException for zero count', async () => {
    const command = new MeterUsageCommand('user-1', 'listings_created', 0);

    await expect(handler.execute(command)).rejects.toThrow(/lớn hơn 0/);
  });

  it('throws ValidationException for negative count', async () => {
    const command = new MeterUsageCommand('user-1', 'listings_created', -1);

    await expect(handler.execute(command)).rejects.toThrow(/lớn hơn 0/);
  });

  it('throws NotFoundException when no subscription found', async () => {
    mockRepo.findByUserId.mockResolvedValue(null);

    const command = new MeterUsageCommand('user-99', 'listings_created', 1);

    await expect(handler.execute(command)).rejects.toThrow('Subscription');
  });

  it('throws ValidationException when subscription is not active', async () => {
    const subscription = createActiveSubscription();
    subscription.cancel();
    subscription.clearDomainEvents();
    mockRepo.findByUserId.mockResolvedValue(subscription);

    const command = new MeterUsageCommand('user-1', 'listings_created', 1);

    await expect(handler.execute(command)).rejects.toThrow(/hoạt động/);
  });
});
