import { ERROR_CODES } from '@insforge/shared-schemas';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Response } from 'node-fetch';

// Helper to make a JSON response that node-fetch will accept.
function jsonResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json', ...headers },
  });
}

describe('Deno Subhosting 429 backoff', () => {
  beforeEach(() => {
    vi.resetModules();
    process.env.DENO_DEPLOY_TOKEN = 't';
    process.env.DENO_DEPLOY_ORG_ID = 'o';
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.doUnmock('node-fetch');
    delete process.env.DENO_DEPLOY_TOKEN;
    delete process.env.DENO_DEPLOY_ORG_ID;
  });

  it('retries on 429 with exponential backoff and eventually succeeds', async () => {
    let attempts = 0;
    vi.doMock('node-fetch', () => ({
      __esModule: true,
      default: vi.fn(async () => {
        attempts++;
        if (attempts < 3) {
          return new Response('', { status: 429 });
        }
        return jsonResponse({
          id: 'dep-1',
          projectId: 'proj-1',
          status: 'succeeded',
          failure_reason: null,
          created_at: new Date().toISOString(),
        });
      }),
      Response,
    }));

    const mod = await import('@/providers/functions/deno-subhosting.provider.js');
    const provider = mod.DenoSubhostingProvider.getInstance();

    const result = await provider.getDeployment('dep-1');
    expect(result).toBeDefined();
    expect(attempts).toBe(3);
  }, 30_000);

  it('honors Retry-After header in seconds', async () => {
    const start = Date.now();
    let attempts = 0;
    vi.doMock('node-fetch', () => ({
      __esModule: true,
      default: vi.fn(async () => {
        attempts++;
        if (attempts === 1) {
          return new Response('', { status: 429, headers: { 'retry-after': '1' } });
        }
        return jsonResponse({
          id: 'dep-2',
          projectId: 'proj-1',
          status: 'succeeded',
          failure_reason: null,
          created_at: new Date().toISOString(),
        });
      }),
      Response,
    }));

    const mod = await import('@/providers/functions/deno-subhosting.provider.js');
    const provider = mod.DenoSubhostingProvider.getInstance();

    await provider.getDeployment('dep-2');
    const elapsed = Date.now() - start;
    expect(elapsed).toBeGreaterThanOrEqual(900);
  }, 30_000);

  it('throws AppError(429, RATE_LIMITED) after exhausting retries', async () => {
    // Pin Math.random so the inner jitter is deterministic — keeps the test
    // free of flakes around timing of the 4 attempts.
    vi.spyOn(Math, 'random').mockReturnValue(0);

    let attempts = 0;
    vi.doMock('node-fetch', () => ({
      __esModule: true,
      default: vi.fn(async () => {
        attempts++;
        return new Response('', { status: 429 });
      }),
      Response,
    }));

    const mod = await import('@/providers/functions/deno-subhosting.provider.js');
    const { AppError } = await import('@/utils/errors.js');
    const provider = mod.DenoSubhostingProvider.getInstance();

    // Exhausted retries must surface as 429 RATE_LIMITED, not the generic 500
    // INTERNAL_ERROR that callers would otherwise raise from `!response.ok`.
    await expect(provider.getDeployment('dep-3')).rejects.toMatchObject({
      constructor: AppError,
      statusCode: 429,
      code: ERROR_CODES.RATE_LIMITED,
    });
    // initial + 3 retries from DEFAULT_RATE_LIMIT_BACKOFF_MS = 4 attempts
    expect(attempts).toBeGreaterThanOrEqual(4);
  }, 30_000);

  it('caps Retry-After at 30s even when upstream asks for much longer', async () => {
    // Spy on setTimeout to capture every requested delay without actually
    // waiting. The provider also uses setTimeout for its initial fetch
    // timeout; we only care that NO requested delay exceeds the 30s cap.
    const recordedDelays: number[] = [];
    const realSetTimeout = global.setTimeout;
    vi.spyOn(global, 'setTimeout').mockImplementation(((
      cb: (...args: unknown[]) => void,
      ms?: number
    ) => {
      recordedDelays.push(ms ?? 0);
      // Run the callback async so abort flows still work, but immediately.
      return realSetTimeout(cb, 0);
    }) as unknown as typeof setTimeout);

    let attempts = 0;
    vi.doMock('node-fetch', () => ({
      __esModule: true,
      default: vi.fn(async () => {
        attempts++;
        if (attempts === 1) {
          // 600 seconds — way beyond the 30s cap.
          return new Response('', { status: 429, headers: { 'retry-after': '600' } });
        }
        return jsonResponse({
          id: 'dep-cap',
          projectId: 'proj-1',
          status: 'succeeded',
          failure_reason: null,
          created_at: new Date().toISOString(),
        });
      }),
      Response,
    }));

    const mod = await import('@/providers/functions/deno-subhosting.provider.js');
    const provider = mod.DenoSubhostingProvider.getInstance();
    await provider.getDeployment('dep-cap');

    // No delay (timeout or backoff) should ever exceed the 30s cap.
    const MAX_RETRY_AFTER_MS = 30_000;
    for (const d of recordedDelays) {
      expect(d).toBeLessThanOrEqual(MAX_RETRY_AFTER_MS);
    }
  }, 30_000);
});

describe('parseRetryAfterMs', () => {
  it('parses integer seconds into ms', async () => {
    const { parseRetryAfterMs } = await import('@/providers/functions/deno-subhosting.provider.js');
    expect(parseRetryAfterMs('5')).toBe(5000);
    expect(parseRetryAfterMs(' 12 ')).toBe(12_000);
    expect(parseRetryAfterMs('0')).toBe(0);
  });

  it('parses HTTP-date as delta from now', async () => {
    const { parseRetryAfterMs } = await import('@/providers/functions/deno-subhosting.provider.js');
    const future = new Date(Date.now() + 5000).toUTCString();
    const delta = parseRetryAfterMs(future);
    // Allow some slop for clock movement during the test.
    expect(delta).toBeGreaterThanOrEqual(4000);
    expect(delta).toBeLessThanOrEqual(6000);
  });

  it('clamps a past HTTP-date at 0', async () => {
    const { parseRetryAfterMs } = await import('@/providers/functions/deno-subhosting.provider.js');
    const past = new Date(Date.now() - 60_000).toUTCString();
    expect(parseRetryAfterMs(past)).toBe(0);
  });

  it('returns NaN for null, empty, or unparseable input', async () => {
    const { parseRetryAfterMs } = await import('@/providers/functions/deno-subhosting.provider.js');
    expect(parseRetryAfterMs(null)).toBeNaN();
    expect(parseRetryAfterMs('')).toBeNaN();
    expect(parseRetryAfterMs('not-a-date')).toBeNaN();
  });
});
