# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Query.project(fullPath).releases()' do
  include GraphqlHelpers

  let_it_be(:stranger) { create(:user) }
  let_it_be(:guest) { create(:user) }
  let_it_be(:reporter) { create(:user) }
  let_it_be(:developer) { create(:user) }

  let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
  let(:opened_url_params) { { state: 'opened', **base_url_params } }
  let(:merged_url_params) { { state: 'merged', **base_url_params } }
  let(:closed_url_params) { { state: 'closed', **base_url_params } }

  let(:query) do
    graphql_query_for(:project, { fullPath: project.full_path },
    %{
      releases {
        count
        nodes {
          tagName
          tagPath
          name
          commit {
            sha
          }
          assets {
            count
            sources {
              nodes {
                url
              }
            }
          }
          evidences {
            nodes {
              sha
            }
          }
          links {
            selfUrl
            openedMergeRequestsUrl
            mergedMergeRequestsUrl
            closedMergeRequestsUrl
            openedIssuesUrl
            closedIssuesUrl
          }
        }
      }
    })
  end

  let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
  let(:post_query) { post_graphql(query, current_user: current_user) }

  let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) }

  before do
    stub_default_url_options(host: 'www.example.com')
  end

  shared_examples 'correct total count' do
    let(:data) { graphql_data.dig('project', 'releases') }

    before do
      create_list(:release, 2, project: project)

      post_query
    end

    it 'returns the total count' do
      expect(data['count']).to eq(project.releases.count)
    end
  end

  shared_examples 'full access to all repository-related fields' do
    describe 'repository-related fields' do
      before do
        post_query
      end

      it 'returns data for fields that are protected in private projects' do
        expected_sources = release.sources.map do |s|
          { 'url' => s.url }
        end

        expected_evidences = release.evidences.map do |e|
          { 'sha' => e.sha }
        end

        expect(data).to eq(
          'tagName' => release.tag,
          'tagPath' => project_tag_path(project, release.tag),
          'name' => release.name,
          'commit' => {
            'sha' => release.commit.sha
          },
          'assets' => {
            'count' => release.assets_count,
            'sources' => {
              'nodes' => expected_sources
            }
          },
          'evidences' => {
            'nodes' => expected_evidences
          },
          'links' => {
            'selfUrl' => project_release_url(project, release),
            'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
            'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
            'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
            'openedIssuesUrl' => project_issues_url(project, opened_url_params),
            'closedIssuesUrl' => project_issues_url(project, closed_url_params)
          }
        )
      end
    end

    it_behaves_like 'correct total count'
  end

  shared_examples 'no access to any repository-related fields' do
    describe 'repository-related fields' do
      before do
        post_query
      end

      it 'does not return data for fields that expose repository information' do
        tag_name = release.tag
        release_name = release.name
        expect(data).to eq(
          'tagName' => tag_name,
          'tagPath' => nil,
          'name' => release_name,
          'commit' => nil,
          'assets' => {
            'count' => release.assets_count(except: [:sources]),
            'sources' => {
              'nodes' => []
            }
          },
          'evidences' => {
            'nodes' => []
          },
          'links' => {
            'closedIssuesUrl' => nil,
            'closedMergeRequestsUrl' => nil,
            'mergedMergeRequestsUrl' => nil,
            'openedIssuesUrl' => nil,
            'openedMergeRequestsUrl' => nil,
            'selfUrl' => project_release_url(project, release)
          }
        )
      end
    end

    it_behaves_like 'correct total count'
  end

  # editUrl is tested separately becuase its permissions
  # are slightly different than other release fields
  shared_examples 'access to editUrl' do
    let(:query) do
      graphql_query_for(:project, { fullPath: project.full_path },
        %{
          releases {
            nodes {
              links {
                editUrl
              }
            }
          }
        })
    end

    before do
      post_query
    end

    it 'returns editUrl' do
      expect(data).to eq(
        'links' => {
          'editUrl' => edit_project_release_url(project, release)
        }
      )
    end
  end

  shared_examples 'no access to editUrl' do
    let(:query) do
      graphql_query_for(:project, { fullPath: project.full_path },
        %{
          releases {
            nodes {
              links {
                editUrl
              }
            }
          }
        })
    end

    before do
      post_query
    end

    it 'does not return editUrl' do
      expect(data).to eq(
        'links' => {
          'editUrl' => nil
        }
      )
    end
  end

  shared_examples 'no access to any release data' do
    before do
      post_query
    end

    it 'returns nil' do
      expect(data).to eq(nil)
    end
  end

  describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do
    context 'when the project is private' do
      let_it_be(:project) { create(:project, :repository, :private) }
      let_it_be(:release) { create(:release, :with_evidence, project: project) }

      before_all do
        project.add_guest(guest)
        project.add_reporter(reporter)
        project.add_developer(developer)
      end

      context 'when the user is not logged in' do
        let(:current_user) { stranger }

        it_behaves_like 'no access to any release data'
      end

      context 'when the user has Guest permissions' do
        let(:current_user) { guest }

        it_behaves_like 'no access to any repository-related fields'
      end

      context 'when the user has Reporter permissions' do
        let(:current_user) { reporter }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'no access to editUrl'
      end

      context 'when the user has Developer permissions' do
        let(:current_user) { developer }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'access to editUrl'
      end
    end

    context 'when the project is public' do
      let_it_be(:project) { create(:project, :repository, :public) }
      let_it_be(:release) { create(:release, :with_evidence, project: project) }

      before_all do
        project.add_guest(guest)
        project.add_reporter(reporter)
        project.add_developer(developer)
      end

      context 'when the user is not logged in' do
        let(:current_user) { stranger }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'no access to editUrl'
      end

      context 'when the user has Guest permissions' do
        let(:current_user) { guest }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'no access to editUrl'
      end

      context 'when the user has Reporter permissions' do
        let(:current_user) { reporter }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'no access to editUrl'
      end

      context 'when the user has Developer permissions' do
        let(:current_user) { developer }

        it_behaves_like 'full access to all repository-related fields'
        it_behaves_like 'access to editUrl'
      end
    end
  end

  describe 'sorting and pagination' do
    let_it_be(:sort_project) { create(:project, :public) }

    let(:data_path)          { [:project, :releases] }
    let(:current_user)       { developer }

    def pagination_query(params)
      graphql_query_for(
        :project,
        { full_path: sort_project.full_path },
        query_graphql_field(:releases, params, "#{page_info} nodes { tagName }")
      )
    end

    def pagination_results_data(nodes)
      nodes.map { |release| release['tagName'] }
    end

    context 'when sorting by released_at' do
      let_it_be(:release5) { create(:release, project: sort_project, tag: 'v5.5.0', released_at: 3.days.from_now) }
      let_it_be(:release1) { create(:release, project: sort_project, tag: 'v5.1.0', released_at: 3.days.ago) }
      let_it_be(:release4) { create(:release, project: sort_project, tag: 'v5.4.0', released_at: 2.days.from_now) }
      let_it_be(:release2) { create(:release, project: sort_project, tag: 'v5.2.0', released_at: 2.days.ago) }
      let_it_be(:release3) { create(:release, project: sort_project, tag: 'v5.3.0', released_at: 1.day.ago) }

      context 'when ascending' do
        it_behaves_like 'sorted paginated query' do
          let(:sort_param) { :RELEASED_AT_ASC }
          let(:first_param) { 2 }
          let(:all_records) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] }
        end
      end

      context 'when descending' do
        it_behaves_like 'sorted paginated query' do
          let(:sort_param) { :RELEASED_AT_DESC }
          let(:first_param) { 2 }
          let(:all_records) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] }
        end
      end
    end

    context 'when sorting by created_at' do
      let_it_be(:release5) { create(:release, project: sort_project, tag: 'v5.5.0', created_at: 3.days.from_now) }
      let_it_be(:release1) { create(:release, project: sort_project, tag: 'v5.1.0', created_at: 3.days.ago) }
      let_it_be(:release4) { create(:release, project: sort_project, tag: 'v5.4.0', created_at: 2.days.from_now) }
      let_it_be(:release2) { create(:release, project: sort_project, tag: 'v5.2.0', created_at: 2.days.ago) }
      let_it_be(:release3) { create(:release, project: sort_project, tag: 'v5.3.0', created_at: 1.day.ago) }

      context 'when ascending' do
        it_behaves_like 'sorted paginated query' do
          let(:sort_param) { :CREATED_ASC }
          let(:first_param) { 2 }
          let(:all_records) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] }
        end
      end

      context 'when descending' do
        it_behaves_like 'sorted paginated query' do
          let(:sort_param) { :CREATED_DESC }
          let(:first_param) { 2 }
          let(:all_records) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] }
        end
      end
    end
  end
end