# frozen_string_literal: true

require_relative '../../../../tooling/graphql/docs/renderer'

RSpec.describe Tooling::Graphql::Docs::Renderer do
  describe '#contents' do
    shared_examples 'renders correctly as GraphQL documentation' do
      it 'contains the expected section' do
        # duplicative - but much better error messages!
        section.lines.each { |line| expect(contents).to include(line) }
        expect(contents).to include(section)
      end
    end

    let(:template) { Rails.root.join('tooling/graphql/docs/templates/default.md.haml') }
    let(:field_description) { 'List of objects.' }
    let(:type) { ::GraphQL::Types::Int }

    let(:query_type) do
      Class.new(Types::BaseObject) { graphql_name 'Query' }.tap do |t|
        # this keeps type and field_description in scope.
        t.field :foo, type, null: true, description: field_description do
          argument :id, GraphQL::Types::ID, required: false, description: 'ID of the object.'
        end
      end
    end

    let(:mutation_root) do
      Class.new(::Types::BaseObject) do
        include ::Gitlab::Graphql::MountMutation
        graphql_name 'Mutation'
      end
    end

    let(:mock_schema) do
      Class.new(GraphQL::Schema) do
        def resolve_type(obj, ctx)
          raise 'Not a real schema'
        end
      end
    end

    subject(:contents) do
      mock_schema.query(query_type)
      mock_schema.mutation(mutation_root) if mutation_root.fields.any?

      described_class.new(
        mock_schema,
        output_dir: nil,
        template: template
      ).contents
    end

    describe 'headings' do
      it 'contains the expected sections' do
        expect(contents.lines.map(&:chomp)).to include(
          '## `Query` type',
          '## `Mutation` type',
          '## Connections',
          '## Object types',
          '## Enumeration types',
          '## Scalar types',
          '## Abstract types',
          '### Unions',
          '### Interfaces',
          '## Input types'
        )
      end
    end

    context 'when a field has a list type' do
      let(:type) do
        Class.new(Types::BaseObject) do
          graphql_name 'ArrayTest'

          field :foo, [GraphQL::Types::String], null: false, description: 'A description.'
        end
      end

      specify do
        type_name = '[String!]!'
        inner_type = 'string'
        expectation = <<~DOC
          ### `ArrayTest`

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="arraytestfoo"></a>`foo` | [`#{type_name}`](##{inner_type}) | A description. |
        DOC

        is_expected.to include(expectation)
      end

      describe 'a top level query field' do
        let(:expectation) do
          <<~DOC
            ### `Query.foo`

            List of objects.

            Returns [`ArrayTest`](#arraytest).

            #### Arguments

            | Name | Type | Description |
            | ---- | ---- | ----------- |
            | <a id="queryfooid"></a>`id` | [`ID`](#id) | ID of the object. |
          DOC
        end

        it 'generates the query with arguments' do
          expect(subject).to include(expectation)
        end

        context 'when description does not end with `.`' do
          let(:field_description) { 'List of objects' }

          it 'adds the `.` to the end' do
            expect(subject).to include(expectation)
          end
        end
      end
    end

    describe 'when fields are not defined in alphabetical order' do
      let(:type) do
        Class.new(Types::BaseObject) do
          graphql_name 'OrderingTest'

          field :foo, GraphQL::Types::String, null: false, description: 'A description of foo field.'
          field :bar, GraphQL::Types::String, null: false, description: 'A description of bar field.'
        end
      end

      it 'lists the fields in alphabetical order' do
        expectation = <<~DOC
          ### `OrderingTest`

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="orderingtestbar"></a>`bar` | [`String!`](#string) | A description of bar field. |
          | <a id="orderingtestfoo"></a>`foo` | [`String!`](#string) | A description of foo field. |
        DOC

        is_expected.to include(expectation)
      end
    end

    context 'when a field has a documentation reference' do
      let(:type) do
        wibble = Class.new(::Types::BaseObject) do
          graphql_name 'Wibble'
          field :x, ::GraphQL::Types::Int, null: false
        end

        Class.new(Types::BaseObject) do
          graphql_name 'DocRefSpec'
          description 'Testing doc refs'

          field :foo,
                type: GraphQL::Types::String,
                null: false,
                description: 'The foo.',
                see: { 'A list of foos' => 'https://example.com/foos' }
          field :bar,
                type: GraphQL::Types::String,
                null: false,
                description: 'The bar.',
                see: { 'A list of bars' => 'https://example.com/bars' } do
                  argument :barity, ::GraphQL::Types::Int, required: false, description: '?'
                end
          field :wibbles,
                type: wibble.connection_type,
                null: true,
                description: 'The wibbles',
                see: { 'wibblance' => 'https://example.com/wibbles' }
        end
      end

      let(:section) do
        <<~DOC
          ### `DocRefSpec`

          Testing doc refs.

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="docrefspecfoo"></a>`foo` | [`String!`](#string) | The foo. See [A list of foos](https://example.com/foos). |
          | <a id="docrefspecwibbles"></a>`wibbles` | [`WibbleConnection`](#wibbleconnection) | The wibbles. See [wibblance](https://example.com/wibbles). (see [Connections](#connections)) |

          #### Fields with arguments

          ##### `DocRefSpec.bar`

          The bar. See [A list of bars](https://example.com/bars).

          Returns [`String!`](#string).

          ###### Arguments

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="docrefspecbarbarity"></a>`barity` | [`Int`](#int) | ?. |
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when an argument is deprecated' do
      let(:type) do
        Class.new(Types::BaseObject) do
          graphql_name 'DeprecatedTest'
          description 'A thing we used to use, but no longer support'

          field :foo,
                type: GraphQL::Types::String,
                null: false,
                description: 'A description.' do
                  argument :foo_arg, GraphQL::Types::String,
                           required: false,
                           description: 'The argument.',
                           deprecated: { reason: 'Bad argument', milestone: '101.2' }
                end
        end
      end

      let(:section) do
        <<~DOC
         ##### `DeprecatedTest.foo`

         A description.

         Returns [`String!`](#string).

         ###### Arguments

         | Name | Type | Description |
         | ---- | ---- | ----------- |
         | <a id="deprecatedtestfoofooarg"></a>`fooArg` **{warning-solid}** | [`String`](#string) | **Deprecated** in 101.2. Bad argument. |
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when a field is deprecated' do
      let(:type) do
        Class.new(Types::BaseObject) do
          graphql_name 'DeprecatedTest'
          description 'A thing we used to use, but no longer support'

          field :foo,
                type: GraphQL::Types::String,
                null: false,
                deprecated: { reason: 'This is deprecated', milestone: '1.10' },
                description: 'A description.'
          field :foo_with_args,
                type: GraphQL::Types::String,
                null: false,
                deprecated: { reason: 'Do not use', milestone: '1.10', replacement: 'X.y' },
                description: 'A description.' do
                  argument :arg, GraphQL::Types::Int, required: false, description: 'Argity'
                end
          field :bar,
                type: GraphQL::Types::String,
                null: false,
                description: 'A description.',
                deprecated: {
                  reason: :renamed,
                  milestone: '1.10',
                  replacement: 'Query.boom'
                }
        end
      end

      let(:section) do
        <<~DOC
          ### `DeprecatedTest`

          A thing we used to use, but no longer support.

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="deprecatedtestbar"></a>`bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: [`Query.boom`](#queryboom). |
          | <a id="deprecatedtestfoo"></a>`foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. |

          #### Fields with arguments

          ##### `DeprecatedTest.fooWithArgs`

          A description.

          WARNING:
          **Deprecated** in 1.10.
          Do not use.
          Use: [`X.y`](#xy).

          Returns [`String!`](#string).

          ###### Arguments

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="deprecatedtestfoowithargsarg"></a>`arg` | [`Int`](#int) | Argity. |
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when a Query.field is deprecated' do
      before do
        query_type.field(
          name: :bar,
          type: type,
          null: true,
          description: 'A bar',
          deprecated: { reason: :renamed, milestone: '10.11', replacement: 'Query.foo' }
        )
      end

      let(:type) { ::GraphQL::Types::Int }
      let(:section) do
        <<~DOC
          ### `Query.bar`

          A bar.

          WARNING:
          **Deprecated** in 10.11.
          This was renamed.
          Use: [`Query.foo`](#queryfoo).

          Returns [`Int`](#int).
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when a field has an Enumeration type' do
      let(:type) do
        enum_type = Class.new(Types::BaseEnum) do
          graphql_name 'MyEnum'
          description 'A test of an enum.'

          value 'BAZ',
                description: 'A description of BAZ.'
          value 'BAR',
                description: 'A description of BAR.',
                deprecated: { reason: 'This is deprecated', milestone: '1.10' }
          value 'BOOP',
                description: 'A description of BOOP.',
                deprecated: { reason: :renamed, replacement: 'MyEnum.BAR', milestone: '1.10' }
        end

        Class.new(Types::BaseObject) do
          graphql_name 'EnumTest'

          field :foo, enum_type, null: false, description: 'A description of foo field.'
        end
      end

      let(:section) do
        <<~DOC
          ### `MyEnum`

          A test of an enum.

          | Value | Description |
          | ----- | ----------- |
          | <a id="myenumbar"></a>`BAR` **{warning-solid}** | **Deprecated** in 1.10. This is deprecated. |
          | <a id="myenumbaz"></a>`BAZ` | A description of BAZ. |
          | <a id="myenumboop"></a>`BOOP` **{warning-solid}** | **Deprecated** in 1.10. This was renamed. Use: [`MyEnum.BAR`](#myenumbar). |
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when a field has a global ID type' do
      let(:type) do
        Class.new(Types::BaseObject) do
          graphql_name 'IDTest'
          description 'A test for rendering IDs.'

          field :foo, ::Types::GlobalIDType[::User], null: true, description: 'A user foo.'
        end
      end

      describe 'section for IDTest' do
        let(:section) do
          <<~DOC
            ### `IDTest`

            A test for rendering IDs.

            #### Fields

            | Name | Type | Description |
            | ---- | ---- | ----------- |
            | <a id="idtestfoo"></a>`foo` | [`UserID`](#userid) | A user foo. |
          DOC
        end

        it_behaves_like 'renders correctly as GraphQL documentation'
      end

      describe 'section for UserID' do
        let(:section) do
          <<~DOC
            ### `UserID`

            A `UserID` is a global ID. It is encoded as a string.

            An example `UserID` is: `"gid://gitlab/User/1"`.
          DOC
        end

        it_behaves_like 'renders correctly as GraphQL documentation'
      end
    end

    context 'when there is a mutation' do
      let(:mutation) do
        mutation = Class.new(::Mutations::BaseMutation)

        mutation.graphql_name 'MakeItPretty'
        mutation.description 'Make everything very pretty.'

        mutation.argument :prettiness_factor,
                          type: GraphQL::FLOAT_TYPE,
                          required: true,
                          description: 'How much prettier?'

        mutation.argument :pulchritude,
                          type: GraphQL::FLOAT_TYPE,
                          required: false,
                          description: 'How much prettier?',
                          deprecated: {
                            reason: :renamed,
                            replacement: 'prettinessFactor',
                            milestone: '72.34'
                          }

        mutation.field :everything,
                       type: GraphQL::Types::String,
                       null: true,
                       description: 'What we made prettier.'

        mutation.field :omnis,
                       type: GraphQL::Types::String,
                       null: true,
                       description: 'What we made prettier.',
                       deprecated: {
                         reason: :renamed,
                         replacement: 'everything',
                         milestone: '72.34'
                       }

        mutation
      end

      before do
        mutation_root.mount_mutation mutation
      end

      it_behaves_like 'renders correctly as GraphQL documentation' do
        let(:section) do
          <<~DOC
            ### `Mutation.makeItPretty`

            Make everything very pretty.

            Input type: `MakeItPrettyInput`

            #### Arguments

            | Name | Type | Description |
            | ---- | ---- | ----------- |
            | <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
            | <a id="mutationmakeitprettyprettinessfactor"></a>`prettinessFactor` | [`Float!`](#float) | How much prettier?. |
            | <a id="mutationmakeitprettypulchritude"></a>`pulchritude` **{warning-solid}** | [`Float`](#float) | **Deprecated:** This was renamed. Please use `prettinessFactor`. Deprecated in 72.34. |

            #### Fields

            | Name | Type | Description |
            | ---- | ---- | ----------- |
            | <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
            | <a id="mutationmakeitprettyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
            | <a id="mutationmakeitprettyeverything"></a>`everything` | [`String`](#string) | What we made prettier. |
            | <a id="mutationmakeitprettyomnis"></a>`omnis` **{warning-solid}** | [`String`](#string) | **Deprecated:** This was renamed. Please use `everything`. Deprecated in 72.34. |
          DOC
        end
      end

      it 'does not render the automatically generated payload type' do
        expect(contents).not_to include('MakeItPrettyPayload')
      end

      it 'does not render the automatically generated input type as its own section' do
        expect(contents).not_to include('# `MakeItPrettyInput`')
      end
    end

    context 'when there is an input type' do
      let(:type) do
        Class.new(::Types::BaseObject) do
          graphql_name 'Foo'
          field :wibble, type: ::GraphQL::Types::Int, null: true do
            argument :date_range,
                     type: ::Types::TimeframeInputType,
                     required: true,
                     description: 'When the foo happened.'
          end
        end
      end

      let(:section) do
        <<~DOC
          ### `Timeframe`

          A time-frame defined as a closed inclusive range of two dates.

          #### Arguments

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="timeframeend"></a>`end` | [`Date!`](#date) | The end of the range. |
          | <a id="timeframestart"></a>`start` | [`Date!`](#date) | The start of the range. |
        DOC
      end

      it_behaves_like 'renders correctly as GraphQL documentation'
    end

    context 'when there is an interface and a union' do
      let(:type) do
        user = Class.new(::Types::BaseObject)
        user.graphql_name 'User'
        user.field :user_field, ::GraphQL::Types::String, null: true
        group = Class.new(::Types::BaseObject)
        group.graphql_name 'Group'
        group.field :group_field, ::GraphQL::Types::String, null: true

        union = Class.new(::Types::BaseUnion)
        union.graphql_name 'UserOrGroup'
        union.description 'Either a user or a group.'
        union.possible_types user, group

        interface = Module.new
        interface.include(::Types::BaseInterface)
        interface.graphql_name 'Flying'
        interface.description 'Something that can fly.'
        interface.field :flight_speed, GraphQL::Types::Int, null: true, description: 'Speed in mph.'

        african_swallow = Class.new(::Types::BaseObject)
        african_swallow.graphql_name 'AfricanSwallow'
        african_swallow.description 'A swallow from Africa.'
        african_swallow.implements interface
        interface.orphan_types african_swallow

        Class.new(::Types::BaseObject) do
          graphql_name 'AbstractTypeTest'
          description 'A test for abstract types.'

          field :foo, union, null: true, description: 'The foo.'
          field :flying, interface, null: true, description: 'A flying thing.'
        end
      end

      it 'lists the fields correctly, and includes descriptions of all the types' do
        type_section = <<~DOC
          ### `AbstractTypeTest`

          A test for abstract types.

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="abstracttypetestflying"></a>`flying` | [`Flying`](#flying) | A flying thing. |
          | <a id="abstracttypetestfoo"></a>`foo` | [`UserOrGroup`](#userorgroup) | The foo. |
        DOC

        union_section = <<~DOC
          #### `UserOrGroup`

          Either a user or a group.

          One of:

          - [`Group`](#group)
          - [`User`](#user)
        DOC

        interface_section = <<~DOC
          #### `Flying`

          Something that can fly.

          Implementations:

          - [`AfricanSwallow`](#africanswallow)

          ##### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="flyingflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
        DOC

        implementation_section = <<~DOC
          ### `AfricanSwallow`

          A swallow from Africa.

          #### Fields

          | Name | Type | Description |
          | ---- | ---- | ----------- |
          | <a id="africanswallowflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
        DOC

        is_expected.to include(
          type_section,
          union_section,
          interface_section,
          implementation_section
        )
      end
    end
  end
end
