# frozen_string_literal: true

RSpec.describe Dry::Struct, method: '.attribute' do
  let(:user_type) { Test::User }

  before do
    module Test
      class Role < Dry::Struct
        attribute :id, 'strict.integer'
        attribute :name, 'strict.string'
      end

      class User < Dry::Struct
      end
    end
  end

  context 'when given a block-style nested type' do
    context 'when the nested type is not already defined' do
      context 'with no superclass type' do
        before do
          module Test
            User.attribute(:permissions, Dry::Types['strict.array']) do
              attribute :id, 'strict.integer'
              attribute :name, 'strict.string'
            end
          end
        end

        it 'defines attributes for the constructor' do
          user = user_type[
            permissions: [{ id: 1, name: 'all' }, { id: 2, name: 'edit_users' }]
          ]

          expect(user.permissions.length).to be(2)
          expect(user.permissions[0].id).to be(1)
          expect(user.permissions[0].name).to eql('all')
          expect(user.permissions[1].id).to be(2)
          expect(user.permissions[1].name).to eql('edit_users')
        end

        it 'defines a nested type' do
          expect { user_type.const_get('Permission') }.to_not raise_error
        end
      end

      context 'with a superclass type' do
        %w(array strict.array coercible.array).each do |array_type|
          context "using #{ array_type }" do
            before do
              module Test
                class BasePermission < Dry::Struct
                  attribute :id, 'strict.integer'
                end
              end

              Test::User.attribute(:permissions, Dry::Types[array_type].of(Test:: BasePermission)) do
                attribute :name, 'strict.string'
              end
            end

            it 'uses the given array type' do
              expect(user_type.schema.key(:permissions).type).
                to eql(Dry::Types[array_type].of(Test::User::Permission))
            end

            it 'defines attributes for the constructor' do
              user = user_type[
                permissions: [{ id: 1, name: 'all' }, { id: 2, name: 'edit_users' }]
              ]

              expect(user.permissions.length).to be(2)
              expect(user.permissions[0].id).to be(1)
              expect(user.permissions[0].name).to eql('all')
              expect(user.permissions[1].id).to be(2)
              expect(user.permissions[1].name).to eql('edit_users')
            end

            it 'defines a nested type' do
              expect { user_type.const_get('Permission') }.to_not raise_error
            end
          end
        end
      end

      context 'with a named type' do
        before do
          module Test
            User.attribute(:permissions, 'strict.array') do
              attribute :id, 'strict.integer'
              attribute :name, 'strict.string'
            end
          end
        end

        it 'uses the given array type' do
          expect(user_type.schema.key(:permissions).type).
            to eql(Dry::Types['strict.array'].of(Test::User::Permission))
        end
      end
    end

    context 'when the nested type is already defined' do
      before do
        module Test
          class User < Dry::Struct
            class Role < Dry::Struct
            end
          end
        end
      end

      it 'raises a Dry::Struct::Error' do
        expect {
          Test::User.attribute(:roles, Dry::Types['strict.array']) {}
        }.to raise_error(Dry::Struct::Error)
      end
    end
  end


  context 'when no nested attribute block given' do
    it 'raises error when type is missing' do
      expect {
        class Test::Foo < Dry::Struct
          attribute :bar
        end
      }.to raise_error(ArgumentError)
    end
  end
end
