import { type CallHandler, type ExecutionContext } from '@nestjs/common';
import { of, throwError } from 'rxjs';
import { type MetricsService } from '../../../infrastructure/metrics.service';
import { HttpMetricsInterceptor } from '../http-metrics.interceptor';

describe('HttpMetricsInterceptor', () => {
  let interceptor: HttpMetricsInterceptor;
  let mockMetricsService: { recordHttpRequest: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockMetricsService = { recordHttpRequest: vi.fn() };
    interceptor = new HttpMetricsInterceptor(mockMetricsService as unknown as MetricsService);
  });

  function createContext(
    requestOverrides: Record<string, unknown> = {},
    responseOverrides: Record<string, unknown> = {},
  ): ExecutionContext {
    const mockRequest = {
      method: 'GET',
      path: '/api/listings',
      route: { path: '/api/listings/:id' },
      ...requestOverrides,
    };
    const mockResponse = {
      statusCode: 200,
      ...responseOverrides,
    };
    return {
      switchToHttp: () => ({
        getRequest: () => mockRequest,
        getResponse: () => mockResponse,
      }),
    } as unknown as ExecutionContext;
  }

  it('records metrics on successful request', async () => {
    const context = createContext(
      { method: 'GET', path: '/api/listings', route: { path: '/api/listings/:id' } },
      { statusCode: 200 },
    );
    const next: CallHandler = { handle: () => of(undefined) };

    await new Promise<void>((resolve, reject) => {
      interceptor.intercept(context, next).subscribe({
        next: () => resolve(),
        error: reject,
      });
    });

    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledOnce();
    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledWith(
      'GET',
      '/api/listings/:id',
      200,
      expect.any(Number),
    );
  });

  it('records metrics on error request', async () => {
    const context = createContext(
      { method: 'POST', path: '/api/payments', route: { path: '/api/payments' } },
      { statusCode: 500 },
    );
    const next: CallHandler = { handle: () => throwError(() => new Error('test')) };

    await new Promise<void>((resolve) => {
      interceptor.intercept(context, next).subscribe({
        next: () => resolve(),
        error: () => resolve(),
      });
    });

    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledOnce();
    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledWith(
      'POST',
      '/api/payments',
      500,
      expect.any(Number),
    );
  });

  it('uses request.path when request.route is undefined', async () => {
    const context = createContext(
      { method: 'GET', path: '/api/search', route: undefined },
      { statusCode: 200 },
    );
    const next: CallHandler = { handle: () => of(undefined) };

    await new Promise<void>((resolve, reject) => {
      interceptor.intercept(context, next).subscribe({
        next: () => resolve(),
        error: reject,
      });
    });

    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledWith(
      'GET',
      '/api/search',
      200,
      expect.any(Number),
    );
  });

  it('uses request.route.path when available', async () => {
    const context = createContext(
      { method: 'GET', path: '/api/listings/abc123', route: { path: '/api/listings/:id' } },
      { statusCode: 200 },
    );
    const next: CallHandler = { handle: () => of(undefined) };

    await new Promise<void>((resolve, reject) => {
      interceptor.intercept(context, next).subscribe({
        next: () => resolve(),
        error: reject,
      });
    });

    expect(mockMetricsService.recordHttpRequest).toHaveBeenCalledWith(
      'GET',
      '/api/listings/:id',
      200,
      expect.any(Number),
    );
  });

  it('records a non-negative duration in seconds', async () => {
    const context = createContext({}, { statusCode: 200 });
    const next: CallHandler = { handle: () => of(undefined) };

    await new Promise<void>((resolve, reject) => {
      interceptor.intercept(context, next).subscribe({
        next: () => resolve(),
        error: reject,
      });
    });

    const [, , , durationSeconds] = mockMetricsService.recordHttpRequest.mock.calls[0] as [
      string,
      string,
      number,
      number,
    ];
    expect(durationSeconds).toBeGreaterThanOrEqual(0);
  });
});
