# encoding: UTF-8

require File.expand_path('../../test_helper', __FILE__)


class RevisionsTest < MiniTest::Spec

  def setup
    @now = Time.now
    @site = setup_site
  end

  def teardown
    teardown_site
  end

  context "Content revisions" do
    setup do
      stub_time(@now)

      Content.delete

      class Page < Spontaneous::Page
        field :title, :string, :default => "New Page"
        box :things
      end
      class Piece < Spontaneous::Piece
        box :things
      end

      @root = Page.create(:uid => "root")
      count = 0
      2.times do |i|
        c = Page.new(:uid => i)
        @root.things << c
        count += 1
        2.times do |j|
          d = Piece.new(:uid => "#{i}.#{j}")
          c.things << d
          count += 1
          2.times do |k|
            d.things << Page.new(:uid => "#{i}.#{j}.#{k}")
            d.save
            count += 1
          end
        end
        c.save
      end
      @root.save
    end

    teardown do
      RevisionsTest.send(:remove_const, :Page) rescue nil
      RevisionsTest.send(:remove_const, :Piece) rescue nil
      Content.delete
      DB.logger = nil
    end

    context "data sources" do

      should "have the right names" do
        Content.revision_table(23).should == '__r00023_content'
        Content.revision_table(nil).should == 'content'
      end

      should "be recognisable" do
        Content.revision_table?('content').should be_false
        Content.revision_table?('__r00023_content').should be_true
        Content.revision_table?('__r00023_not').should be_false
        Content.revision_table?('subscribers').should be_false
      end

      should "be switchable within blocks" do
        Content.dataset.should be_content_revision
        Content.with_revision(23) do
          Content.revision.should ==23
          Content.dataset.should be_content_revision(23)
        end
        Content.dataset.should  be_content_revision
        Content.revision.should be_nil
      end

      should "know which revision is active" do
        Content.with_revision(23) do
          Content.revision.should == 23
        end
      end

      should "understand the with_editable" do
        Content.with_revision(23) do
          Content.dataset.should be_content_revision(23)
          Content.with_editable do
            Content.dataset.should be_content_revision
          end
          Content.dataset.should be_content_revision(23)
        end
        Content.dataset.should be_content_revision
      end

      should "understand with_published" do
        Site.stubs(:published_revision).returns(99)
        Content.with_published do
          Content.dataset.should be_content_revision(99)
          Content.with_editable do
            Content.dataset.should be_content_revision
          end
          Content.dataset.should be_content_revision(99)
        end
        Content.dataset.should be_content_revision
      end

      should "be stackable" do
        Content.dataset.should be_content_revision
        Content.with_revision(23) do
          Content.dataset.should be_content_revision(23)
          Content.with_revision(24) do
            Content.dataset.should be_content_revision(24)
          end
          Content.dataset.should be_content_revision(23)
        end
        Content.dataset.should be_content_revision
      end

      should "reset datasource after an exception" do
        Content.dataset.should be_content_revision
        begin
          Content.with_revision(23) do
            Content.dataset.should be_content_revision(23)
            raise Exception.new
          end
        rescue Exception
        end
        Content.dataset.should be_content_revision
      end

      should "read revision from the environment if present" do
        ENV["SPOT_REVISION"] = '1001'
        Content.with_published do
          Content.dataset.should be_content_revision(1001)
        end
        ENV.delete("SPOT_REVISION")
      end

      context "subclasses" do
        setup do
          class ::Subclass < Page; end
        end

        teardown do
          Object.send(:remove_const, :Subclass)
        end

        should "set all subclasses to use the same dataset" do
          Content.with_revision(23) do
            Subclass.revision.should ==23
            Subclass.dataset.should be_content_revision(23, Subclass.schema_id)
            # piece wasn't loaded until this point
            Piece.dataset.should  be_content_revision(23)
            Piece.revision.should == 23
          end
          Subclass.dataset.should  be_content_revision(nil, Subclass.schema_id)
          Piece.dataset.should  be_content_revision(nil)
        end
      end
    end

    context "content revisions" do
      setup do
        @revision = 1
      end
      teardown do
        Content.delete_revision(@revision)
        Content.delete_revision(@revision+1) rescue nil
      end
      should "be testable for existance" do
        Content.revision_exists?(@revision).should be_false
        Content.create_revision(@revision)
        Content.revision_exists?(@revision).should be_true
      end
      should "be deletable en masse" do
        tables = (1..10).map { |i| Content.revision_table(i).to_sym }
        tables.each do |t|
          DB.create_table(t){Integer :id}
        end
        tables.each do |t|
          DB.tables.include?(t).should be_true
        end
        Content.delete_all_revisions!
        tables.each do |t|
          DB.tables.include?(t).should be_false
        end
      end

      should "be creatable from current content" do
        DB.tables.include?(Content.revision_table(@revision).to_sym).should be_false
        Content.create_revision(@revision)
        DB.tables.include?(Content.revision_table(@revision).to_sym).should be_true
        count = Content.count
        Content.with_revision(@revision) do
          Content.count.should == count
          Content.all.each do |published|
            Content.with_editable do
              e = Content[published.id]
              e.should == published
            end
          end
        end
      end

      should "be creatable from any revision" do
        revision = 2
        source_revision = @revision
        source_revision_count = nil

        Content.create_revision(source_revision)

        Content.with_revision(source_revision) do
          Content.first(:uid => "0").destroy
          source_revision_count = Content.count
        end

        Content.count.should == source_revision_count + 7

        Content.create_revision(revision, source_revision)

        Content.with_revision(revision) do
          Content.count.should == source_revision_count
          Content.all.each do |published|

            Content.with_revision(source_revision) do
              e = Content.first :id => published.id
              e.should == published
            end
          end
        end
      end

      should "have the correct indexes" do
        Content.create_revision(@revision)
        content_indexes = DB.indexes(:content)
        published_indexes = DB.indexes(Content.revision_table(@revision))
        # made slightly complex by the fact that the index names depend on the table names
        # (which are different)
        assert_same_elements published_indexes.values, content_indexes.values
      end


      context "incremental publishing" do
        setup do
          @initial_revision = 1
          @final_revision = 2
          Content.create_revision(@initial_revision)
          Content.delete_revision(@final_revision) rescue nil
          Content.delete_revision(@final_revision+1) rescue nil
        end

        teardown do
          begin
            Content.delete_revision(@initial_revision)
            Content.delete_revision(@final_revision)
            Content.delete_revision(@final_revision+1)
          rescue
          end
          DB.logger = nil
        end

        should "duplicate changes to only a single item" do
          editable1 = Content.first(:uid => '1.0')
          editable1.label.should be_nil
          editable1.label = "published"
          editable1.save
          editable2 = Content.first(:uid => '1.1')
          editable2.label = "unpublished"
          editable2.save
          editable2.reload
          Content.publish(@final_revision, [editable1.id])
          editable1.reload
          Content.with_revision(@final_revision) do
            published = Content.first :id => editable1.id
            unpublished = Content.first :id => editable2.id
            assert_content_equal(published, editable1)

            unpublished.should_not == editable2
          end
        end

        should "publish additions to contents of a page" do
          editable1 = Content.first(:uid => '0')
          new_content = Piece.new(:uid => "new")

          editable1.things << new_content
          editable1.save
          Content.publish(@final_revision, [editable1.id])
          new_content.reload
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            published2 = Content[new_content.id]
            assert_content_equal(published2, new_content)
            assert_content_equal(published1, editable1)
          end
        end

        should "publish deletions to contents of page" do
          editable1 = Content.first(:uid => '0')
          deleted = editable1.contents.first
          editable1.contents.first.destroy
          Content.publish(@final_revision, [editable1.id])
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            assert_content_equal(published1, editable1)
            Content[deleted.id].should be_nil
          end
        end

        should "not publish page additions" do
          editable1 = Content.first(:uid => '0')
          new_page = Page.new(:uid => "new")
          editable1.things << new_page
          editable1.save
          new_page.save
          Content.publish(@final_revision, [editable1.id])
          new_page.reload
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            published2 = Content[new_page.id]
            published2.should be_nil
            assert_content_equal(published1, editable1)
          end
        end

        should "not publish changes to existing pages unless explicitly asked" do
          editable1 = Content.first(:uid => '0')
          editable1.things << Piece.new(:uid => "added")
          editable1.save
          editable2 = Content.first(:uid => '0.0.0')
          new_content = Piece.new(:uid => "new")
          editable2.things << new_content
          editable2.save
          Content.publish(@final_revision, [editable1.id])
          editable1.reload
          editable2.reload
          new_content.reload
          Content.with_revision(@final_revision) do
            published1 = Content.first :id => editable1.id
            Content.first(:uid => "added").should_not be_nil
            published3 = Content.first :id => editable2.id
            assert_content_equal(published1, editable1)
            published3.should_not == editable2
            published3.uid.should_not == "new"
          end
          Content.publish(@final_revision+1, [editable2.id])
          editable1.reload
          editable2.reload
          new_content.reload
          Content.with_revision(@final_revision+1) do
            published1 = Content.first :id => editable1.id
            assert_content_equal(published1, editable1)
            published2 = Content.first :id => editable2.id
            assert_content_equal(published2, editable2)
            published3 = Content.first :id => editable2.contents.first.id
            # published3.should == editable2.contents.first
            assert_content_equal(published3, editable2.contents.first)
          end
        end

        should "insert an entry value into the parent of a newly added page when that page is published" do
          editable1 = Content.first(:uid => '0')
          new_page = Page.new(:uid => "new")
          editable1.things << new_page
          editable1.save
          new_page.save
          Content.publish(@final_revision, [new_page.id])
          new_page.reload
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            published2 = Content[new_page.id]
            assert_content_equal(published2, new_page)
            editable1.entry_store.should == published1.entry_store
          end
        end

        should "choose a sensible position for entry into the parent of a newly added page xxx" do
          editable1 = Content.first(:uid => '0')
          new_page1 = Page.new(:uid => "new1")
          new_page2 = Page.new(:uid => "new2")
          editable1.things << new_page1
          editable1.things << new_page2
          editable1.save
          new_page1.save
          new_page2.save
          Content.publish(@final_revision, [new_page2.id])
          new_page1.reload
          new_page2.reload
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            published2 = Content[new_page1.id]
            published3 = Content[new_page2.id]
            published2.should be_nil
            assert_content_equal(published3, new_page2)
            editable1.entry_store.reject { |e| e[0] == new_page1.id }.should == published1.entry_store
          end
        end

        should "not duplicate entries when publishing pages for the first time" do
          editable1 = Content.first(:uid => '0')
          new_page1 = Page.new(:uid => "new1")
          new_page2 = Page.new(:uid => "new2")
          editable1.things << new_page1
          editable1.things << new_page2
          editable1.save
          new_page1.save
          new_page2.save
          Content.publish(@final_revision, [editable1.id, new_page2.id])
          new_page1.reload
          new_page2.reload
          editable1.reload
          Content.with_revision(@final_revision) do
            published1 = Content[editable1.id]
            published2 = Content[new_page1.id]
            published3 = Content[new_page2.id]
            published2.should be_nil
            assert_content_equal(published3, new_page2)
            assert_content_equal(published1, editable1)
          end
        end

        should "remove deleted pages from the published content" do
          page = Page.first :uid => "0"
          piece = page.things.first
          child = piece.things.first
          page.things.first.destroy
          Content.publish(@final_revision, [page.id])

          Content.with_revision(@final_revision) do
            published_parent = Content[page.id]
            published_piece = Content[piece.id]
            published_page = Content[child.id]
            published_parent.should  == page.reload
            published_piece.should be_nil
            published_page.should be_nil
          end
        end
      end
    end


    context "publication timestamps" do
      setup do
        @revision = 1
        Content.delete_revision(@revision+1)
      end
      teardown do
        Content.delete_revision(@revision)
        Content.delete_revision(@revision+1)
      end

      should "set correct timestamps on first publish" do
        first = Content.first
        first.reload.first_published_at.should be_nil
        first.reload.last_published_at.should be_nil
        Content.publish(@revision)
        first.reload.first_published_at.to_i.should == @now.to_i
        first.reload.last_published_at.to_i.should == @now.to_i
        first.reload.first_published_revision.should == @revision
        Content.with_editable do
          first.reload.first_published_at.to_i.should == @now.to_i
          first.reload.last_published_at.to_i.should == @now.to_i
          first.reload.first_published_revision.should == @revision
        end
        Content.with_revision(@revision) do
          first.reload.first_published_at.to_i.should == @now.to_i
          first.reload.last_published_at.to_i.should == @now.to_i
          first.reload.first_published_revision.should == @revision
        end
      end

      should "set correct timestamps on later publishes" do
        first = Content.first
        first.first_published_at.should be_nil
        Content.publish(@revision)
        first.reload.first_published_at.to_i.should == @now.to_i
        c = Content.create
        c.first_published_at.should be_nil
        stub_time(@now + 100)
        Content.publish(@revision+1)
        first.reload.first_published_at.to_i.should == @now.to_i
        first.reload.last_published_at.to_i.should == @now.to_i + 100
        Content.with_editable do
          c = Content.first :id => c.id
          c.first_published_at.to_i.should == @now.to_i + 100
        end
        Content.with_revision(@revision+1) do
          c = Content.first :id => c.id
          c.first_published_at.to_i.should == @now.to_i + 100
        end
      end

      should "not set publishing date for items not published" do
        Content.publish(@revision)
        page = Content.first
        page.uid = "fish"
        page.save
        added = Content.create
        added.first_published_at.should be_nil
        Content.publish(@revision+1, [page])
        page.first_published_at.to_i.should == @now.to_i
        added.first_published_at.should be_nil
        added.last_published_at.should be_nil
      end

      should "not set publishing dates if exception raised in passed block" do
        Content.first.first_published_at.should be_nil
        begin
          Content.publish(@revision) do
            raise Exception
          end
        rescue Exception; end
        Content.first.first_published_at.should be_nil
      end

      should "delete revision tables if exception raised in passed block" do
        Content.revision_exists?(@revision).should be_false
        begin
          Content.publish(@revision) do
            Content.revision_exists?(@revision).should be_true
            Content.revision.should == @revision
            raise Exception
          end
        rescue Exception; end
        Content.revision_exists?(@revision).should be_false
      end

      should "always publish all if no previous revisions exist" do
        page = Content.first
        Content.filter(:first_published_at => nil).count.should == Content.count
        Content.publish(@revision, [page])
        Content.filter(:first_published_at => nil).count.should == 0
      end
    end
  end
end