import { type ExecutionContext, ForbiddenException } from '@nestjs/common';
import { type Reflector } from '@nestjs/core';
import { type QueryBus } from '@nestjs/cqrs';
import { type EventEmitter2 } from '@nestjs/event-emitter';
import { type QuotaCheckResult } from '../../application/queries/check-quota/check-quota.handler';
import { QuotaGuard } from '../guards/quota.guard';

function createMockContext(user?: { sub: string }): ExecutionContext {
  return {
    getHandler: vi.fn(),
    getClass: vi.fn(),
    switchToHttp: () => ({
      getRequest: () => ({ user }),
    }),
  } as unknown as ExecutionContext;
}

describe('QuotaGuard', () => {
  let guard: QuotaGuard;
  let mockReflector: { getAllAndOverride: ReturnType<typeof vi.fn> };
  let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
  let mockEventEmitter: { emit: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockReflector = { getAllAndOverride: vi.fn() };
    mockQueryBus = { execute: vi.fn() };
    mockEventEmitter = { emit: vi.fn() };

    guard = new QuotaGuard(
      mockReflector as unknown as Reflector,
      mockQueryBus as unknown as QueryBus,
      mockEventEmitter as unknown as EventEmitter2,
    );
  });

  it('allows when no metric is set', async () => {
    mockReflector.getAllAndOverride.mockReturnValue(undefined);
    const context = createMockContext({ sub: 'user-1' });

    const result = await guard.canActivate(context);
    expect(result).toBe(true);
    expect(mockQueryBus.execute).not.toHaveBeenCalled();
  });

  it('allows when user is not authenticated', async () => {
    mockReflector.getAllAndOverride.mockReturnValue('listings_created');
    const context = createMockContext(undefined);

    const result = await guard.canActivate(context);
    expect(result).toBe(true);
  });

  it('allows when quota is not exceeded', async () => {
    mockReflector.getAllAndOverride.mockReturnValue('listings_created');
    const quotaResult: QuotaCheckResult = {
      metric: 'listings_created',
      limit: 50,
      used: 10,
      remaining: 40,
      allowed: true,
    };
    mockQueryBus.execute.mockResolvedValue(quotaResult);
    const context = createMockContext({ sub: 'user-1' });

    const result = await guard.canActivate(context);
    expect(result).toBe(true);
    expect(mockEventEmitter.emit).not.toHaveBeenCalled();
  });

  it('throws ForbiddenException when quota exceeded', async () => {
    mockReflector.getAllAndOverride.mockReturnValue('listings_created');
    const quotaResult: QuotaCheckResult = {
      metric: 'listings_created',
      limit: 3,
      used: 3,
      remaining: 0,
      allowed: false,
    };
    mockQueryBus.execute.mockResolvedValue(quotaResult);
    const context = createMockContext({ sub: 'user-1' });

    await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
  });

  it('emits QuotaExceededEvent when quota exceeded', async () => {
    mockReflector.getAllAndOverride.mockReturnValue('listings_created');
    const quotaResult: QuotaCheckResult = {
      metric: 'listings_created',
      limit: 3,
      used: 3,
      remaining: 0,
      allowed: false,
    };
    mockQueryBus.execute.mockResolvedValue(quotaResult);
    const context = createMockContext({ sub: 'user-1' });

    try {
      await guard.canActivate(context);
    } catch {
      // expected
    }

    expect(mockEventEmitter.emit).toHaveBeenCalledWith(
      'quota.exceeded',
      expect.objectContaining({
        aggregateId: 'user-1',
        metric: 'listings_created',
        limit: 3,
        used: 3,
      }),
    );
  });

  it('includes QUOTA_EXCEEDED code in error response', async () => {
    mockReflector.getAllAndOverride.mockReturnValue('analytics_queries');
    const quotaResult: QuotaCheckResult = {
      metric: 'analytics_queries',
      limit: 0,
      used: 0,
      remaining: 0,
      allowed: false,
    };
    mockQueryBus.execute.mockResolvedValue(quotaResult);
    const context = createMockContext({ sub: 'user-1' });

    try {
      await guard.canActivate(context);
      expect.unreachable('Should have thrown');
    } catch (error) {
      expect(error).toBeInstanceOf(ForbiddenException);
      const response = (error as ForbiddenException).getResponse();
      expect(response).toMatchObject({
        code: 'QUOTA_EXCEEDED',
        quota: { metric: 'analytics_queries', limit: 0, used: 0 },
      });
    }
  });
});
