import { describe, it, expect } from 'vitest';
import { of, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { currentIdempotencyKey, httpClientFetch, withIdempotencyKey } from './api-client.provider';

/** Minimal stand-in for HttpClient — only `.request(...)` is ever called by the
 *  adapter under test, so no TestBed/HttpClientTestingModule needed. */
function fakeHttpClient(
  request: (method: string, url: string, options: { headers: Record<string, string> }) => unknown,
): HttpClient {
  return { request } as unknown as HttpClient;
}

describe('withIdempotencyKey / currentIdempotencyKey', () => {
  it('threads the key to every read made inside the wrapped fn', async () => {
    const seen: string[] = [];
    await withIdempotencyKey('fixed-key', async () => {
      seen.push(currentIdempotencyKey());
      seen.push(currentIdempotencyKey());
    });
    expect(seen).toEqual(['fixed-key', 'fixed-key']);
  });

  it('clears the key once the wrapped fn settles', async () => {
    await withIdempotencyKey('fixed-key', async () => undefined);
    expect(currentIdempotencyKey()).not.toBe('fixed-key');
  });

  it('falls back to a generated uuid-shaped key when none is pending', () => {
    expect(currentIdempotencyKey()).toMatch(
      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
    );
  });
});

describe('httpClientFetch', () => {
  it('sends the pending idempotency key as a header for a write, not a fresh one per attempt', async () => {
    let sentHeaders: Record<string, string> | undefined;
    const http = fakeHttpClient((_method, _url, opts) => {
      sentHeaders = opts.headers;
      return of(new HttpResponse({ status: 200, body: '' }));
    });

    await withIdempotencyKey('logical-submit-key', () =>
      httpClientFetch(http).fetch('/api/v1/change-requests', { method: 'POST' }),
    );

    expect(sentHeaders?.['Idempotency-Key']).toBe('logical-submit-key');
  });

  // `http.request(...)` itself is only called once per `fetch()` — it returns a
  // cold Observable, and `retry` resubscribes to *that*, not to `.request()`
  // again (exactly how Angular's real HttpClient triggers a fresh network call
  // per subscription). So attempts are counted where the resubscription lands:
  // the `throwError` factory, not the outer mock call.
  it('retries a failing GET twice before giving up', async () => {
    let attempts = 0;
    const http = fakeHttpClient(() =>
      throwError(() => {
        attempts++;
        return new HttpErrorResponse({ status: 500 });
      }),
    );

    const res = await httpClientFetch(http).fetch('/api/v1/notes', { method: 'GET' });

    expect(attempts).toBe(3); // 1 original + 2 retries
    expect(res.status).toBe(500);
  });

  it('never retries a failing write', async () => {
    let attempts = 0;
    const http = fakeHttpClient(() =>
      throwError(() => {
        attempts++;
        return new HttpErrorResponse({ status: 500 });
      }),
    );

    const res = await httpClientFetch(http).fetch('/api/v1/change-requests', { method: 'POST' });

    expect(attempts).toBe(1);
    expect(res.status).toBe(500);
  });
});
