# frozen_string_literal: true

require 'spec_helper'

RSpec.describe LooksAhead do
  include GraphqlHelpers

  let_it_be(:the_user) { create(:user) }
  let_it_be(:label_a) { create(:label) }
  let_it_be(:label_b) { create(:label) }
  let_it_be(:issue_a) { create(:issue, author: the_user, labels: [label_a, label_b]) }
  let_it_be(:issue_b) { create(:issue, author: the_user, labels: [label_a]) }
  let_it_be(:issue_c) { create(:issue, author: the_user, labels: [label_b]) }
  let_it_be(:issue_d) { create(:issue, author: the_user) }

  # Simplified schema to test lookahead
  let_it_be(:schema) do
    issues_resolver = Class.new(GraphQL::Schema::Resolver) do
      include LooksAhead

      def resolve_with_lookahead(**args)
        apply_lookahead(object.issues)
      end

      def unconditional_includes
        [project: :group]
      end

      def preloads
        { labels: [:labels] }
      end
    end

    label = Class.new(GraphQL::Schema::Object) do
      graphql_name 'Label'
      field :id, Integer, null: false
    end
    issue = Class.new(GraphQL::Schema::Object) do
      graphql_name 'Issue'
      field :title, String, null: true
      field :labels, label.connection_type, null: true
    end
    user = Class.new(GraphQL::Schema::Object) do
      graphql_name 'User'
      field :name, String, null: true
      field :issues, issue.connection_type, null: true
      field :issues_with_lookahead, issue.connection_type, resolver: issues_resolver, null: true
    end

    Class.new(GraphQL::Schema) do
      query(Class.new(GraphQL::Schema::Object) do
        graphql_name 'Query'
        field :find_user, user, null: true do
          argument :username, String, required: true
        end

        def find_user(username:)
          context[:user_db].find { |u| u.username == username }
        end
      end)
    end
  end

  let(:document) do
    GraphQL.parse <<-GRAPHQL
    query($username: String!){
      findUser(username: $username) {
        name
        issues {
          nodes {
            title
            labels { nodes { id } }
          }
        }
        issuesWithLookahead {
          nodes {
            title
            labels { nodes { id } }
          }
        }
      }
    }
    GRAPHQL
  end

  def query(doc = document)
    GraphQL::Query.new(
      schema,
      document: doc,
      context: { user_db: [the_user] },
      variables: { username: the_user.username }
    )
  end

  def run_query(gql_query)
    query(GraphQL.parse(gql_query)).result
  end

  shared_examples 'a working query on the test schema' do
    it 'has a good test setup', :aggregate_failures do
      expected_label_ids = [label_a, label_b].cycle.take(4).map(&:id)
      issue_titles = [issue_a, issue_b, issue_c, issue_d].map(&:title)

      res = query.result

      expect(res['errors']).to be_blank
      expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name)
      %w[issues issuesWithLookahead].each do |field|
        expect(all_issue_titles(res, field)).to match_array(issue_titles)
        expect(all_label_ids(res, field)).to match_array(expected_label_ids)
      end
    end
  end

  it_behaves_like 'a working query on the test schema'

  it 'preloads labels on issues' do
    expect(the_user.issues).to receive(:preload).with({ project: :group }, :labels)

    query.result
  end

  it 'issues fewer queries than the naive approach' do
    the_user.reload # ensure no attributes are loaded before we begin
    naive = <<-GQL
    query($username: String!){
      findUser(username: $username) {
        name
        issues {
          nodes {
            labels { nodes { id } }
          }
        }
      }
    }
    GQL
    with_lookahead = <<-GQL
    query($username: String!){
      findUser(username: $username) {
        name
        issuesWithLookahead {
          nodes {
            labels { nodes { id } }
          }
        }
      }
    }
    GQL

    expect { run_query(with_lookahead) }.to issue_same_number_of_queries_as { run_query(naive) }.or_fewer
  end

  private

  def all_label_ids(result, field_name)
    result.dig('data', 'findUser', field_name, 'nodes').flat_map do |node|
      node.dig('labels', 'nodes').map { |n| n['id'] }
    end
  end

  def all_issue_titles(result, field_name)
    result.dig('data', 'findUser', field_name, 'nodes').map do |node|
      node['title']
    end
  end
end
