require 'test_helper'
require 'time_travel_helper'

class AssociationsTest < ActiveSupport::TestCase
  # These would have been done in test_helper.rb if using_mysql? is true
  unless using_mysql?
    self.use_transactional_fixtures = false
    setup { DatabaseCleaner.start }
  end

  teardown do
    Timecop.return
    # This would have been done in test_helper.rb if using_mysql? is true
    DatabaseCleaner.clean unless using_mysql?
  end

  context "a has_one association" do
    setup { @widget = Widget.create :name => 'widget_0' }

    context 'before the associated was created' do
      setup do
        @widget.update_attributes :name => 'widget_1'
        @wotsit = @widget.create_wotsit :name => 'wotsit_0'
      end

      context 'when reified' do
        setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }

        should 'see the associated as it was at the time' do
          assert_nil @widget_0.wotsit
        end

        should 'not persist changes to the live association' do
          assert_equal @wotsit, @widget.wotsit(true)
        end
      end
    end

    context 'where the association is created between model versions' do
      setup do
        @wotsit = @widget.create_wotsit :name => 'wotsit_0'
        Timecop.travel 1.second.since
        @widget.update_attributes :name => 'widget_1'
      end

      context 'when reified' do
        setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }

        should 'see the associated as it was at the time' do
          assert_equal 'wotsit_0', @widget_0.wotsit.name
        end

        should 'not persist changes to the live association' do
          assert_equal @wotsit, @widget.wotsit(true)
        end
      end

      context 'and then the associated is updated between model versions' do
        setup do
          @wotsit.update_attributes :name => 'wotsit_1'
          @wotsit.update_attributes :name => 'wotsit_2'
          Timecop.travel 1.second.since
          @widget.update_attributes :name => 'widget_2'
          @wotsit.update_attributes :name => 'wotsit_3'
        end

        context 'when reified' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }

          should 'see the associated as it was at the time' do
            assert_equal 'wotsit_2', @widget_1.wotsit.name
          end

          should 'not persist changes to the live association' do
            assert_equal 'wotsit_3', @widget.wotsit(true).name
          end
        end

        context 'when reified opting out of has_one reification' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }

          should 'see the associated as it is live' do
            assert_equal 'wotsit_3', @widget_1.wotsit.name
          end
        end
      end

      context 'and then the associated is destroyed' do
        setup do
          @wotsit.destroy
        end

        context 'when reify' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }

          should 'see the associated as it was at the time' do
            assert_equal @wotsit, @widget_1.wotsit
          end

          should 'not persist changes to the live association' do
            assert_nil @widget.wotsit(true)
          end
        end

        context 'and then the model is updated' do
          setup do
            Timecop.travel 1.second.since
            @widget.update_attributes :name => 'widget_3'
          end

          context 'when reified' do
            setup { @widget_2 = @widget.versions.last.reify(:has_one => true) }

            should 'see the associated as it was at the time' do
              assert_nil @widget_2.wotsit
            end
          end
        end
      end
    end
  end

  context "a has_many association" do
    setup { @customer = Customer.create :name => 'customer_0' }

    context 'updated before the associated was created' do
      setup do
        @customer.update_attributes! :name => 'customer_1'
        @customer.orders.create! :order_date => Date.today
      end

      context 'when reified' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal [], @customer_0.orders
        end

        should 'not persist changes to the live association' do
          assert_not_equal [], @customer.orders(true)
        end
      end

      context 'when reified with option mark_for_destruction' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

        should 'mark the associated for destruction' do
          assert_equal [true], @customer_0.orders.map(&:marked_for_destruction?)
        end
      end
    end

    context 'where the association is created between model versions' do
      setup do
        @order = @customer.orders.create! :order_date => 'order_date_0'
        Timecop.travel 1.second.since
        @customer.update_attributes :name => 'customer_1'
      end

      context 'when reified' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
        end
      end

      context 'and then a nested has_many association is created' do
        setup do
          @order.line_items.create! :product => 'product_0'
        end

        context 'when reified' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

          should 'see the live version of the nested association' do
            assert_equal ['product_0'], @customer_0.orders.first.line_items.map(&:product)
          end
        end
      end

      context 'and then the associated is updated between model versions' do
        setup do
          @order.update_attributes :order_date => 'order_date_1'
          @order.update_attributes :order_date => 'order_date_2'
          Timecop.travel 1.second.since
          @customer.update_attributes :name => 'customer_2'
          @order.update_attributes :order_date => 'order_date_3'
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal ['order_date_3'], @customer.orders(true).map(&:order_date)
          end
        end

        context 'when reified opting out of has_many reification' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => false) }

          should 'see the associated as it is live' do
            assert_equal ['order_date_3'], @customer_1.orders.map(&:order_date)
          end
        end

        context 'and then the associated is destroyed' do
          setup do
            @order.destroy
          end

          context 'when reified' do
            setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
            end

            should 'not persist changes to the live association' do
              assert_equal [], @customer.orders(true)
            end
          end
        end
      end

      context 'and then the associated is destroyed' do
        setup do
          @order.destroy
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [@order.order_date], @customer_1.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal [], @customer.orders(true)
          end
        end
      end

      context 'and then the associated is destroyed between model versions' do
        setup do
          @order.destroy
          Timecop.travel 1.second.since
          @customer.update_attributes :name => 'customer_2'
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @customer_1.orders
          end
        end
      end

      context 'and then another association is added' do
        setup do
          @customer.orders.create! :order_date => 'order_date_1'
        end

        context 'when reified' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal ['order_date_0', 'order_date_1'], @customer.orders(true).map(&:order_date).sort
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'mark the newly associated for destruction' do
            assert @customer_0.orders.detect { |o| o.order_date == 'order_date_1'}.marked_for_destruction?
          end
        end
      end
    end
  end

  context "has_many through associations" do
    context "Books, Authors, and Authorships" do
      setup { @book = Book.create :title => 'book_0' }

      context 'updated before the associated was created' do
        setup do
          @book.update_attributes! :title => 'book_1'
          @book.authors.create! :name => 'author_0'
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @book_0.authors
          end

          should 'not persist changes to the live association' do
            assert_equal ['author_0'], @book.authors(true).map(&:name)
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'mark the associated for destruction' do
            assert_equal [true], @book_0.authors.map(&:marked_for_destruction?)
          end

          should 'mark the associated-through for destruction' do
            assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
          end
        end
      end

      context 'updated before it is associated with an existing one' do
        setup do
          person_existing = Person.create(:name => 'person_existing')
          Timecop.travel 1.second.since
          @book.update_attributes! :title => 'book_1'
          @book.authors << person_existing
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @book_0.authors
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'not mark the associated for destruction' do
            assert_equal [false], @book_0.authors.map(&:marked_for_destruction?)
          end

          should 'mark the associated-through for destruction' do
            assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
          end
        end
      end

      context 'where the association is created between model versions' do
        setup do
          @author = @book.authors.create! :name => 'author_0'
          @person_existing = Person.create(:name => 'person_existing')
          Timecop.travel 1.second.since
          @book.update_attributes! :title => 'book_1'
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['author_0'], @book_0.authors.map(&:name)
          end
        end

        context 'and then the associated is updated between model versions' do
          setup do
            @author.update_attributes :name => 'author_1'
            @author.update_attributes :name => 'author_2'
            Timecop.travel 1.second.since
            @book.update_attributes :title => 'book_2'
            @author.update_attributes :name => 'author_3'
          end

          context 'when reified' do
            setup { @book_1 = @book.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal ['author_2'], @book_1.authors.map(&:name)
            end

            should 'not persist changes to the live association' do
              assert_equal ['author_3'], @book.authors(true).map(&:name)
            end
          end

          context 'when reified opting out of has_many reification' do
            setup { @book_1 = @book.versions.last.reify(:has_many => false) }

            should 'see the associated as it is live' do
              assert_equal ['author_3'], @book_1.authors.map(&:name)
            end
          end
        end

        context 'and then the associated is destroyed' do
          setup do
            @author.destroy
          end

          context 'when reified' do
            setup { @book_1 = @book.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal [@author.name], @book_1.authors.map(&:name)
            end

            should 'not persist changes to the live association' do
              assert_equal [], @book.authors(true)
            end
          end
        end

        context 'and then the associated is destroyed between model versions' do
          setup do
            @author.destroy
            Timecop.travel 1.second.since
            @book.update_attributes :title => 'book_2'
          end

          context 'when reified' do
            setup { @book_1 = @book.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal [], @book_1.authors
            end
          end
        end

        context 'and then the associated is dissociated between model versions' do
          setup do
            @book.authors = []
            Timecop.travel 1.second.since
            @book.update_attributes :title => 'book_2'
          end

          context 'when reified' do
            setup { @book_1 = @book.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal [], @book_1.authors
            end
          end
        end

        context 'and then another associated is created' do
          setup do
            @book.authors.create! :name => 'author_1'
          end

          context 'when reified' do
            setup { @book_0 = @book.versions.last.reify(:has_many => true) }

            should 'only see the first associated' do
              assert_equal ['author_0'], @book_0.authors.map(&:name)
            end

            should 'not persist changes to the live association' do
              assert_equal ['author_0', 'author_1'], @book.authors(true).map(&:name)
            end
          end

          context 'when reified with option mark_for_destruction' do
            setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

            should 'mark the newly associated for destruction' do
              assert @book_0.authors.detect { |a| a.name == 'author_1' }.marked_for_destruction?
            end

            should 'mark the newly associated-through for destruction' do
              assert @book_0.authorships.detect { |as| as.person.name == 'author_1' }.marked_for_destruction?
            end
          end
        end

        context 'and then an existing one is associated' do
          setup do
            @book.authors << @person_existing
          end

          context 'when reified' do
            setup { @book_0 = @book.versions.last.reify(:has_many => true) }

            should 'only see the first associated' do
              assert_equal ['author_0'], @book_0.authors.map(&:name)
            end

            should 'not persist changes to the live association' do
              assert_equal ['author_0', 'person_existing'], @book.authors(true).map(&:name).sort
            end
          end

          context 'when reified with option mark_for_destruction' do
            setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

            should 'not mark the newly associated for destruction' do
              assert !@book_0.authors.detect { |a| a.name == 'person_existing' }.marked_for_destruction?
            end

            should 'mark the newly associated-through for destruction' do
              assert @book_0.authorships.detect { |as| as.person.name == 'person_existing' }.marked_for_destruction?
            end
          end
        end
      end

      context 'updated before the associated without paper_trail was created' do
        setup do
          @book.update_attributes! :title => 'book_1'
          @book.editors.create! :name => 'editor_0'
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'see the live association' do
            assert_equal ['editor_0'], @book_0.editors.map(&:name)
          end
        end
      end
    end

    context "Chapters, Sections, Paragraphs, Quotations, and Citations" do
      setup { @chapter = Chapter.create(:name => 'ch_1') }

      context "before any associations are created" do
        setup do
          @chapter.update_attributes(:name => "ch_2")
        end

        should "not reify any associations" do
          chapter_v1 = @chapter.versions[1].reify(:has_many => true)
          assert_equal "ch_1", chapter_v1.name
          assert_equal [], chapter_v1.sections
          assert_equal [], chapter_v1.paragraphs
        end
      end

      context "after the first has_many through relationship is created" do
        setup do
          assert_equal 1, @chapter.versions.size
          @chapter.update_attributes :name => "ch_2"
          assert_equal 2, @chapter.versions.size

          Timecop.travel 1.second.since
          @chapter.sections.create :name => "section 1"
          Timecop.travel 1.second.since
          @chapter.sections.first.update_attributes :name => "section 2"
          Timecop.travel 1.second.since
          @chapter.update_attributes :name => "ch_3"
          assert_equal 3, @chapter.versions.size

          Timecop.travel 1.second.since
          @chapter.sections.first.update_attributes :name => "section 3"
        end

        context "version 1" do
          should "have no sections" do
            chapter_v1 = @chapter.versions[1].reify(:has_many => true)
            assert_equal [], chapter_v1.sections
          end
        end

        context "version 2" do
          should "have one section" do
            chapter_v2 = @chapter.versions[2].reify(:has_many => true)
            assert_equal 1, chapter_v2.sections.size

            # Shows the value of the section as it was before
            # the chapter was updated.
            assert_equal ['section 2'], chapter_v2.sections.map(&:name)

            # Shows the value of the chapter as it was before
            assert_equal "ch_2", chapter_v2.name
          end
        end

        context "version 2, before the section was destroyed" do
          setup do
            @chapter.update_attributes :name => 'ch_3'
            Timecop.travel 1.second.since
            @chapter.sections.destroy_all
            Timecop.travel 1.second.since
          end

          should "have the one section" do
            chapter_v2 = @chapter.versions[2].reify(:has_many => true)
            assert_equal ['section 2'], chapter_v2.sections.map(&:name)
          end
        end

        context "version 3, after the section was destroyed" do
          setup do
            @chapter.sections.destroy_all
            Timecop.travel 1.second.since
            @chapter.update_attributes :name => 'ch_4'
            Timecop.travel 1.second.since
          end

          should "have no sections" do
            chapter_v3 = @chapter.versions[3].reify(:has_many => true)
            assert_equal 0, chapter_v3.sections.size
          end
        end

        context "after creating a paragraph" do
          setup do
            assert_equal 3, @chapter.versions.size
            @section = @chapter.sections.first
            @paragraph = @section.paragraphs.create :name => 'para1'
          end

          context "new chapter version" do
            should "have one paragraph" do
              initial_section_name = @section.name
              initial_paragraph_name = @paragraph.name
              Timecop.travel 1.second.since
              @chapter.update_attributes :name => 'ch_5'
              assert_equal 4, @chapter.versions.size
              Timecop.travel 1.second.since
              @paragraph.update_attributes :name => 'para3'
              chapter_v3 = @chapter.versions[3].reify(:has_many => true)
              assert_equal [initial_section_name], chapter_v3.sections.map(&:name)
              paragraphs = chapter_v3.sections.first.paragraphs
              assert_equal 1, paragraphs.size
              assert_equal [initial_paragraph_name], paragraphs.map(&:name)
            end
          end

          context "destroy a section" do
            should "not have any sections or paragraphs" do
              @section.destroy
              Timecop.travel 1.second.since
              @chapter.update_attributes(:name => 'chapter 6')
              assert_equal 4, @chapter.versions.size
              chapter_v3 = @chapter.versions[3].reify(:has_many => true)
              assert_equal 0, chapter_v3.sections.size
              assert_equal 0, chapter_v3.paragraphs.size
            end
          end

          context "the version before a paragraph is destroyed" do
            should "have the one paragraph" do
              initial_paragraph_name = @section.paragraphs.first.name
              Timecop.travel 1.second.since
              @chapter.update_attributes(:name => 'chapter 6')
              Timecop.travel 1.second.since
              @paragraph.destroy
              chapter_v3 = @chapter.versions[3].reify(:has_many => true)
              paragraphs = chapter_v3.sections.first.paragraphs
              assert_equal 1, paragraphs.size
              assert_equal initial_paragraph_name, paragraphs.first.name
            end
          end

          context "the version after a paragraph is destroyed" do
            should "have no paragraphs" do
              @paragraph.destroy
              Timecop.travel 1.second.since
              @chapter.update_attributes(:name => 'chapter 6')
              chapter_v3 = @chapter.versions[3].reify(:has_many => true)
              assert_equal 0, chapter_v3.paragraphs.size
              assert_equal [], chapter_v3.sections.first.paragraphs
            end
          end
        end
      end

      context "a chapter with one paragraph and one citation" do
        should "reify paragraphs and citations" do
          chapter = Chapter.create(:name => 'Chapter One')
          section = Section.create(:name => 'Section One', :chapter => chapter)
          paragraph = Paragraph.create(:name => 'Paragraph One', :section => section)
          quotation = Quotation.create(:chapter => chapter)
          citation = Citation.create(:quotation => quotation)
          Timecop.travel 1.second.since
          chapter.update_attributes(:name => 'Chapter One, Vers. Two')
          assert_equal 2, chapter.versions.count
          paragraph.destroy
          citation.destroy
          reified = chapter.versions[1].reify(:has_many => true)
          assert_equal [paragraph], reified.sections.first.paragraphs
          assert_equal [citation], reified.quotations.first.citations
        end
      end
    end
  end
end
