import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { toApiError } from './api-error.mapper';
import { ApiErrorCode } from './models/api-error.model';

function errorResponse(init: {
  status?: number;
  body?: unknown;
  headers?: Record<string, string>;
  url?: string;
}): HttpErrorResponse {
  return new HttpErrorResponse({
    status: init.status ?? 500,
    error: init.body,
    headers: new HttpHeaders(init.headers ?? {}),
    url: init.url ?? 'https://api.example.com/api/v2/offers',
  });
}

describe('toApiError', () => {
  it('reads a full envelope', () => {
    // Arrange
    const response = errorResponse({
      status: 422,
      body: {
        error: {
          code: 'VALIDATION_FAILED',
          message: 'One or more fields are invalid.',
          status: 422,
          requestId: 'b3f1c2a0',
          details: [{ field: 'sku', code: 'REQUIRED', message: 'SKU is required.' }],
        },
      },
    });

    // Act
    const error = toApiError(response);

    // Assert
    expect(error.code).toBe('VALIDATION_FAILED');
    expect(error.status).toBe(422);
    expect(error.requestId).toBe('b3f1c2a0');
    expect(error.hasEnvelope).toBeTrue();
    expect(error.details.length).toBe(1);
    expect(error.details[0].field).toBe('sku');
  });

  it('accepts an unwrapped envelope, because some gateways strip the wrapper', () => {
    const error = toApiError(
      errorResponse({ status: 409, body: { code: 'CONFLICT', message: 'Already exists.', status: 409 } }),
    );

    expect(error.code).toBe('CONFLICT');
    expect(error.hasEnvelope).toBeTrue();
  });

  it('synthesizes a code when the body carries no envelope', () => {
    const error = toApiError(errorResponse({ status: 404, body: '<html>Not Found</html>' }));

    expect(error.code).toBe(ApiErrorCode.NOT_FOUND);
    expect(error.hasEnvelope).toBeFalse();
    expect(error.details).toEqual([]);
  });

  it('maps a network failure to NETWORK_UNAVAILABLE', () => {
    const error = toApiError(errorResponse({ status: 0, body: null }));

    expect(error.code).toBe(ApiErrorCode.NETWORK_UNAVAILABLE);
    expect(error.status).toBe(0);
  });

  it('prefers the transport status over a self-reported one', () => {
    // A proxy can return 502 while the body still claims 200-era metadata.
    const error = toApiError(
      errorResponse({ status: 502, body: { error: { code: 'UPSTREAM', message: 'x', status: 200 } } }),
    );

    expect(error.status).toBe(502);
  });

  it('reads Retry-After given as seconds', () => {
    const error = toApiError(
      errorResponse({ status: 429, body: { error: { code: 'RATE_LIMITED', message: 'slow down', status: 429 } }, headers: { 'Retry-After': '17' } }),
    );

    expect(error.retryAfterSeconds).toBe(17);
  });

  it('reads Retry-After given as an HTTP date', () => {
    const future = new Date(Date.now() + 12_000).toUTCString();

    const error = toApiError(errorResponse({ status: 429, headers: { 'Retry-After': future } }));

    // Allow a second of slack for clock/rounding.
    expect(error.retryAfterSeconds).toBeGreaterThanOrEqual(11);
    expect(error.retryAfterSeconds).toBeLessThanOrEqual(13);
  });

  it('falls back to the body retryAfterSeconds when no header is present', () => {
    const error = toApiError(
      errorResponse({ status: 429, body: { error: { code: 'RATE_LIMITED', message: 'x', status: 429, retryAfterSeconds: 9 } } }),
    );

    expect(error.retryAfterSeconds).toBe(9);
  });

  it('defaults a 429 with no hint to a non-zero delay, so callers cannot busy-loop', () => {
    const error = toApiError(errorResponse({ status: 429, body: null }));

    expect(error.retryAfterSeconds).toBeGreaterThan(0);
  });

  it('never sets retryAfterSeconds for errors that are not throttled', () => {
    const error = toApiError(errorResponse({ status: 500, body: null }));

    expect(error.retryAfterSeconds).toBeUndefined();
  });

  it('drops malformed detail entries rather than failing', () => {
    const error = toApiError(
      errorResponse({
        status: 422,
        body: { error: { code: 'VALIDATION_FAILED', message: 'x', status: 422, details: [{ nope: 1 }, 'string', { field: 'sku' }] } },
      }),
    );

    expect(error.details.length).toBe(1);
    expect(error.details[0].field).toBe('sku');
    expect(error.details[0].code).toBe(ApiErrorCode.VALIDATION_FAILED);
  });

  it('treats an envelope with a non-string code as absent', () => {
    const error = toApiError(errorResponse({ status: 400, body: { error: { code: 42, message: 'x' } } }));

    expect(error.hasEnvelope).toBeFalse();
  });
});
