/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @emails oncall+relay
 * @format
 */

// flowlint ambiguous-object-type:error

'use strict';

const fetchQuery = require('../fetchQuery');

const {
  createOperationDescriptor,
  getRequest,
  RelayFeatureFlags,
} = require('relay-runtime');
const {
  createMockEnvironment,
  generateAndCompile,
} = require('relay-test-utils-internal');

const response = {
  data: {
    node: {
      __typename: 'User',
      id: '4',
    },
  },
};

describe('fetchQuery', () => {
  let query;
  let variables;
  let environment;
  let retained = [];
  beforeEach(() => {
    retained = [];
    environment = createMockEnvironment();
    environment.retain.mockImplementation(obj => {
      const idx = retained.push(obj);
      return {
        dispose: () => {
          retained = retained.filter((o, ii) => ii === idx);
        },
      };
    });
    variables = {id: '4'};
    query = generateAndCompile(
      `query TestQuery($id: ID!) {
          node(id: $id) {
            id
          }
        }
      `,
    ).TestQuery;
  });

  it('fetches request and does not retain data', () => {
    let calledObserver = false;
    const observer = {
      complete: () => {
        calledObserver = true;
        expect(retained.length).toEqual(0);
      },
    };
    const subscription = fetchQuery(environment, query, variables).subscribe(
      observer,
    );
    environment.mock.nextValue(query, response);
    environment.mock.complete(query);
    subscription.unsubscribe();
    expect(calledObserver).toEqual(true);
    expect(retained.length).toEqual(0);
  });

  it('provides data snapshot on next', () => {
    let calledNext = false;
    const observer = {
      next: data => {
        calledNext = true;
        expect(retained.length).toEqual(0);
        expect(data).toEqual({
          node: {
            id: '4',
          },
        });
      },
    };
    fetchQuery(environment, query, variables).subscribe(observer);
    environment.mock.nextValue(query, response);
    expect(calledNext).toEqual(true);
    environment.mock.complete(query);
    expect(retained.length).toEqual(0);
  });

  it('unsubscribes when request is disposed', () => {
    let calledNext = false;
    let calledUnsubscribe = false;
    const observer = {
      next: () => {
        calledNext = true;
        expect(retained.length).toEqual(0);
      },
      unsubscribe: () => {
        calledUnsubscribe = true;
      },
    };
    const subscription = fetchQuery(environment, query, variables).subscribe(
      observer,
    );
    environment.mock.nextValue(query, response);
    subscription.unsubscribe();
    expect(calledNext).toEqual(true);
    expect(calledUnsubscribe).toEqual(true);
  });

  it('handles error correctly', () => {
    let calledError = false;
    const observer = {
      error: error => {
        calledError = true;
        expect(error.message).toEqual('Oops');
        expect(retained.length).toEqual(0);
      },
    };
    const subscription = fetchQuery(environment, query, variables).subscribe(
      observer,
    );
    environment.mock.reject(query, new Error('Oops'));
    expect(calledError).toEqual(true);
    expect(retained.length).toEqual(0);
    subscription.unsubscribe();
  });

  describe('.toPromise()', () => {
    it('fetches request and does not retain query data', async () => {
      const promise = fetchQuery(environment, query, variables).toPromise();
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(true);
      environment.mock.nextValue(query, response);
      const data = await promise;
      expect(data).toEqual({
        node: {
          id: '4',
        },
      });
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(true);
      expect(retained.length).toEqual(0);

      environment.mock.complete(query);
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(false);
      expect(retained.length).toEqual(0);
    });

    it('rejects when error occurs', async () => {
      const promise = fetchQuery(environment, query, variables).toPromise();
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(true);
      environment.mock.reject(query, new Error('Oops'));
      try {
        await promise;
      } catch (error) {
        expect(error.message).toEqual('Oops');
      }
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(false);
      expect(retained.length).toEqual(0);
    });
  });

  describe('store-or-network fetchPolicy', () => {
    it('fetches data if not cached yet', async () => {
      const promise = fetchQuery(environment, query, variables, {
        fetchPolicy: 'store-or-network',
      }).toPromise();
      // Needs to load data from network
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(true);
      environment.mock.nextValue(query, response);
      const data = await promise;
      expect(data).toEqual({
        node: {
          id: '4',
        },
      });
    });
    it('reads from store if cached already', async () => {
      // Populate the store
      const queryNode = getRequest(query);
      const operation = createOperationDescriptor(queryNode, variables);
      environment.getStore().retain(operation);
      environment.commitPayload(operation, response.data);

      // Fetch after data is in store
      const promise = fetchQuery(environment, query, variables, {
        fetchPolicy: 'store-or-network',
      }).toPromise();
      // Shouldn't be loading because the data is already cached
      expect(
        environment.mock.isLoading(query, variables, {force: true}),
      ).toEqual(false);
      const data = await promise;
      expect(data).toEqual({
        node: {
          id: '4',
        },
      });
    });
  });
});

describe('fetchQuery with missing @required value', () => {
  beforeEach(() => {
    RelayFeatureFlags.ENABLE_REQUIRED_DIRECTIVES = true;
  });
  afterEach(() => {
    RelayFeatureFlags.ENABLE_REQUIRED_DIRECTIVES = false;
  });
  it('provides data snapshot on next', () => {
    const logs = [];
    const environment = createMockEnvironment({
      log: event => {
        if (event.name === 'read.missing_required_field') {
          logs.push(event);
        }
      },
    });
    const query = generateAndCompile(
      `query TestQuery {
          me {
            name @required(action: LOG)
          }
        }
      `,
    ).TestQuery;

    const observer = {next: jest.fn()};
    const subscription = fetchQuery(environment, query, {}).subscribe(observer);
    expect(observer.next).not.toHaveBeenCalled();

    expect(logs.length).toBe(0);
    environment.mock.nextValue(query, {
      data: {
        me: {
          name: null,
        },
      },
    });
    subscription.unsubscribe();
    expect(observer.next).toHaveBeenCalledWith({me: null});
    expect(logs).toEqual([
      {
        fieldPath: 'me.name',
        name: 'read.missing_required_field',
        owner: 'TestQuery',
      },
    ]);
  });

  it('throws on resolution', () => {
    const environment = createMockEnvironment({});
    const query = generateAndCompile(
      `query TestQuery {
          me {
            name @required(action: THROW)
          }
        }
      `,
    ).TestQuery;

    const observer = {next: jest.fn(), error: jest.fn()};
    const subscription = fetchQuery(environment, query, {}).subscribe(observer);
    expect(observer.next).not.toHaveBeenCalled();
    expect(observer.error).not.toHaveBeenCalled();

    environment.mock.nextValue(query, {data: {me: {name: null}}});
    subscription.unsubscribe();
    expect(observer.error).toHaveBeenCalledWith(
      Error("Relay: Missing @required value at path 'me.name' in 'TestQuery'."),
    );
    expect(observer.next).not.toHaveBeenCalled();
  });

  it('does not report missing required values in fragments', () => {
    const environment = createMockEnvironment({});
    const query = generateAndCompile(
      `query TestQuery {
          me {
            ...TestFragment
          }
        }
        fragment TestFragment on User {
          name @required(action: THROW)
        }
      `,
    ).TestQuery;

    const observer = {next: jest.fn(), error: jest.fn()};
    const subscription = fetchQuery(environment, query, {}).subscribe(observer);
    environment.mock.nextValue(query, {data: {me: {name: null}}});

    subscription.unsubscribe();

    expect(observer.error).not.toHaveBeenCalled();
  });
});
