import { describe, expect, it } from 'vitest';
import { Code, ConnectError } from '@connectrpc/connect';
import { backoffMs, isConnectionCode, retryable, toGatewayError } from './retry';

describe('toGatewayError', () => {
  it('collapses every transport-level failure to a connection code (so it is retried + suppressed)', () => {
    for (const c of [Code.Unavailable, Code.DeadlineExceeded, Code.Canceled, Code.Aborted, Code.Unknown]) {
      const e = toGatewayError(new ConnectError('x', c));
      expect(e.code).toBe('unavailable');
      expect(isConnectionCode(e.code)).toBe(true);
    }
  });

  it('treats a raw (non-Connect) network error as a connection failure', () => {
    expect(toGatewayError(new TypeError('Failed to fetch')).code).toBe('unavailable');
  });

  it('preserves rate-limit, an invalid session, and a genuine server-internal error', () => {
    expect(toGatewayError(new ConnectError('x', Code.ResourceExhausted)).code).toBe('rate_limited');
    expect(toGatewayError(new ConnectError('x', Code.Unauthenticated)).code).toBe('session_invalid');
    expect(toGatewayError(new ConnectError('x', Code.Internal)).code).toBe('internal');
    expect(isConnectionCode('internal')).toBe(false);
  });

  it('maps the Subscribe FailedPrecondition to the update_required sentinel (client too old)', () => {
    expect(toGatewayError(new ConnectError('x', Code.FailedPrecondition)).code).toBe('update_required');
  });
});

describe('retryable', () => {
  it('does not auto-retry a rate-limit rejection (a deliberate throttle, surfaced not spun)', () => {
    // A rate-limit is the server saying "slow down": auto-retrying cannot succeed until the bucket
    // refills, only freezes the calling button for the whole backoff and feeds the gateway's ban
    // tripwire with more rejections. It is surfaced to the caller instead. (Regression guard: the
    // old retry path also called reportOffline, which on the session-less login screen — where the
    // reachability probe can never heal — latched a stuck phantom offline.)
    expect(retryable('rate_limited', 'game.submit_play')).toBe(false);
    expect(retryable('rate_limited', 'games.list')).toBe(false);
    expect(retryable('rate_limited', 'auth.email.login')).toBe(false);
  });

  it('retries only read-only ops on a transport unavailable (a mutation could double-apply)', () => {
    expect(retryable('unavailable', 'games.list')).toBe(true);
    expect(retryable('unavailable', 'game.state')).toBe(true);
    expect(retryable('unavailable', 'draft.get')).toBe(true);
    // mutations are not auto-resent on a dropped connection
    expect(retryable('unavailable', 'game.submit_play')).toBe(false);
    expect(retryable('unavailable', 'chat.post')).toBe(false);
    expect(retryable('unavailable', 'game.hide')).toBe(false);
  });

  it('never retries a domain rejection or an unknown code', () => {
    expect(retryable('not_your_turn', 'game.submit_play')).toBe(false);
    expect(retryable('illegal_play', 'game.submit_play')).toBe(false);
    expect(retryable('not_found', 'game.state')).toBe(false);
    expect(retryable('internal', 'games.list')).toBe(false);
  });
});

describe('isConnectionCode', () => {
  it('flags only the transport connectivity code (a rate-limit is a surfaced message, not connectivity)', () => {
    expect(isConnectionCode('unavailable')).toBe(true);
    // A rate-limit must NOT read as connectivity: isConnectionCode gates both reportOffline (the
    // offline chrome) and handleError's toast suppression, so treating it as connectivity produced
    // a silent, stuck phantom offline on the login screen. It surfaces as error.rate_limited.
    expect(isConnectionCode('rate_limited')).toBe(false);
    expect(isConnectionCode('not_your_turn')).toBe(false);
    expect(isConnectionCode('internal')).toBe(false);
  });
});

describe('backoffMs', () => {
  it('grows exponentially and caps at 8s (+ jitter under 250ms)', () => {
    // attempt 1 ~ 500, 2 ~ 1000, 3 ~ 2000, 4 ~ 4000, 5 ~ 8000, then capped.
    expect(backoffMs(1)).toBeGreaterThanOrEqual(500);
    expect(backoffMs(1)).toBeLessThan(750);
    expect(backoffMs(3)).toBeGreaterThanOrEqual(2000);
    expect(backoffMs(3)).toBeLessThan(2250);
    for (const n of [5, 6, 10, 20]) {
      expect(backoffMs(n)).toBeGreaterThanOrEqual(8000);
      expect(backoffMs(n)).toBeLessThan(8250);
    }
  });
});
