import { describe, expect, it, vi } from 'vitest';

import { ApiError, createApiClient } from './api-client';
import { PublicConfigError, readPublicRuntimeConfig } from './config';

describe('public runtime config', () => {
  it('uses the production API origin without exposing a secret', () => {
    expect(readPublicRuntimeConfig({})).toEqual({ apiBaseUrl: 'https://api.press.zone' });
  });

  it('accepts HTTPS overrides and removes trailing slashes', () => {
    expect(readPublicRuntimeConfig({ PUBLIC_API_BASE_URL: 'https://preview-api.press.zone/' }))
      .toEqual({ apiBaseUrl: 'https://preview-api.press.zone' });
  });

  it.each([
    ['PUBLIC_STRIPE_SECRET', 'secret'],
    ['PUBLIC_DATABASE_URL', 'postgres://example'],
    ['PUBLIC_SESSION_TOKEN', 'token'],
  ])('rejects secret-like public configuration: %s', (name, value) => {
    expect(() => readPublicRuntimeConfig({ [name]: value })).toThrow(PublicConfigError);
  });

  it('rejects credentials embedded in the API URL', () => {
    expect(() => readPublicRuntimeConfig({ PUBLIC_API_BASE_URL: 'https://user:pass@api.press.zone' }))
      .toThrow('must not contain credentials');
  });
});

describe('API client', () => {
  it('always sends browser session credentials', async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      Response.json({ balance: 3 }),
    );
    const client = createApiClient({ apiBaseUrl: 'https://api.press.zone' }, fetchMock);

    await expect(client.request<{ balance: number }>('/v1/credits')).resolves.toEqual({ balance: 3 });
    expect(fetchMock).toHaveBeenCalledOnce();
    expect(fetchMock.mock.calls[0]?.[0]).toBe('https://api.press.zone/v1/credits');
    expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({ credentials: 'include', method: 'GET' });
  });

  it('serializes typed request bodies as JSON', async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(Response.json({ id: 'job-1' }));
    const client = createApiClient({ apiBaseUrl: 'https://api.press.zone' }, fetchMock);

    await client.request<{ id: string }, { uploadId: string }>('/v1/conversions', {
      method: 'POST',
      body: { uploadId: 'upload-1' },
    });

    expect(fetchMock.mock.calls[0]?.[1]?.body).toBe('{"uploadId":"upload-1"}');
  });

  it('normalizes structured HTTP errors', async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      Response.json(
        { code: 'OUT_OF_CREDITS', message: 'Buy credits to continue.', requestId: 'req-1' },
        { status: 409 },
      ),
    );
    const client = createApiClient({ apiBaseUrl: 'https://api.press.zone' }, fetchMock);

    const error = await client.request('/v1/conversions').catch((cause: unknown) => cause);
    expect(error).toBeInstanceOf(ApiError);
    expect(error).toMatchObject({
      kind: 'conflict',
      status: 409,
      message: 'The request conflicts with the current state.',
      code: 'OUT_OF_CREDITS',
      requestId: 'req-1',
    });
  });

  it('does not expose service-controlled error messages or unbounded metadata', async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      Response.json(
        {
          code: `INTERNAL_${'X'.repeat(100)}`,
          message: 'SQL failed at internal-db.example:5432 with password secret',
          requestId: `req-${'x'.repeat(200)}`,
        },
        { status: 500 },
      ),
    );
    const client = createApiClient({ apiBaseUrl: 'https://api.press.zone' }, fetchMock);

    await expect(client.request('/v1/conversions')).rejects.toMatchObject({
      kind: 'server',
      status: 500,
      message: 'The service could not complete the request.',
      code: null,
      requestId: null,
    });
  });

  it('normalizes transport failures without leaking implementation details', async () => {
    const fetchMock = vi.fn<typeof fetch>().mockRejectedValue(new Error('socket details'));
    const client = createApiClient({ apiBaseUrl: 'https://api.press.zone' }, fetchMock);

    await expect(client.request('/v1/session')).rejects.toMatchObject({
      kind: 'network',
      status: null,
      message: 'Unable to reach the service. Try again.',
    });
  });
});
