import { ApolloLink, Observable } from '@apollo/client/core';

import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';

jest.mock('~/captcha/wait_for_captcha_to_be_solved');

describe('apolloCaptchaLink', () => {
  const SPAM_LOG_ID = 'SPAM_LOG_ID';
  const CAPTCHA_SITE_KEY = 'CAPTCHA_SITE_KEY';
  const CAPTCHA_RESPONSE = 'CAPTCHA_RESPONSE';

  const SUCCESS_RESPONSE = {
    data: {
      user: {
        id: 3,
        name: 'foo',
      },
    },
    errors: [],
  };

  const NON_CAPTCHA_ERROR_RESPONSE = {
    data: {
      user: null,
    },
    errors: [
      {
        message: 'Something is severely wrong with your query.',
        path: ['user'],
        locations: [{ line: 2, column: 3 }],
        extensions: {
          message: 'Object not found',
          type: 2,
        },
      },
    ],
  };

  const SPAM_ERROR_RESPONSE = {
    data: {
      user: null,
    },
    errors: [
      {
        message: 'Your Query was detected to be spam.',
        path: ['user'],
        locations: [{ line: 2, column: 3 }],
        extensions: {
          spam: true,
        },
      },
    ],
  };

  const CAPTCHA_ERROR_RESPONSE = {
    data: {
      user: null,
    },
    errors: [
      {
        message: 'This is an unrelated error, captcha should still work despite this.',
        path: ['user'],
        locations: [{ line: 2, column: 3 }],
      },
      {
        message: 'You need to solve a Captcha.',
        path: ['user'],
        locations: [{ line: 2, column: 3 }],
        extensions: {
          spam: true,
          needs_captcha_response: true,
          captcha_site_key: CAPTCHA_SITE_KEY,
          spam_log_id: SPAM_LOG_ID,
        },
      },
    ],
  };

  let link;

  let mockLinkImplementation;
  let mockContext;

  const setupLink = (...responses) => {
    mockLinkImplementation = jest.fn().mockImplementation(() => {
      return Observable.of(responses.shift());
    });
    link = ApolloLink.from([apolloCaptchaLink, new ApolloLink(mockLinkImplementation)]);
  };

  function mockOperation() {
    mockContext = jest.fn();
    return { operationName: 'operation', variables: {}, setContext: mockContext };
  }

  it('successful responses are passed through', () => {
    setupLink(SUCCESS_RESPONSE);

    return new Promise((resolve) => {
      link.request(mockOperation()).subscribe((result) => {
        expect(result).toEqual(SUCCESS_RESPONSE);
        expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
        expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
        resolve();
      });
    });
  });

  it('non-spam related errors are passed through', () => {
    setupLink(NON_CAPTCHA_ERROR_RESPONSE);

    return new Promise((resolve) => {
      link.request(mockOperation()).subscribe((result) => {
        expect(result).toEqual(NON_CAPTCHA_ERROR_RESPONSE);
        expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
        expect(mockContext).not.toHaveBeenCalled();
        expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
        resolve();
      });
    });
  });

  it('unresolvable spam errors are passed through', () => {
    setupLink(SPAM_ERROR_RESPONSE);
    return new Promise((resolve) => {
      link.request(mockOperation()).subscribe((result) => {
        expect(result).toEqual(SPAM_ERROR_RESPONSE);
        expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
        expect(mockContext).not.toHaveBeenCalled();
        expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
        resolve();
      });
    });
  });

  describe('resolvable spam errors', () => {
    it('re-submits request with spam headers if the captcha modal was solved correctly', () => {
      waitForCaptchaToBeSolved.mockResolvedValue(CAPTCHA_RESPONSE);
      setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
      return new Promise((resolve) => {
        link.request(mockOperation()).subscribe((result) => {
          expect(result).toEqual(SUCCESS_RESPONSE);
          expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
          expect(mockContext).toHaveBeenCalledWith({
            headers: {
              'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
              'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
            },
          });
          expect(mockLinkImplementation).toHaveBeenCalledTimes(2);
          resolve();
        });
      });
    });

    it('throws error if the captcha modal was not solved correctly', () => {
      const error = new UnsolvedCaptchaError();
      waitForCaptchaToBeSolved.mockRejectedValue(error);

      setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
      return new Promise((resolve, reject) => {
        link.request(mockOperation()).subscribe({
          next: reject,
          error: (result) => {
            expect(result).toEqual(error);
            expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
            expect(mockContext).not.toHaveBeenCalled();
            expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
            resolve();
          },
        });
      });
    });
  });
});