/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import fetchMock from 'fetch-mock';
import callApi from '@superset-ui/core/src/connection/callApi/callApi';
import parseResponse from '@superset-ui/core/src/connection/callApi/parseResponse';

import { LOGIN_GLOB } from '../fixtures/constants';

describe('parseResponse()', () => {
  beforeAll(() => {
    fetchMock.get(LOGIN_GLOB, { result: '1234' });
  });

  afterAll(fetchMock.restore);

  const mockGetUrl = '/mock/get/url';
  const mockPostUrl = '/mock/post/url';
  const mockErrorUrl = '/mock/error/url';
  const mockNoParseUrl = '/mock/noparse/url';

  const mockGetPayload = { get: 'payload' };
  const mockPostPayload = { post: 'payload' };
  const mockErrorPayload = { status: 500, statusText: 'Internal error' };

  fetchMock.get(mockGetUrl, mockGetPayload);
  fetchMock.post(mockPostUrl, mockPostPayload);
  fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
  fetchMock.get(mockNoParseUrl, new Response('test response'));

  afterEach(fetchMock.reset);

  it('returns a Promise', () => {
    const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
    const parsedResponsePromise = parseResponse(apiPromise);
    expect(parsedResponsePromise).toBeInstanceOf(Promise);
  });

  it('resolves to { json, response } if the request succeeds', async () => {
    expect.assertions(4);
    const args = await parseResponse(callApi({ url: mockGetUrl, method: 'GET' }));
    expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
    const keys = Object.keys(args);
    expect(keys).toContain('response');
    expect(keys).toContain('json');
    expect(args.json).toEqual(expect.objectContaining(mockGetPayload) as typeof args.json);
  });

  it('throws if `parseMethod=json` and .json() fails', async () => {
    expect.assertions(3);

    const mockTextUrl = '/mock/text/url';
    const mockTextResponse =
      '<html><head></head><body>I could be a stack trace or something</body></html>';
    fetchMock.get(mockTextUrl, mockTextResponse);

    let error;
    try {
      await parseResponse(callApi({ url: mockTextUrl, method: 'GET' }));
    } catch (err) {
      error = err as Error;
    } finally {
      expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
      expect(error?.stack).toBeDefined();
      expect(error?.message).toContain('Unexpected token');
    }
  });

  it('resolves to { text, response } if the `parseMethod=text`', async () => {
    expect.assertions(4);

    // test with json + bigint to ensure that it was not first parsed as json
    const mockTextParseUrl = '/mock/textparse/url';
    const mockTextJsonResponse = '{ "value": 9223372036854775807 }';
    fetchMock.get(mockTextParseUrl, mockTextJsonResponse);

    const args = await parseResponse(callApi({ url: mockTextParseUrl, method: 'GET' }), 'text');
    expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1);
    const keys = Object.keys(args);
    expect(keys).toContain('response');
    expect(keys).toContain('text');
    expect(args.text).toBe(mockTextJsonResponse);
  });

  it('throws if parseMethod is not null|json|text', async () => {
    expect.assertions(1);

    let error;
    try {
      await parseResponse(
        callApi({ url: mockNoParseUrl, method: 'GET' }),
        'something-else' as never,
      );
    } catch (err) {
      error = err;
    } finally {
      expect(error.message).toEqual(expect.stringContaining('Expected parseResponse=json'));
    }
  });

  it('resolves to unmodified `Response` object if `parseMethod=null|raw`', async () => {
    expect.assertions(3);
    const responseNull = await parseResponse(callApi({ url: mockNoParseUrl, method: 'GET' }), null);
    const responseRaw = await parseResponse(callApi({ url: mockNoParseUrl, method: 'GET' }), 'raw');
    expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(2);
    expect(responseNull.bodyUsed).toBe(false);
    expect(responseRaw.bodyUsed).toBe(false);
  });

  it('rejects if request.ok=false', async () => {
    expect.assertions(3);
    const mockNotOkayUrl = '/mock/notokay/url';
    fetchMock.get(mockNotOkayUrl, 404); // 404s result in not response.ok=false

    const apiPromise = callApi({ url: mockNotOkayUrl, method: 'GET' });

    let error;
    try {
      await parseResponse(apiPromise);
    } catch (err) {
      error = err as { ok: boolean; status: number };
    } finally {
      expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1);
      expect(error?.ok).toBe(false);
      expect(error?.status).toBe(404);
    }
  });
});
