import { CallHandler, ExecutionContext, RequestTimeoutException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { firstValueFrom, of, timer } from 'rxjs';
import { RequestTimeoutInterceptor } from './request-timeout.interceptor';

describe('RequestTimeoutInterceptor', () => {
  const createContext = (url = '/api/v1/feed/me') =>
    ({
      getType: () => 'http',
      getHandler: () => createContext,
      getClass: () => RequestTimeoutInterceptor,
      switchToHttp: () => ({ getRequest: () => ({ originalUrl: url }) }),
    }) as unknown as ExecutionContext;

  const createCoordinator = (draining = false) => ({
    isDraining: jest.fn().mockReturnValue(draining),
    beginRequest: jest.fn().mockReturnValue(!draining),
    endRequest: jest.fn(),
  });

  const createInterceptor = (options: { timeoutMs?: number; override?: number; draining?: boolean }) => {
    const config = {
      get: jest.fn().mockReturnValue(options.timeoutMs ?? 10),
    } as unknown as ConfigService;
    const reflector = {
      getAllAndOverride: jest.fn().mockReturnValue(options.override),
    } as unknown as Reflector;
    const coordinator = createCoordinator(options.draining);
    return {
      coordinator,
      interceptor: new RequestTimeoutInterceptor(config, reflector, coordinator as any),
    };
  };

  it('returns the response and releases the in-flight request', async () => {
    const { interceptor, coordinator } = createInterceptor({ timeoutMs: 50 });
    const handler: CallHandler = { handle: () => of({ ok: true }) };

    await expect(firstValueFrom(interceptor.intercept(createContext(), handler))).resolves.toEqual({
      ok: true,
    });
    expect(coordinator.beginRequest).toHaveBeenCalledTimes(1);
    expect(coordinator.endRequest).toHaveBeenCalledTimes(1);
  });

  it('turns a slow handler into HTTP 408 and releases the request', async () => {
    const { interceptor, coordinator } = createInterceptor({ timeoutMs: 5 });
    const handler: CallHandler = { handle: () => timer(30) };

    await expect(firstValueFrom(interceptor.intercept(createContext(), handler))).rejects.toBeInstanceOf(
      RequestTimeoutException,
    );
    expect(coordinator.endRequest).toHaveBeenCalledTimes(1);
  });

  it('allows a route-level zero override to disable the timeout', async () => {
    const { interceptor } = createInterceptor({ timeoutMs: 5, override: 0 });
    const handler: CallHandler = { handle: () => timer(15) };

    await expect(firstValueFrom(interceptor.intercept(createContext(), handler))).resolves.toBe(0);
  });

  it('rejects new work while draining but still allows readiness checks', async () => {
    const { interceptor } = createInterceptor({ draining: true });
    const handler: CallHandler = { handle: () => of({ status: 'draining' }) };

    await expect(firstValueFrom(interceptor.intercept(createContext(), handler))).rejects.toMatchObject({
      status: 503,
    });
    await expect(
      firstValueFrom(interceptor.intercept(createContext('/api/v1/health/ready'), handler)),
    ).resolves.toEqual({ status: 'draining' });
  });
});
