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',
        },
        'relationships' => {},
      })
    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/links/user',
              'related' => '/long-comments/1/user',
            },
          },
          'post' => {
            'links' => {
              'self' => '/long-comments/1/links/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',
        },
        'relationships' => {},
        '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/links/author',
                'related' => '/posts/1/author',
              },
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/links/long-comments',
                'related' => '/posts/1/long-comments',
              },
            },
          },
        })
      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/links/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/links/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/links/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/links/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/links/author',
                'related' => '/posts/1/author',
              },
              'data' => nil,
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/links/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/links/author',
                'related' => '/posts/1/author',
              },
              'data' => nil,
            },
            'long-comments' => {
              'links' => {
                'self' => '/posts/1/links/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
  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 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 '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: MyApp::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 = {
        # Note that in this case the primary data does not include linkage for 'long-comments',
        # forcing clients to still have to request linkage from long-comments and post. This is an
        # odd but valid data state because the user requested to only include the leaf author node,
        # and we only automatically expose direct children linkages if they match given includes.
        #
        # Spec: Resource linkage in a compound document allows a client to link together
        # all of the included resource objects without having to GET any relationship URLs.
        # http://jsonapi.org/format/#document-structure-resource-relationships
        #
        # Also, spec: A request for comments.author should not automatically also include
        # comments in the response. This can happen if the client already has the comments locally,
        # and now wants to fetch the associated authors without fetching the comments again.
        # http://jsonapi.org/format/#fetching-includes
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
        'included' => [
          # Only the author is included:
          serialize_primary(post.author, {serializer: MyApp::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 = {
        # Same note about primary data linkages as above.
        'data' => serialize_primary(post, {serializer: MyApp::PostSerializer}),
        'included' => [
          serialize_primary(first_user, {serializer: MyApp::UserSerializer}),
          serialize_primary(second_user, {serializer: MyApp::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: MyApp::UserSerializer}),
        ],
      }
      includes = ['long-comments', '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}),
          serialize_primary(long_comments.last, {serializer: MyApp::LongCommentSerializer}),
          serialize_primary(post.author, {serializer: MyApp::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
  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' => {'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' => {'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::PostSerializerWithContextHandling}

      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
    xit 'is correctly passed through all serializers' do
    end
  end
end