# frozen_string_literal: true

require 'test_helper'
module ActiveModel
  class Serializer
    class ReflectionTest < ActiveSupport::TestCase
      class Blog < ActiveModelSerializers::Model
        attributes :id
      end
      class BlogSerializer < ActiveModel::Serializer
        type 'blog'
        attributes :id
      end

      setup do
        @expected_meta = { id: 1 }
        @expected_links = { self: 'no_uri_validation' }
        @empty_links = {}
        model_attributes = { blog: Blog.new(@expected_meta) }
        @model = Class.new(ActiveModelSerializers::Model) do
          attributes(*model_attributes.keys)

          def self.name
            'TestModel'
          end
        end.new(model_attributes)
        @instance_options = {}
      end

      def evaluate_association_value(association)
        association.lazy_association.eval_reflection_block
      end

      # TODO: Remaining tests
      # test_reflection_value_block_with_scope
      # test_reflection_value_uses_serializer_instance_method
      # test_reflection_excluded_eh_blank_is_false
      # test_reflection_excluded_eh_if
      # test_reflection_excluded_eh_unless
      # test_evaluate_condition_symbol_serializer_method
      # test_evaluate_condition_string_serializer_method
      # test_evaluate_condition_proc
      # test_evaluate_condition_proc_yields_serializer
      # test_evaluate_condition_other
      # test_options_key
      # test_options_polymorphic
      # test_options_serializer
      # test_options_virtual_value
      # test_options_namespace

      def test_reflection_value
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_nil reflection.block
        assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
        assert_equal true, reflection.options.fetch(:include_data_setting)

        include_slice = :does_not_matter
        assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
      end

      def test_reflection_value_block
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            object.blog
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_respond_to reflection.block, :call
        assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
        assert_equal true, reflection.options.fetch(:include_data_setting)

        include_slice = :does_not_matter
        assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
      end

      def test_reflection_value_block_with_explicit_include_data_true
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            include_data true
            object.blog
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_respond_to reflection.block, :call
        assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
        assert_equal true, reflection.options.fetch(:include_data_setting)

        include_slice = :does_not_matter
        assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
      end

      def test_reflection_value_block_with_include_data_false_mutates_the_reflection_include_data
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            include_data false
            object.blog
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_respond_to reflection.block, :call
        assert_equal true, reflection.options.fetch(:include_data_setting)
        include_slice = :does_not_matter
        assert_nil reflection.send(:value, serializer_instance, include_slice)
        assert_equal false, reflection.options.fetch(:include_data_setting)
      end

      def test_reflection_value_block_with_include_data_if_sideloaded_included_mutates_the_reflection_include_data
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            include_data :if_sideloaded
            object.blog
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_respond_to reflection.block, :call
        assert_equal true, reflection.options.fetch(:include_data_setting)
        include_slice = {}
        assert_nil reflection.send(:value, serializer_instance, include_slice)
        assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
      end

      def test_reflection_value_block_with_include_data_if_sideloaded_excluded_mutates_the_reflection_include_data
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            include_data :if_sideloaded
            object.blog
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)

        # Assert
        assert_respond_to reflection.block, :call
        assert_equal true, reflection.options.fetch(:include_data_setting)
        include_slice = { blog: :does_not_matter }
        assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
        assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
      end

      def test_reflection_block_with_link_mutates_the_reflection_links
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            link :self, 'no_uri_validation'
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_equal @empty_links, reflection.options.fetch(:links)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)

        # Assert association links empty when not yet evaluated
        assert_equal @empty_links, reflection.options.fetch(:links)
        assert_equal @empty_links, association.links

        evaluate_association_value(association)

        assert_equal @expected_links, association.links
        assert_equal @expected_links, reflection.options.fetch(:links)
      end

      def test_reflection_block_with_link_block_mutates_the_reflection_links
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            link :self do
              'no_uri_validation'
            end
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_equal @empty_links, reflection.options.fetch(:links)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)

        # Assert association links empty when not yet evaluated
        assert_equal @empty_links, association.links

        evaluate_association_value(association)

        # Assert before instance_eval link
        link = association.links.fetch(:self)
        assert_respond_to link, :call
        assert_respond_to reflection.options.fetch(:links).fetch(:self), :call

        # Assert after instance_eval link
        assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link)
        assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
      end

      def test_reflection_block_with_meta_mutates_the_reflection_meta
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            meta(id: object.blog.id)
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_nil reflection.options.fetch(:meta)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)

        evaluate_association_value(association)

        assert_equal @expected_meta, association.meta
        assert_equal @expected_meta, reflection.options.fetch(:meta)
      end

      def test_reflection_block_with_meta_block_mutates_the_reflection_meta
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            meta do
              { id: object.blog.id }
            end
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_nil reflection.options.fetch(:meta)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)
        # Assert before instance_eval meta

        evaluate_association_value(association)

        assert_respond_to association.meta, :call
        assert_respond_to reflection.options.fetch(:meta), :call

        # Assert after instance_eval meta
        assert_equal @expected_meta, reflection.instance_eval(&association.meta)
        assert_respond_to reflection.options.fetch(:meta), :call
        assert_respond_to association.meta, :call
      end

      # rubocop:disable Metrics/AbcSize
      def test_reflection_block_with_meta_in_link_block_mutates_the_reflection_meta
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            link :self do
              meta(id: object.blog.id)
              'no_uri_validation'
            end
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_nil reflection.options.fetch(:meta)
        assert_equal @empty_links, reflection.options.fetch(:links)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)
        # Assert before instance_eval link meta
        assert_nil association.meta
        assert_nil reflection.options.fetch(:meta)

        evaluate_association_value(association)

        link = association.links.fetch(:self)
        assert_respond_to link, :call
        assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
        assert_nil reflection.options.fetch(:meta)

        # Assert after instance_eval link
        assert_equal 'no_uri_validation', reflection.instance_eval(&link)
        assert_equal @expected_meta, reflection.options.fetch(:meta)
        assert_equal @expected_meta, association.meta
      end
      # rubocop:enable Metrics/AbcSize

      # rubocop:disable Metrics/AbcSize
      def test_reflection_block_with_meta_block_in_link_block_mutates_the_reflection_meta
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            link :self do
              meta do
                { id: object.blog.id }
              end
              'no_uri_validation'
            end
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_nil reflection.options.fetch(:meta)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)
        assert_nil association.meta
        assert_nil reflection.options.fetch(:meta)

        # Assert before instance_eval link

        evaluate_association_value(association)

        link = association.links.fetch(:self)
        assert_nil reflection.options.fetch(:meta)
        assert_respond_to link, :call
        assert_respond_to association.links.fetch(:self), :call

        # Assert after instance_eval link
        assert_equal 'no_uri_validation', reflection.instance_eval(&link)
        assert_respond_to association.links.fetch(:self), :call
        # Assert before instance_eval link meta
        assert_respond_to reflection.options.fetch(:meta), :call
        assert_respond_to association.meta, :call

        # Assert after instance_eval link meta
        assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta))
        assert_respond_to association.meta, :call
      end
      # rubocop:enable Metrics/AbcSize

      def test_no_href_in_vanilla_reflection
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            link :self do
              href 'no_uri_validation'
            end
          end
        end
        serializer_instance = serializer_class.new(@model, @instance_options)

        # Get Reflection
        reflection = serializer_class._reflections.fetch(:blog)
        assert_equal @empty_links, reflection.options.fetch(:links)

        # Build Association
        association = reflection.build_association(serializer_instance, @instance_options)
        # Assert before instance_eval link

        evaluate_association_value(association)

        link = association.links.fetch(:self)
        assert_respond_to link, :call

        # Assert after instance_eval link
        exception = assert_raise(NoMethodError) do
          reflection.instance_eval(&link)
        end
        assert_match(/undefined method `href'/, exception.message)
      end

      # rubocop:disable Metrics/AbcSize
      def test_mutating_reflection_block_is_not_thread_safe
        serializer_class = Class.new(ActiveModel::Serializer) do
          has_one :blog do
            meta(id: object.blog.id)
          end
        end
        model1_meta = @expected_meta
        # Evaluate reflection meta for model with id 1
        serializer_instance = serializer_class.new(@model, @instance_options)
        reflection = serializer_class._reflections.fetch(:blog)
        assert_nil reflection.options.fetch(:meta)
        association = reflection.build_association(serializer_instance, @instance_options)

        evaluate_association_value(association)

        assert_equal model1_meta, association.meta
        assert_equal model1_meta, reflection.options.fetch(:meta)

        model2_meta = @expected_meta.merge(id: 2)
        # Evaluate reflection meta for model with id 2
        @model.blog.id = 2
        assert_equal 2, @model.blog.id # sanity check
        serializer_instance = serializer_class.new(@model, @instance_options)
        reflection = serializer_class._reflections.fetch(:blog)

        # WARN: Thread-safety issue
        # Before the reflection is evaluated, it has the value from the previous evaluation
        assert_equal model1_meta, reflection.options.fetch(:meta)

        association = reflection.build_association(serializer_instance, @instance_options)

        evaluate_association_value(association)

        assert_equal model2_meta, association.meta
        assert_equal model2_meta, reflection.options.fetch(:meta)
      end
      # rubocop:enable Metrics/AbcSize
    end
    class ThreadedReflectionTest < ActiveSupport::TestCase
      class Post < ::Model
        attributes :id, :title, :body
        associations :comments
      end
      class Comment < ::Model
        attributes :id, :body
        associations :post
      end
      class CommentSerializer < ActiveModel::Serializer
        type 'comment'
        attributes :id, :body
        has_one :post
      end
      class PostSerializer < ActiveModel::Serializer
        type 'post'
        attributes :id, :title, :body
        has_many :comments, serializer: CommentSerializer do
          sleep 0.1
          object.comments
        end
      end

      # per https://github.com/rails-api/active_model_serializers/issues/2270
      def test_concurrent_serialization
        post1 = Post.new(id: 1, title: 'Post 1 Title', body: 'Post 1 Body')
        post1.comments = [Comment.new(id: 1, body: 'Comment on Post 1', post: post1)]
        post2 = Post.new(id: 2, title: 'Post 2 Title', body: 'Post 2 Body')
        post2.comments = [Comment.new(id: 2, body: 'Comment on Post 2', post: post2)]
        serialized_posts = {
          first: Set.new,
          second: Set.new
        }
        t1 = Thread.new do
          10.times do
            serialized_posts[:first] << PostSerializer.new(post1, {}).to_json
          end
        end
        t2 = Thread.new do
          10.times do
            serialized_posts[:second] << PostSerializer.new(post2, {}).to_json
          end
        end
        t1.join
        t2.join
        expected_first_post_serialization  = '{"id":1,"title":"Post 1 Title","body":"Post 1 Body","comments":[{"id":1,"body":"Comment on Post 1"}]}'
        expected_second_post_serialization = '{"id":2,"title":"Post 2 Title","body":"Post 2 Body","comments":[{"id":2,"body":"Comment on Post 2"}]}'

        assert_equal [expected_second_post_serialization], serialized_posts[:second].to_a
        assert_equal [expected_first_post_serialization], serialized_posts[:first].to_a
      end
    end
  end
end
