import {
  buildURL,
  callHooks,
  detectResponseType,
  isJSONSerializable,
  isPayloadMethod,
  joinURL,
  resolveFetchOptions,
} from './utils';
import { describe, expect, it } from 'vitest';
import type { FetchContext } from './types';

// ---------------------------------------------------------------------------
// isPayloadMethod
// ---------------------------------------------------------------------------

describe('isPayloadMethod', () => {
  it('returns true for payload methods', () => {
    expect(isPayloadMethod('POST')).toBe(true);
    expect(isPayloadMethod('PUT')).toBe(true);
    expect(isPayloadMethod('PATCH')).toBe(true);
    expect(isPayloadMethod('DELETE')).toBe(true);
  });

  it('returns false for non-payload methods', () => {
    expect(isPayloadMethod('GET')).toBe(false);
    expect(isPayloadMethod('HEAD')).toBe(false);
    expect(isPayloadMethod('OPTIONS')).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// isJSONSerializable
// ---------------------------------------------------------------------------

describe('isJSONSerializable', () => {
  it('returns false for undefined', () => {
    expect(isJSONSerializable(undefined)).toBe(false);
  });

  it('returns true for primitives', () => {
    expect(isJSONSerializable('hello')).toBe(true);
    expect(isJSONSerializable(42)).toBe(true);
    expect(isJSONSerializable(true)).toBe(true);
    expect(isJSONSerializable(null)).toBe(true);
  });

  it('returns false for functions, symbols, bigints', () => {
    expect(isJSONSerializable(() => {})).toBe(false);
    expect(isJSONSerializable(Symbol('x'))).toBe(false);
    expect(isJSONSerializable(42n)).toBe(false);
  });

  it('returns true for plain arrays', () => {
    expect(isJSONSerializable([1, 2, 3])).toBe(true);
  });

  it('returns false for ArrayBuffer-like values', () => {
    expect(isJSONSerializable(new Uint8Array([1, 2]))).toBe(false);
  });

  it('returns false for FormData and URLSearchParams', () => {
    expect(isJSONSerializable(new FormData())).toBe(false);
    expect(isJSONSerializable(new URLSearchParams())).toBe(false);
  });

  it('returns true for plain objects', () => {
    expect(isJSONSerializable({ a: 1 })).toBe(true);
  });

  it('returns true for objects with toJSON', () => {
    expect(isJSONSerializable({ toJSON: () => ({}) })).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// detectResponseType
// ---------------------------------------------------------------------------

describe('detectResponseType', () => {
  it('defaults to json when content-type is empty', () => {
    expect(detectResponseType('')).toBe('json');
    expect(detectResponseType()).toBe('json');
  });

  it('detects json content types', () => {
    expect(detectResponseType('application/json')).toBe('json');
    expect(detectResponseType('application/json; charset=utf-8')).toBe('json');
    expect(detectResponseType('application/vnd.api+json')).toBe('json');
  });

  it('detects event-stream as stream', () => {
    expect(detectResponseType('text/event-stream')).toBe('stream');
  });

  it('detects text content types', () => {
    expect(detectResponseType('text/plain')).toBe('text');
    expect(detectResponseType('text/html')).toBe('text');
    expect(detectResponseType('application/xml')).toBe('text');
  });

  it('falls back to blob for binary types', () => {
    expect(detectResponseType('image/png')).toBe('blob');
    expect(detectResponseType('application/octet-stream')).toBe('blob');
  });
});

// ---------------------------------------------------------------------------
// buildURL
// ---------------------------------------------------------------------------

describe('buildURL', () => {
  it('appends query params to a clean URL', () => {
    expect(buildURL('https://api.example.com', { page: 1, limit: 20 })).toBe(
      'https://api.example.com?page=1&limit=20',
    );
  });

  it('appends to an existing query string with &', () => {
    expect(buildURL('https://api.example.com?foo=bar', { baz: 'qux' })).toBe(
      'https://api.example.com?foo=bar&baz=qux',
    );
  });

  it('omits null and undefined values', () => {
    expect(buildURL('https://api.example.com', { a: null, b: undefined, c: 'keep' })).toBe(
      'https://api.example.com?c=keep',
    );
  });

  it('returns the URL unchanged when all params are omitted', () => {
    expect(buildURL('https://api.example.com', { a: null })).toBe('https://api.example.com');
  });

  it('inserts the query string before a fragment', () => {
    expect(buildURL('https://api.example.com/p#section', { a: 1 })).toBe(
      'https://api.example.com/p?a=1#section',
    );
  });

  it('appends to an existing query string before a fragment', () => {
    expect(buildURL('https://api.example.com/p?foo=bar#section', { baz: 'qux' })).toBe(
      'https://api.example.com/p?foo=bar&baz=qux#section',
    );
  });
});

// ---------------------------------------------------------------------------
// joinURL
// ---------------------------------------------------------------------------

describe('joinURL', () => {
  it('joins base and path correctly', () => {
    expect(joinURL('https://api.example.com/v1', '/users')).toBe(
      'https://api.example.com/v1/users',
    );
  });

  it('does not double slashes', () => {
    expect(joinURL('https://api.example.com/v1/', '/users')).toBe(
      'https://api.example.com/v1/users',
    );
  });

  it('adds a slash when neither side has one', () => {
    expect(joinURL('https://api.example.com/v1', 'users')).toBe(
      'https://api.example.com/v1/users',
    );
  });

  it('returns base when path is empty', () => {
    expect(joinURL('https://api.example.com', '')).toBe('https://api.example.com');
  });

  it('returns base when path is "/"', () => {
    expect(joinURL('https://api.example.com', '/')).toBe('https://api.example.com');
  });
});

// ---------------------------------------------------------------------------
// callHooks
// ---------------------------------------------------------------------------

describe('callHooks', () => {
  function makeCtx(): FetchContext {
    return {
      request: 'https://example.com',
      options: { headers: new Headers() },
      response: undefined,
      error: undefined,
    } as FetchContext;
  }

  it('does nothing when hooks is undefined', async () => {
    await expect(callHooks(makeCtx(), undefined)).resolves.toBeUndefined();
  });

  it('calls a single hook', async () => {
    const calls: number[] = [];
    await callHooks(makeCtx(), () => {
      calls.push(1);
    });
    expect(calls).toEqual([1]);
  });

  it('calls an array of hooks in order', async () => {
    const calls: number[] = [];
    await callHooks(makeCtx(), [
      () => { calls.push(1); },
      () => { calls.push(2); },
      () => { calls.push(3); },
    ]);
    expect(calls).toEqual([1, 2, 3]);
  });

  it('awaits async hooks', async () => {
    const calls: number[] = [];
    await callHooks(makeCtx(), [
      async () => {
        await Promise.resolve();
        calls.push(1);
      },
      () => {
        calls.push(2);
      },
    ]);
    expect(calls).toEqual([1, 2]);
  });
});

// ---------------------------------------------------------------------------
// resolveFetchOptions
// ---------------------------------------------------------------------------

describe('resolveFetchOptions', () => {
  it('returns an object with a Headers instance', () => {
    const resolved = resolveFetchOptions('https://example.com', undefined, undefined);
    expect(resolved.headers).toBeInstanceOf(Headers);
  });

  it('merges input and default headers (input wins)', () => {
    const resolved = resolveFetchOptions(
      'https://example.com',
      { headers: { 'x-custom': 'input' } },
      { headers: { 'x-custom': 'default', 'x-default-only': 'yes' } },
    );
    expect(resolved.headers.get('x-custom')).toBe('input');
    expect(resolved.headers.get('x-default-only')).toBe('yes');
  });

  it('merges query params from defaults and input', () => {
    const resolved = resolveFetchOptions(
      'https://example.com',
      { query: { a: '1' } },
      { query: { b: '2' } },
    );
    expect(resolved.query).toEqual({ a: '1', b: '2' });
  });

  it('merges params alias into query', () => {
    const resolved = resolveFetchOptions(
      'https://example.com',
      { params: { p: '10' } },
      undefined,
    );
    expect(resolved.query).toEqual({ p: '10' });
    expect(resolved.params).toEqual({ p: '10' });
  });
});
