require 'active_model/errors'
require 'active_model/naming'
require 'active_model/translation'
require 'pry'

describe JSONAPI::Serializer do
  def serialize_primary(object, options = {})
    # Note: intentional high-coupling to protected method for tests.
    JSONAPI::Serializer.send(:serialize_primary, object, options)
  end

  describe 'internal-only serialize_primary' do
    it 'serializes nil to nil' do
      # Spec: Primary data MUST be either:
      # - a single resource object or null, for requests that target single resources
      # http://jsonapi.org/format/#document-structure-top-level
      primary_data = serialize_primary(nil, {serializer: MyApp::PostSerializer})
      expect(primary_data).to be_nil
    end

    it 'can serialize primary data for a simple object' do
      post = create(:post)
      primary_data = serialize_primary(post, {serializer: MyApp::SimplestPostSerializer})
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'attributes' => {
          'title' => 'Title for Post 1',
          'long-content' => 'Body for Post 1',
        },
        'links' => {
          'self' => '/posts/1',
        },
      })
    end

    it 'can serialize primary data for a simple object with a long name' do
      long_comment = create(:long_comment, post: create(:post))
      primary_data = serialize_primary(long_comment, {serializer: MyApp::LongCommentSerializer})
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'long-comments',
        'attributes' => {
          'body' => 'Body for LongComment 1',
        },
        'links' => {
          'self' => '/long-comments/1',
        },
        'relationships' => {
          'user' => {
            'links' => {
              'self' => '/long-comments/1/relationships/user',
              'related' => '/long-comments/1/user',
            },
          },
          'post' => {
            'links' => {
              'self' => '/long-comments/1/relationships/post',
              'related' => '/long-comments/1/post',
            },
          },
        },
      })
    end

    it 'can serialize primary data for a simple object with resource-level metadata' do
      post = create(:post)
      primary_data = serialize_primary(post, {serializer: MyApp::PostSerializerWithMetadata})
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'attributes' => {
          'title' => 'Title for Post 1',
          'long-content' => 'Body for Post 1',
        },
        'links' => {
          'self' => '/posts/1',
        },
        'meta' => {
          'copyright' => 'Copyright 2015 Example Corp.',
          'authors' => [
            'Aliens',
          ],
        },
      })
    end

    context 'without any linkage includes (default)' do
      it 'can serialize primary data for an object with to-one and to-many relationships' do
        post = create(:post)
        primary_data = serialize_primary(post, {serializer: MyApp::PostSerializer})
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          },
          'relationships' => {
            # Both to-one and to-many links are present, but neither include linkage:
            'author' => {
              'links' => {
                'self' => '/posts/1/relationships/author',
                'related' => '/posts/1/author',
              },
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/relationships/long-comments',
                'related' => '/posts/1/long-comments',
              },
            },
          },
        })
      end

      it 'does not include relationship links if relationship_{self_link,_related_link} are nil' do
        post = create(:post)
        primary_data = serialize_primary(post, {serializer: MyApp::PostSerializerWithoutLinks})
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          # This is technically invalid since relationships MUST contain at least one of links,
          # data, or meta, but we leave that up to the user.
          'relationships' => {
            'author' => {},
            'long-comments' => {},
          },
        })
      end

      it 'does not include id when it is nil' do
        post = create(:post)
        post.id = nil
        primary_data = serialize_primary(post, {serializer: MyApp::PostSerializerWithoutLinks})
        expect(primary_data).to eq({
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'relationships' => {
            'author' => {},
            'long-comments' => {},
          },
        })
      end

      it 'serializes object when multiple attributes are declared once' do
        post = create(:post)
        primary_data = serialize_primary(post, {serializer: MyApp::MultipleAttributesSerializer})
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'body' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          }
        })
      end
    end

    context 'with linkage includes' do
      it 'can serialize primary data for a null to-one relationship' do
        post = create(:post, author: nil)
        options = {
          serializer: MyApp::PostSerializer,
          include_linkages: ['author', 'long-comments'],
        }
        primary_data = serialize_primary(post, options)
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          },
          'relationships' => {
            'author' => {
              'links' => {
                'self' => '/posts/1/relationships/author',
                'related' => '/posts/1/author',
              },
              # Spec: Resource linkage MUST be represented as one of the following:
              # - null for empty to-one relationships.
              # http://jsonapi.org/format/#document-structure-resource-relationships
              'data' => nil,
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/relationships/long-comments',
                'related' => '/posts/1/long-comments',
              },
              'data' => [],
            },
          },
        })
      end

      it 'can serialize primary data for a simple to-one relationship' do
        post = create(:post, :with_author)
        options = {
          serializer: MyApp::PostSerializer,
          include_linkages: ['author', 'long-comments'],
        }
        primary_data = serialize_primary(post, options)
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          },
          'relationships' => {
            'author' => {
              'links' => {
                'self' => '/posts/1/relationships/author',
                'related' => '/posts/1/author',
              },
              # Spec: Resource linkage MUST be represented as one of the following:
              # - a 'linkage object' (defined below) for non-empty to-one relationships.
              # http://jsonapi.org/format/#document-structure-resource-relationships
              'data' => {
                'type' => 'users',
                'id' => '1',
              },
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/relationships/long-comments',
                'related' => '/posts/1/long-comments',
              },
              'data' => [],
            },
          },
        })
      end

      it 'can serialize primary data for an empty to-many relationship' do
        post = create(:post, long_comments: [])
        options = {
          serializer: MyApp::PostSerializer,
          include_linkages: ['author', 'long-comments'],
        }
        primary_data = serialize_primary(post, options)
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          },
          'relationships' => {
            'author' => {
              'links' => {
                'self' => '/posts/1/relationships/author',
                'related' => '/posts/1/author',
              },
              'data' => nil,
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/relationships/long-comments',
                'related' => '/posts/1/long-comments',
              },
              # Spec: Resource linkage MUST be represented as one of the following:
              # - an empty array ([]) for empty to-many relationships.
              # http://jsonapi.org/format/#document-structure-resource-relationships
              'data' => [],
            },
          },
        })
      end

      it 'can serialize primary data for a simple to-many relationship' do
        long_comments = create_list(:long_comment, 2)
        post = create(:post, long_comments: long_comments)
        options = {
          serializer: MyApp::PostSerializer,
          include_linkages: ['author', 'long-comments'],
        }
        primary_data = serialize_primary(post, options)
        expect(primary_data).to eq({
          'id' => '1',
          'type' => 'posts',
          'attributes' => {
            'title' => 'Title for Post 1',
            'long-content' => 'Body for Post 1',
          },
          'links' => {
            'self' => '/posts/1',
          },
          'relationships' => {
            'author' => {
              'links' => {
                'self' => '/posts/1/relationships/author',
                'related' => '/posts/1/author',
              },
              'data' => nil,
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/relationships/long-comments',
                'related' => '/posts/1/long-comments',
              },
              # Spec: Resource linkage MUST be represented as one of the following:
              # - an array of linkage objects for non-empty to-many relationships.
              # http://jsonapi.org/format/#document-structure-resource-relationships
              'data' => [
                {
                  'type' => 'long-comments',
                  'id' => '1',
                },
                {
                  'type' => 'long-comments',
                  'id' => '2',
                },
              ],
            },
          },
        })
      end
    end

    it 'can serialize primary data for an empty serializer with no attributes' do
      post = create(:post)
      primary_data = serialize_primary(post, {serializer: MyApp::EmptySerializer})
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'links' => {
          'self' => '/posts/1',
        },
      })
    end

    it 'can find the correct serializer by object class name' do
      post = create(:post)
      primary_data = serialize_primary(post)
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'attributes' => {
          'title' => 'Title for Post 1',
          'long-content' => 'Body for Post 1',
        },
        'links' => {
          'self' => '/posts/1',
        },
        'relationships' => {
          'author' => {
            'links' => {
              'self' => '/posts/1/relationships/author',
              'related' => '/posts/1/author',
            },
          },
          'long-comments' => {
            'links' => {
              'self' => '/posts/1/relationships/long-comments',
              'related' => '/posts/1/long-comments',
            },
          },
        },
      })
    end

    it 'allows to exclude linkages for relationships' do
      long_comments = create_list(:long_comment, 2)
      post = create(:post, :with_author, long_comments: long_comments)
      primary_data = serialize_primary(post, { serializer: MyApp::PostSerializerWithoutIncludeLinks })
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'attributes' => {
          'title' => 'Title for Post 1'
        },
        'links' => {
          'self' => '/posts/1',
        },
        'relationships' => {
          'author' => {
          },
          'long-comments' => {
          },
        }
      })
    end

    it 'allows to include data for relationships' do
      long_comments = create_list(:long_comment, 2)
      post = create(:post, :with_author, long_comments: long_comments)
      primary_data = serialize_primary(post, { serializer: MyApp::PostSerializerWithIncludeData })
      expect(primary_data).to eq({
        'id' => '1',
        'type' => 'posts',
        'attributes' => {
          'title' => 'Title for Post 1'
        },
        'links' => {
          'self' => '/posts/1',
        },
        'relationships' => {
          'author' => {
            'links' => {
              'self' => '/posts/1/relationships/author',
              'related' => '/posts/1/author'
            },
            'data' => {
              'type' => 'users',
              'id' => '1'
            }
          },
          'long-comments' => {
            'links' => {
              'self' => '/posts/1/relationships/long-comments',
              'related' => '/posts/1/long-comments'
            },
            'data' => [
              {
                'type' => 'long-comments',
                'id' => '1'
              },
              {
                'type' => 'long-comments',
                'id' => '2'
              }
            ]
          },
        }
      })
    end
  end

  # The members data and errors MUST NOT coexist in the same document.
  describe 'JSONAPI::Serializer.serialize_errors' do
    it 'can include a top level errors node' do
      errors = [
        {
          'source' => {'pointer' => '/data/attributes/first-name'},
          'title' => 'Invalid Attribute',
          'detail' => 'First name must contain at least three characters.'
        },
        {
          'source' => {'pointer' => '/data/attributes/first-name'},
          'title' => 'Invalid Attribute',
          'detail' => 'First name must contain an emoji.'
        }
      ]
      expect(JSONAPI::Serializer.serialize_errors(errors)).to eq({'errors' => errors})
    end

    it 'works for active_record' do
      # DummyUser exists so we can test calling user.errors.to_hash(full_messages: true)
      class DummyUser
        extend ActiveModel::Naming
        extend ActiveModel::Translation

        def initialize
          @errors = ActiveModel::Errors.new(self)
          @errors.add(:email, :invalid, message: 'is invalid')
          @errors.add(:email, :blank, message: "can't be blank")
          @errors.add(:first_name, :blank, message: "can't be blank")
        end

        attr_accessor :first_name, :email
        attr_reader :errors

        def read_attribute_for_validation(attr)
          send(attr)
        end
      end
      user = DummyUser.new
      jsonapi_errors = [
        {
          'source' => {'pointer' => '/data/attributes/email'},
          'detail' => 'Email is invalid'
        },
        {
          'source' => {'pointer' => '/data/attributes/email'},
          'detail' => "Email can't be blank"
        },
        {
          'source' => {'pointer' => '/data/attributes/first-name'},
          'detail' => "First name can't be blank"
        }
      ]
      expect(JSONAPI::Serializer.serialize_errors(user.errors)).to eq({
        'errors' => jsonapi_errors,
      })
    end
  end

  describe 'JSONAPI::Serializer.serialize' do
    # The following tests rely on the fact that serialize_primary has been tested above, so object
    # primary data is not explicitly tested here. If things are broken, look above here first.

    it 'can serialize a nil object' do
      expect(JSONAPI::Serializer.serialize(nil)).to eq({'data' => nil})
    end

    it 'can serialize a nil object with includes' do
      # Also, the include argument is not validated in this case because we don't know the type.
      data = JSONAPI::Serializer.serialize(nil, include: ['fake'])
      expect(data).to eq({'data' => nil, 'included' => []})
    end

    it 'can serialize an empty array' do
      # Also, the include argument is not validated in this case because we don't know the type.
      data = JSONAPI::Serializer.serialize([], is_collection: true, include: ['fake'])
      expect(data).to eq({'data' => [], 'included' => []})
    end

    it 'can serialize a simple object' do
      post = create(:post)
      expect(JSONAPI::Serializer.serialize(post)).to eq({
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    it 'can include a top level jsonapi node' do
      post = create(:post)
      jsonapi_version = {'version' => '1.0'}
      expect(JSONAPI::Serializer.serialize(post, jsonapi: jsonapi_version)).to eq({
        'jsonapi' => {'version' => '1.0'},
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    it 'can include a top level meta node' do
      post = create(:post)
      meta = {authors: ['Yehuda Katz', 'Steve Klabnik'], copyright: 'Copyright 2015 Example Corp.'}
      expect(JSONAPI::Serializer.serialize(post, meta: meta)).to eq({
        'meta' => meta,
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    it 'can include a top level links node' do
      post = create(:post)
      links = {self: 'http://example.com/posts'}
      expect(JSONAPI::Serializer.serialize(post, links: links)).to eq({
        'links' => links,
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    # TODO: remove this code on next major release
    it 'can include a top level errors node - deprecated' do
      post = create(:post)
      errors = [
        {
          "source" => { "pointer" => "/data/attributes/first-name" },
          "title" => "Invalid Attribute",
          "detail" => "First name must contain at least three characters."
        },
        {
          "source" => { "pointer" => "/data/attributes/first-name" },
          "title" => "Invalid Attribute",
          "detail" => "First name must contain an emoji."
        }
      ]
      expect(JSONAPI::Serializer.serialize(post, errors: errors)).to eq({
        'errors' => errors,
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    it 'can serialize a single object with an `each` method by passing skip_collection_check: true' do
      post = create(:post)
      post.define_singleton_method(:each) do
        "defining this just to defeat the duck-type check"
      end

      expect(JSONAPI::Serializer.serialize(post, skip_collection_check: true)).to eq({
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
      })
    end

    it 'can serialize a collection' do
      posts = create_list(:post, 2)
      expect(JSONAPI::Serializer.serialize(posts, is_collection: true)).to eq({
        'data' => [
          serialize_primary(posts.first, {serializer: MyApp::PostSerializer}),
          serialize_primary(posts.last, {serializer: MyApp::PostSerializer}),
        ],
      })
    end

    it 'raises AmbiguousCollectionError if is_collection is not passed' do
      posts = create_list(:post, 2)
      error = JSONAPI::Serializer::AmbiguousCollectionError
      expect { JSONAPI::Serializer.serialize(posts) }.to raise_error(error)
    end

    it 'raises error if include is not named correctly' do
      post = create(:post)
      error = JSONAPI::Serializer::InvalidIncludeError
      expect { JSONAPI::Serializer.serialize(post, include: ['long_comments']) }.to raise_error(error)
    end

    it 'can serialize a nil object when given serializer' do
      options = {serializer: MyApp::PostSerializer}
      expect(JSONAPI::Serializer.serialize(nil, options)).to eq({'data' => nil})
    end

    it 'can serialize an empty array when given serializer' do
      options = {is_collection: true, serializer: MyApp::PostSerializer}
      expect(JSONAPI::Serializer.serialize([], options)).to eq({'data' => []})
    end

    it 'can serialize a simple object when given serializer' do
      post = create(:post)
      options = {serializer: MyApp::SimplestPostSerializer}
      expect(JSONAPI::Serializer.serialize(post, options)).to eq({
        'data' => serialize_primary(post, {serializer: MyApp::SimplestPostSerializer}),
      })
    end

    it 'handles include of nil to-one relationship with compound document' do
      post = create(:post)

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['author'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['author'])).to eq({
        'data' => expected_primary_data,
        'included' => [],
      })
    end

    it 'handles include of simple to-one relationship with compound document' do
      post = create(:post, :with_author)

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['author'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['author'])).to eq({
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(post.author, {serializer: MyAppOtherNamespace::UserSerializer}),
        ],
      })
    end

    it 'handles include of empty to-many relationships with compound document' do
      post = create(:post, :with_author, long_comments: [])

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['long-comments'])).to eq({
        'data' => expected_primary_data,
        'included' => [],
      })
    end

    it 'handles include of to-many relationships with compound document' do
      long_comments = create_list(:long_comment, 2)
      post = create(:post, :with_author, long_comments: long_comments)

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['long-comments'])).to eq({
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(long_comments.first, {serializer: MyApp::LongCommentSerializer}),
          serialize_primary(long_comments.last, {serializer: MyApp::LongCommentSerializer}),
        ],
      })
    end

    it 'only includes one copy of each referenced relationship' do
      long_comment = create(:long_comment)
      long_comments = [long_comment, long_comment]
      post = create(:post, :with_author, long_comments: long_comments)

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['long-comments'])).to eq({
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(long_comment, {serializer: MyApp::LongCommentSerializer}),
        ],
      })
    end

    it 'handles circular-referencing relationships with compound document' do
      long_comments = create_list(:long_comment, 2)
      post = create(:post, :with_author, long_comments: long_comments)

      # Make sure each long-comment has a circular reference back to the post.
      long_comments.each { |c| c.post = post }

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expect(JSONAPI::Serializer.serialize(post, include: ['long-comments'])).to eq({
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(post.long_comments.first, {serializer: MyApp::LongCommentSerializer}),
          serialize_primary(post.long_comments.last, {serializer: MyApp::LongCommentSerializer}),
        ],
      })
    end

    it 'errors if include is not a defined attribute' do
      user = create(:user)
      expect { JSONAPI::Serializer.serialize(user, include: ['fake-attr']) }.to raise_error
    end

    it 'handles recursive loading of relationships' do
      user = create(:user)
      long_comments = create_list(:long_comment, 2, user: user)
      post = create(:post, :with_author, long_comments: long_comments)
      # Make sure each long-comment has a circular reference back to the post.
      long_comments.each { |c| c.post = post }

      expected_data = {
        'data' => serialize_primary(post, {
          serializer: MyApp::PostSerializer,
          include_linkages: ['long-comments']
        }),
        'included' => [
          # Intermediates are included: long-comments, long-comments.post, and long-comments.post.author
          #  http://jsonapi.org/format/#document-structure-compound-documents
          serialize_primary(post.long_comments.first, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['post']
          }),
          serialize_primary(post.long_comments.last, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['post']
          }),
          serialize_primary(post, {
            serializer: MyApp::PostSerializer,
            include_linkages: ['author', 'post.long-comments', ]
          }),
          serialize_primary(post.author, {serializer: MyAppOtherNamespace::UserSerializer})
        ],
      }
      includes = ['long-comments.post.author']
      actual_data = JSONAPI::Serializer.serialize(post, include: includes)
      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end

    it 'handles recursive loading of multiple to-one relationships on children' do
      first_user = create(:user)
      second_user = create(:user)
      first_comment = create(:long_comment, user: first_user)
      second_comment = create(:long_comment, user: second_user)
      long_comments = [first_comment, second_comment]
      post = create(:post, :with_author, long_comments: long_comments)
      # Make sure each long-comment has a circular reference back to the post.
      long_comments.each { |c| c.post = post }

      expected_data = {
        'data' => serialize_primary(post, {
          serializer: MyApp::PostSerializer,
          include_linkages: ['long-comments']
        }),
        'included' => [
          serialize_primary(first_comment, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['user']
          }),
          serialize_primary(second_comment, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['user']
          }),
          serialize_primary(first_user, {serializer: MyAppOtherNamespace::UserSerializer}),
          serialize_primary(second_user, {serializer: MyAppOtherNamespace::UserSerializer}),
        ],
      }

      includes = ['long-comments.user']
      actual_data = JSONAPI::Serializer.serialize(post, include: includes)

      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end

    it 'includes linkage in compounded resources only if the immediate parent was also included' do
      comment_user = create(:user)
      long_comments = [create(:long_comment, user: comment_user)]
      post = create(:post, :with_author, long_comments: long_comments)

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expected_data = {
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(long_comments.first, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['user'],
          }),
          # Note: post.author does not show up here because it was not included.
          serialize_primary(comment_user, {serializer: MyAppOtherNamespace::UserSerializer}),
        ],
      }
      includes = ['long-comments.user']
      actual_data = JSONAPI::Serializer.serialize(post, include: includes)

      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end

    it 'handles recursive loading of to-many relationships with overlapping include paths' do
      user = create(:user)
      long_comments = create_list(:long_comment, 2, user: user)
      post = create(:post, :with_author, long_comments: long_comments)
      # Make sure each long-comment has a circular reference back to the post.
      long_comments.each { |c| c.post = post }

      expected_primary_data = serialize_primary(post, {
        serializer: MyApp::PostSerializer,
        include_linkages: ['long-comments'],
      })
      expected_data = {
        'data' => expected_primary_data,
        'included' => [
          serialize_primary(long_comments.first, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['post'],
          }),
          serialize_primary(long_comments.last, {
            serializer: MyApp::LongCommentSerializer,
            include_linkages: ['post'],
          }),
          serialize_primary(post, {
            serializer: MyApp::PostSerializer,
            include_linkages: ['author'],
          }),
          serialize_primary(post.author, {serializer: MyAppOtherNamespace::UserSerializer}),
        ],
      }
      # Also test that it handles string include arguments.
      includes = 'long-comments,long-comments.post.author'
      actual_data = JSONAPI::Serializer.serialize(post, include: includes)

      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end

    context 'on collection' do
      it 'handles include of has_many relationships with compound document' do
        long_comments = create_list(:long_comment, 2)
        posts = create_list(:post, 2, :with_author, long_comments: long_comments)

        expected_primary_data = JSONAPI::Serializer.send(:serialize_primary_multi, posts, {
          serializer: MyApp::PostSerializer,
          include_linkages: ['long-comments'],
        })
        data = JSONAPI::Serializer.serialize(posts, is_collection: true, include: ['long-comments'])
        expect(data).to eq({
          'data' => expected_primary_data,
          'included' => [
            serialize_primary(long_comments.first, {serializer: MyApp::LongCommentSerializer}),
            serialize_primary(long_comments.last, {serializer: MyApp::LongCommentSerializer}),
          ],
        })
      end
    end

    context 'sparse fieldsets' do
      it 'allows to limit fields(attributes) for serialized resource' do
        first_user = create(:user)
        second_user = create(:user)
        first_comment = create(:long_comment, user: first_user)
        second_comment = create(:long_comment, user: second_user)
        long_comments = [first_comment, second_comment]
        post = create(:post, :with_author, long_comments: long_comments)

        serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: :title})
        expect(serialized_data).to eq ({
          'data' => {
            'type' => 'posts',
            'id' => post.id.to_s,
            'attributes' => {
              'title' => post.title,
            },
            'links' => {
              'self' => '/posts/1'
            }
          }
        })
      end

      it 'allows to limit fields(relationships) for serialized resource' do
        first_user = create(:user)
        second_user = create(:user)
        first_comment = create(:long_comment, user: first_user)
        second_comment = create(:long_comment, user: second_user)
        long_comments = [first_comment, second_comment]
        post = create(:post, :with_author, long_comments: long_comments)

        fields = {posts: 'title,author,long-comments'}
        serialized_data = JSONAPI::Serializer.serialize(post, fields: fields)
        expect(serialized_data['data']['relationships']).to eq ({
          'author' => {
            'links' => {
              'self' => '/posts/1/relationships/author',
              'related' => '/posts/1/author'
            }
          },
          'long-comments' => {
            'links' => {
              'self' => '/posts/1/relationships/long-comments',
              'related' => '/posts/1/long-comments'
            }
          }
        })
      end

      it "allows also to pass specific fields as array instead of comma-separates values" do
        first_user = create(:user)
        second_user = create(:user)
        first_comment = create(:long_comment, user: first_user)
        second_comment = create(:long_comment, user: second_user)
        long_comments = [first_comment, second_comment]
        post = create(:post, :with_author, long_comments: long_comments)

        serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: ['title', 'author']})
        expect(serialized_data['data']['attributes']).to eq ({
          'title' => post.title
        })
        expect(serialized_data['data']['relationships']).to eq ({
          'author' => {
            'links' => {
              'self' => '/posts/1/relationships/author',
              'related' => '/posts/1/author'
            }
          }
        })
      end

      it 'allows to limit fields(attributes and relationships) for included resources' do
        first_user = create(:user)
        second_user = create(:user)
        first_comment = create(:long_comment, user: first_user)
        second_comment = create(:long_comment, user: second_user)
        long_comments = [first_comment, second_comment]
        post = create(:post, :with_author, long_comments: long_comments)

        expected_primary_data = serialize_primary(post, {
          serializer: MyApp::PostSerializer,
          include_linkages: ['author'],
          fields: {'posts' => [:title, :author] }
        })

        fields = {posts: 'title,author', users: ''}
        serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
        expect(serialized_data).to eq ({
          'data' => expected_primary_data,
          'included' => [
            serialize_primary(
              post.author,
              serializer: MyAppOtherNamespace::UserSerializer,
              fields: {'users' => []},
            )
          ]
        })

        fields = {posts: 'title,author'}
        serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
        expect(serialized_data).to eq ({
          'data' => expected_primary_data,
          'included' => [
            serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer)
          ]
        })
      end
    end
  end

  describe 'serialize (class method)' do
    it 'delegates to module method but overrides serializer' do
      post = create(:post)
      expect(MyApp::SimplestPostSerializer.serialize(post)).to eq({
        'data' => serialize_primary(post, {serializer: MyApp::SimplestPostSerializer}),
      })
    end
  end

  describe 'internal-only parse_relationship_paths' do
    it 'correctly handles empty arrays' do
      result = JSONAPI::Serializer.send(:parse_relationship_paths, [])
      expect(result).to eq({})
    end

    it 'correctly handles single-level relationship paths' do
      result = JSONAPI::Serializer.send(:parse_relationship_paths, ['foo'])
      expect(result).to eq({
        'foo' => {_include: true}
      })
    end

    it 'correctly handles multi-level relationship paths' do
      result = JSONAPI::Serializer.send(:parse_relationship_paths, ['foo.bar'])
      expect(result).to eq({
        'foo' => {_include: true, 'bar' => {_include: true}}
      })
    end

    it 'correctly handles multi-level relationship paths with same parent' do
      paths = ['foo', 'foo.bar']
      result = JSONAPI::Serializer.send(:parse_relationship_paths, paths)
      expect(result).to eq({
        'foo' => {_include: true, 'bar' => {_include: true}}
      })
    end

    it 'correctly handles multi-level relationship paths with different parent' do
      paths = ['foo', 'bar', 'bar.baz']
      result = JSONAPI::Serializer.send(:parse_relationship_paths, paths)
      expect(result).to eq({
        'foo' => {_include: true},
        'bar' => {_include: true, 'baz' => {_include: true}},
      })
    end

    it 'correctly handles three-leveled path' do
      paths = ['foo', 'foo.bar', 'foo.bar.baz']
      result = JSONAPI::Serializer.send(:parse_relationship_paths, paths)
      expect(result).to eq({
        'foo' => {_include: true, 'bar' => {_include: true, 'baz' => {_include: true}}}
      })
    end

    it 'correctly handles three-leveled path with skipped middle' do
      paths = ['foo', 'foo.bar.baz']
      result = JSONAPI::Serializer.send(:parse_relationship_paths, paths)
      expect(result).to eq({
        'foo' => {_include: true, 'bar' => {_include: true, 'baz' => {_include: true}}}
      })
    end
  end

  describe 'if/unless handling with contexts' do
    it 'can be used to show/hide attributes' do
      post = create(:post)
      options = {serializer: MyApp::PostSerializerWithContext}

      options[:context] = {show_body: false}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to_not have_key('body')

      options[:context] = {show_body: true}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to have_key('body')
      expect(data['data']['attributes']['body']).to eq('Body for Post 1')

      options[:context] = {hide_body: true}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to_not have_key('body')

      options[:context] = {hide_body: false}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to have_key('body')
      expect(data['data']['attributes']['body']).to eq('Body for Post 1')

      options[:context] = {show_body: false, hide_body: false}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to_not have_key('body')

      options[:context] = {show_body: true, hide_body: false}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to have_key('body')
      expect(data['data']['attributes']['body']).to eq('Body for Post 1')

      # Remember: attribute is configured as if: show_body?, unless: hide_body?
      # and the results should be logically AND'd together:
      options[:context] = {show_body: false, hide_body: true}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to_not have_key('body')

      options[:context] = {show_body: true, hide_body: true}
      data = JSONAPI::Serializer.serialize(post, options)
      expect(data['data']['attributes']).to_not have_key('body')
    end
  end

  describe 'context' do
    it 'is passed through all relationship serializers' do
      # Force long_comments to be serialized by the context-sensitive serializer.
      expect_any_instance_of(MyApp::LongComment).to receive(:jsonapi_serializer_class_name)
        .at_least(:once)
        .and_return('MyApp::LongCommentsSerializerWithContext')

      user = create(:user, name: 'Long Comment Author -- Should Not Be Serialized')
      long_comment = create(:long_comment, user: user)
      post = create(:post, :with_author, long_comments: [long_comment])

      context = {show_body: false, show_comments_user: false}
      expected_data = {
        'data' => serialize_primary(post, {
          serializer: MyApp::PostSerializerWithContext,
          include_linkages: ['long-comments'],
          context: context,
        }),
        'included' => [
          serialize_primary(long_comment, {
            serializer: MyApp::LongCommentsSerializerWithContext,
            context: context,
          }),
        ],
      }
      includes = ['long-comments']
      actual_data = JSONAPI::Serializer.serialize(post, context: context, include: includes)
      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end
  end

  describe 'base_url' do
    it 'is empty by default' do
      long_comments = create_list(:long_comment, 1)
      post = create(:post, long_comments: long_comments)
      data = JSONAPI::Serializer.serialize(post)
      expect(data['data']['links']['self']).to eq('/posts/1')
      expect(data['data']['relationships']['author']['links']).to eq({
        'self' => '/posts/1/relationships/author',
        'related' => '/posts/1/author'
      })
    end

    it 'adds base_url to links if passed' do
      long_comments = create_list(:long_comment, 1)
      post = create(:post, long_comments: long_comments)
      data = JSONAPI::Serializer.serialize(post, base_url: 'http://example.com')
      expect(data['data']['links']['self']).to eq('http://example.com/posts/1')
      expect(data['data']['relationships']['author']['links']).to eq({
        'self' => 'http://example.com/posts/1/relationships/author',
        'related' => 'http://example.com/posts/1/author'
      })
    end

    it 'uses overriden base_url method if it exists' do
      long_comments = create_list(:long_comment, 1)
      post = create(:post, long_comments: long_comments)
      data = JSONAPI::Serializer.serialize(post, serializer: MyApp::PostSerializerWithBaseUrl)
      expect(data['data']['links']['self']).to eq('http://example.com/posts/1')
      expect(data['data']['relationships']['author']['links']).to eq({
        'self' => 'http://example.com/posts/1/relationships/author',
        'related' => 'http://example.com/posts/1/author'
      })
    end
  end

  describe 'inheritance through subclassing' do
    it 'inherits attributes' do
      tagged_post = create(:tagged_post)
      options = {serializer: MyApp::PostSerializerWithInheritedProperties}
      data = JSONAPI::Serializer.serialize(tagged_post, options);
      expect(data['data']['attributes']['title']).to eq('Title for TaggedPost 1');
      expect(data['data']['attributes']['tag']).to eq('Tag for TaggedPost 1');
    end

    it 'inherits relations' do
      long_comments = create_list(:long_comment, 2)
      tagged_post = create(:tagged_post, :with_author, long_comments: long_comments)
      options = {serializer: MyApp::PostSerializerWithInheritedProperties}
      data = JSONAPI::Serializer.serialize(tagged_post, options);

      expect(data['data']['relationships']).to eq({
        'author' => {
          'links' => {
            'self' => '/tagged-posts/1/relationships/author',
            'related' => '/tagged-posts/1/author',
          },
        },
        'long-comments' => {
          'links' => {
            'self' => '/tagged-posts/1/relationships/long-comments',
            'related' => '/tagged-posts/1/long-comments',
          }
        }
      })
    end
  end

  describe 'include validation' do
    it 'raises an exception when join character is invalid' do
      expect do
        JSONAPI::Serializer.serialize(create(:post), include: 'long_comments');
      end.to raise_error(JSONAPI::Serializer::InvalidIncludeError)

      expect do
        JSONAPI::Serializer.serialize(create(:post), include: 'long-comments');
      end.not_to raise_error

      expect do
        JSONAPI::Serializer.serialize(create(:underscore_test), include: 'tagged-posts');
      end.to raise_error(JSONAPI::Serializer::InvalidIncludeError)

      expect do
        JSONAPI::Serializer.serialize(create(:underscore_test), include: 'tagged_posts');
      end.not_to raise_error
    end
  end

  describe 'serializer with namespace option' do
    it 'can serialize a simple object with namespace Api::V1' do
      user = create(:user)
      expect(JSONAPI::Serializer.serialize(user, {namespace: Api::V1})).to eq({
        'data' => serialize_primary(user, {serializer: Api::V1::MyApp::UserSerializer}),
      })
    end

    it 'handles recursive loading of relationships with namespaces' do
      user = create(:user)
      long_comments = create_list(:long_comment, 2, user: user)
      post = create(:post, :with_author, long_comments: long_comments)
      # Make sure each long-comment has a circular reference back to the post.
      long_comments.each { |c| c.post = post }

      expected_data = {
        'data' => serialize_primary(post, {
          serializer: Api::V1::MyApp::PostSerializer,
          include_linkages: ['long-comments']
        }),
        'included' => [
          # Intermediates are included: long-comments, long-comments.post, and long-comments.post.author
          #  http://jsonapi.org/format/#document-structure-compound-documents
          serialize_primary(post.long_comments.first, {
            serializer: Api::V1::MyApp::LongCommentSerializer,
            include_linkages: ['post']
          }),
          serialize_primary(post.long_comments.last, {
            serializer: Api::V1::MyApp::LongCommentSerializer,
            include_linkages: ['post']
          }),
          serialize_primary(post, {
            serializer: Api::V1::MyApp::PostSerializer,
            include_linkages: ['author', 'post.long-comments']
          }),
          serialize_primary(post.author, {serializer: Api::V1::MyApp::UserSerializer})
        ],
      }
      includes = ['long-comments.post.author']
      actual_data = JSONAPI::Serializer.serialize(post, include: includes, namespace: Api::V1)
      # Multiple expectations for better diff output for debugging.
      expect(actual_data['data']).to eq(expected_data['data'])
      expect(actual_data['included']).to eq(expected_data['included'])
      expect(actual_data).to eq(expected_data)
    end
  end

  describe 'instrumentation' do
    let(:post) { create(:post, :with_author) }
    let(:events) { [] }

    before do
      ActiveSupport::Notifications.subscribe(notification_name) do |*args|
        events << ActiveSupport::Notifications::Event.new(*args)
      end
    end

    describe 'serialize_primary' do
      let(:notification_name) { 'render.jsonapi_serializers.serialize_primary' }

      it 'sends an event for a single serialize call' do
        JSONAPI::Serializer.serialize(post)

        expect(events.length).to eq(1)
        expect(events[0].name).to eq(notification_name)
        expect(events[0].payload).to eq({class_name: "MyApp::Post"})
      end

      it 'sends events for includes' do
        JSONAPI::Serializer.serialize(post, include: ['author'])

        expect(events.length).to eq(2)
        expect(events[0].payload).to eq({class_name: "MyApp::Post"})
        expect(events[1].payload).to eq({class_name: "MyApp::User"})
      end
    end

    describe 'find_recursive_relationships' do
      let(:notification_name) { 'render.jsonapi_serializers.find_recursive_relationships' }

      it 'does not send event when there are no includes' do
        JSONAPI::Serializer.serialize(post)
        expect(events.length).to eq(0)
      end

      it 'sends events for includes' do
        JSONAPI::Serializer.serialize(post, include: ['author'])

        expect(events.length).to eq(2)
        expect(events[0].name).to eq(notification_name)
        expect(events[0].payload).to eq({class_name: "MyApp::User"})
        expect(events[1].name).to eq(notification_name)
        expect(events[1].payload).to eq({class_name: "MyApp::Post"})
      end
    end
  end
end
