import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AlqoveClient } from '../client';

/**
 * Captures the arguments of the most recent fetch call so assertions can read
 * the URL, method, headers, and body the client produced.
 */
function mockFetchOk(json: unknown = { ok: true }) {
  const fetchMock = vi.fn(async () => ({
    ok: true,
    status: 200,
    json: async () => json,
  })) as unknown as typeof fetch;
  vi.stubGlobal('fetch', fetchMock);
  return fetchMock as unknown as ReturnType<typeof vi.fn>;
}

function headersOf(fetchMock: ReturnType<typeof vi.fn>, callIndex = 0): Record<string, string> {
  return fetchMock.mock.calls[callIndex][1].headers as Record<string, string>;
}

describe('AlqoveClient per-call headers + idempotency + auth (Task 0)', () => {
  beforeEach(() => {
    // Deterministic auto-generated key so we can detect when it is NOT used.
    vi.spyOn(crypto, 'randomUUID').mockReturnValue('00000000-0000-0000-0000-000000000000');
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    vi.restoreAllMocks();
  });

  it('auto-generates a random Idempotency-Key on POST when none supplied (back-compat)', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({ baseUrl: 'http://api.test' });

    await client.post('/v1/things', { a: 1 });

    expect(headersOf(fetchMock)['Idempotency-Key']).toBe(
      '00000000-0000-0000-0000-000000000000',
    );
  });

  it('lets a caller-supplied Idempotency-Key override the auto-generated one (no random key)', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({ baseUrl: 'http://api.test' });

    await client.request('POST', '/v1/checkin/abc/requests', {
      body: { idempotency_key: 'stable-key-123' },
      headers: { 'Idempotency-Key': 'stable-key-123' },
    });

    expect(crypto.randomUUID).not.toHaveBeenCalled();
    expect(headersOf(fetchMock)['Idempotency-Key']).toBe('stable-key-123');
  });

  it('re-sends the identical Idempotency-Key on retry (safe replay, no regeneration)', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({ baseUrl: 'http://api.test' });

    const opts = {
      body: { idempotency_key: 'stable-key-123' },
      headers: { 'Idempotency-Key': 'stable-key-123' },
    };
    await client.request('POST', '/v1/checkin/abc/requests', opts);
    await client.request('POST', '/v1/checkin/abc/requests', opts);

    expect(headersOf(fetchMock, 0)['Idempotency-Key']).toBe('stable-key-123');
    expect(headersOf(fetchMock, 1)['Idempotency-Key']).toBe('stable-key-123');
  });

  it('sets Authorization from a per-call authToken, overriding the constructed getToken', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({
      baseUrl: 'http://api.test',
      getToken: () => 'constructed-token',
    });

    await client.request('POST', '/v1/checkin/abc/requests', {
      body: { x: 1 },
      authToken: 'per-call-token',
    });

    expect(headersOf(fetchMock)['Authorization']).toBe('Bearer per-call-token');
  });

  it('does not send Authorization when no getToken and no authToken (anonymous)', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({ baseUrl: 'http://api.test' });

    await client.request('GET', '/v1/checkin/abc');

    expect(headersOf(fetchMock)['Authorization']).toBeUndefined();
  });

  it('still uses the constructed getToken when no per-call authToken given (back-compat)', async () => {
    const fetchMock = mockFetchOk();
    const client = new AlqoveClient({
      baseUrl: 'http://api.test',
      getToken: () => 'constructed-token',
    });

    await client.get('/v1/me');

    expect(headersOf(fetchMock)['Authorization']).toBe('Bearer constructed-token');
  });
});

describe('AlqoveClient 204 No Content (Port 00 DELETE endpoints)', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('resolves undefined on 204 instead of calling response.json()', async () => {
    const json = vi.fn(async () => {
      throw new SyntaxError('Unexpected end of JSON input');
    });
    vi.stubGlobal(
      'fetch',
      vi.fn(async () => ({ ok: true, status: 204, json })) as unknown as typeof fetch,
    );
    const client = new AlqoveClient({ baseUrl: 'http://api.test' });

    await expect(client.delete('/v1/stores/s1/members/m1')).resolves.toBeUndefined();
    expect(json).not.toHaveBeenCalled();
  });
});
