import nock from 'nock';
import createClient from 'openapi-fetch';
import { vi } from 'vitest';

import { paths } from '@/lib/gen';

export function createMockApiClient(
  apiBase = process.env.NEXT_PUBLIC_API_BASE_ECS || ''
) {
  return createClient<paths>({
    baseUrl: apiBase,
    fetch: async (url: RequestInfo | URL, options?: RequestInit) =>
      fetch(url, {
        ...options,
        headers: {
          ...options?.headers,
          Authorization: `Bearer TEST-TOKEN`,
        },
      }),
  });
}

/**
 * Converts an OpenAPI path with parameters to a RegExp that can be used by nock
 *
 * @example
 * ```ts
 * import nock from 'nock';
 *
 * const scope = nock('http://localhost:8000')
 *   .get(apiPathRegex('/api/clips/{id}'))
 *   .reply(200, { id: '123', title: 'Test Clip' });
 * ```
 */
export function apiPathRegex(apiPath: string) {
  return new RegExp(
    `^${apiPath
      .split('/')
      .map((pathPart) => {
        const matches = pathPart.match(/^\{(.*)\}$/);
        return matches
          ? `(?<${matches[1]}>[^\/]+)`
          : pathPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      })
      .join('/')}$`
  );
}

/**
 * Extracts the parameter values in A URI by comparing against the OpenAPI
 * pathname with parameters placeholders
 */
export function parseUriWithApiPath(uri: string, apiPath: string) {
  const params: Record<string, string> = {};
  const [, uriPath = '/', queryString = ''] =
    uri.match(/^([^?]*)(\?.*)?/) || [];
  const uriPathSplit = uriPath.split('/');
  const apiPathSplit = apiPath.split('/');
  for (let i = 0; i < apiPathSplit.length; i++) {
    const matchesParam = apiPathSplit[i].match(/^\{(.*)\}$/);
    if (matchesParam) {
      params[matchesParam[1]] = uriPathSplit[i] || '';
    }
  }
  return {
    uri,
    queryString,
    apiPath,
    params,
  };
}

/**
 * Utility for automatically converting a URI string from OpenAPI to a RegExp
 * that can be used by nock
 */
function wrapInterceptor(interceptor: nock.InterceptFunction) {
  return function interceptFn(
    this: nock.Scope,
    ...args: Parameters<nock.InterceptFunction>
  ) {
    const [uri, ...restArgs] = args;
    return interceptor.call(
      this,
      typeof uri === 'string' ? apiPathRegex(uri) : uri,
      ...restArgs
    );
  };
}

/**
 * `nock` scope that automatically resolves strings that contain path paramters
 * in the OpenAPI format for convenience
 *
 * This guards against unmocked API requests, becuase we should only be using
 * mocks in the tests to begin with.
 */
export const mockApiScope = nock(process.env.NEXT_PUBLIC_API_BASE_ECS || '', {
  allowUnmocked: false,
});

mockApiScope.get = wrapInterceptor(mockApiScope.get);
mockApiScope.post = wrapInterceptor(mockApiScope.post);
mockApiScope.put = wrapInterceptor(mockApiScope.put);
mockApiScope.head = wrapInterceptor(mockApiScope.head);
mockApiScope.patch = wrapInterceptor(mockApiScope.patch);
mockApiScope.merge = wrapInterceptor(mockApiScope.merge);
mockApiScope.delete = wrapInterceptor(mockApiScope.delete);
mockApiScope.options = wrapInterceptor(mockApiScope.options);

const mockApiClient = createMockApiClient();

/**
 * Replacement for the standard `@/lib/apiClient` module interface
 *
 * This is best used in conjunction with
 *
 * @example
 * ```ts
 * import nock from 'nock';
 * import { getMockModule, mockApiScope } from '@/__test__/mockApiClient';
 *
 * vi.mock('@/lib/apiClient', () => getMockModule());
 *
 * describe('your test suite', () => {
 *   beforeEach(() => {
 *     vi.clearAllMocks();
 *     nock.cleanAll();
 *   });
 *
 *   it('makes an API request', async () => {
 *     mockApiScope
 *       .get('/api/path/{param}')
 *       .reply(200, () => ({
 *         success: true,
 *       }));
 *
 *     // Test code that makes an API request to the above mocked path
 *     // ...
 *
 *   });
 * });
 * ```
 */
export function getMockModule() {
  return {
    useApiClient: () => mockApiClient,
    getInstance: () => mockApiClient,
    setGetAuthToken: vi.fn(),
  };
}
