# encoding: UTF-8

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


describe "Revisions" do

  Revision = Spontaneous::Publishing::Revision

  start do
    site = setup_site
    let(:site) { site }

    Content.delete

    class Page
      field :title, :string, :default => "New Page"
      box :things
    end
    class 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

    Revision.history_dataset(Content).delete
    Revision.archive_dataset(Content).delete
    Revision.delete_all(Content)

    let(:root) { root }
  end

  finish do
    Object.send(:remove_const, :Page) rescue nil
    Object.send(:remove_const, :Piece) rescue nil
    Content.delete
    teardown_site
  end

  before do
    @now = Time.now
    stub_time(@now)
  end

  after do
    Revision.delete_all(Content)
  end

  describe "data sources" do

    it "have the right names" do
      Content.revision_table(23).must_equal :'__r00023_content'
      Content.revision_table(nil).must_equal :'content'
    end

    it "be recognisable" do
      refute Content.revision_table?('content')
      assert Content.revision_table?('__r00023_content')
      refute Content.revision_table?('__r00023_not')
      refute Content.revision_table?('subscribers')
    end

    it "be switchable within blocks" do
      Content.with_revision(23) do
        Content.revision.must_equal 23
        Content.mapper.current_revision.must_equal 23
      end
      Content.mapper.current_revision.must_be_nil
    end

    it "know which revision is active" do
      Content.with_revision(23) do
        Content.revision.must_equal 23
      end
    end

    it "understand with_editable" do
      Content.with_revision(23) do
        Content.mapper.current_revision.must_equal 23
        Content.with_editable do
          Content.mapper.current_revision.must_be_nil
        end
      end
    end

    it "understand with_published" do
      site.stubs(:published_revision).returns(99)
      Content.with_published(site) do
        Content.mapper.current_revision.must_equal 99
      end
    end

    it "be stackable" do
      Content.with_revision(23) do
        Content.mapper.current_revision.must_equal 23
        Content.with_revision(24) do
          Content.mapper.current_revision.must_equal 24
        end
      end
    end

    it "reset datasource after an exception" do
      begin
        Content.with_revision(23) do
          Content.mapper.current_revision.must_equal 23
          raise "Fail"
        end
      rescue Exception
      end
      Content.mapper.current_revision.must_be_nil
    end

    it "read revision from the environment if present" do
      ENV["SPOT_REVISION"] = '1001'
      Content.with_published(site) do
        Content.mapper.current_revision.must_equal 1001
      end
      ENV.delete("SPOT_REVISION")
    end

    describe "subclasses" do
      before do
        class ::Subclass < Page; end
      end

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

      it "set all subclasses to use the same dataset" do
        Content.with_revision(23) do
          Subclass.revision.must_equal 23
          Subclass.mapper.current_revision.must_equal 23
          # piece wasn't loaded until this point
          Piece.mapper.current_revision.must_equal 23
        end
      end
    end
  end

  describe "content revisions" do
    before do
      @revision = 1
    end

    after do
      # Revision.delete_all(Content)
    end

    it "be testable for existance" do
      refute Content.revision_exists?(@revision)
      Revision.create(Content, @revision)
      assert Revision.exists?(Content, @revision)
    end

    it "not be deletable if their revision is nil" do
      Revision.delete(Content, nil)
      assert Content.db.table_exists?(:content)
    end

    it "be deletable en masse" do
      revisions = (1..10).to_a
      tables = revisions.map { |i| Content.revision_table(i).to_sym }

      revisions.each do |r|
        Content.history_dataset.insert(:revision => r, :uid => "revision-#{r}")
        Content.archive_dataset.insert(:revision => r, :uid => "archive-#{r}")
      end

      Content.history_dataset.count.must_equal 10
      Content.archive_dataset.count.must_equal 10

      tables.each do |t|
        DB.create_table(t){Integer :id} rescue nil
      end

      tables.each do |t|
        assert DB.tables.include?(t)
      end

      Revision.delete_all(Content)

      tables.each do |t|
        refute DB.tables.include?(t)
      end

      Content.history_dataset.count.must_equal 0
      Content.archive_dataset.count.must_equal 0
    end

    it "be creatable from current content" do
      refute DB.tables.include?(Content.revision_table(@revision).to_sym)
      Revision.create(Content, @revision)
      assert DB.tables.include?(Content.revision_table(@revision).to_sym)
      count = Content.count
      Content.with_revision(@revision) do
        Content.count.must_equal count
        Content.all.each do |published|
          published.revision.must_equal @revision
          Content.with_editable do
            e = Content[published.id]
            assert_content_equal(e, published, :revision)
            e.revision.must_be_nil
          end
        end
      end
      Content.history_dataset(@revision).count.must_equal count
      Content.history_dataset.select(:revision).group(:revision).all.must_equal [{:revision => 1}]
      Content.archive_dataset(@revision).count.must_equal count
      Content.archive_dataset.select(:revision).group(:revision).all.must_equal [{:revision => 1}]
    end

    it "adds an index for the primary key" do
      Revision.create(Content, @revision)
      pk = Content.primary_key
      published_indexes = DB.indexes(Content.revision_table(@revision))
      pk_index = published_indexes.detect { |name, index| index[:columns] == [pk] }
      pk_index.wont_equal nil
      name, options = pk_index
      options[:unique].must_equal true
    end

    it "have the correct indexes" do
      Revision.create(Content, @revision)
      content_indexes = DB.indexes(:content)
      # filter out the pk index as the DB::indexes call doesn't include it
      published_indexes = DB.indexes(Content.revision_table(@revision)).reject { |name, index| index[:columns] == [:id] }
      # made slightly complex by the fact that the index names depend on the table names
      # (which are different)
      assert_has_elements published_indexes.values, content_indexes.values
    end

    it "only be kept until a new revision is available" do
      (0..2).each do |r|
        Revision.create(Content, @revision+r)
        Content.history_dataset(@revision+r).count.must_equal 15
        Content.archive_dataset(@revision+r).count.must_equal 15
      end
      Content.revision_tables.must_equal [:__r00001_content, :__r00002_content, :__r00003_content]
      Revision.cleanup(Content, @revision+2, 2)
      Content.revision_tables.must_equal [:__r00003_content]
      Content.history_dataset(@revision).count.must_equal 0
      Content.archive_dataset(@revision).count.must_equal 15
      Content.history_dataset(@revision+2).count.must_equal 15
    end


    describe "incremental publishing" do
      before do
        @initial_revision = 1
        @final_revision = 2
        Revision.create(Content, @initial_revision)
      end

      it "duplicate changes to only a single item" do
        editable1 = Content.first(:uid => '1.0')
        editable1.label.must_be_nil
        editable1.label = "published"
        editable1.save
        editable2 = Content.first(:uid => '1.1')
        editable2.label = "unpublished"
        editable2.save
        editable2.reload
        Revision.patch(Content, @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, :revision)
          assert_content_unequal(unpublished, editable2, :revision)
        end
      end

      it "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
        Revision.patch(Content, @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, :revision)
          assert_content_equal(published1, editable1, :revision)
        end
      end

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

      it "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
        Revision.patch(Content, @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.must_be_nil
          assert_content_equal(published1, editable1, :revision)
        end
      end

      it "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
        Revision.patch(Content, @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").wont_be_nil
          published3 = Content.first :id => editable2.id
          assert_content_equal(published1, editable1, :revision)
          assert_content_unequal(published3, editable2, :revision)
          published3.uid.wont_equal "new"
        end
        Revision.patch(Content, @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, :revision)
          published2 = Content.first :id => editable2.id
          assert_content_equal(published2, editable2, :revision)
          published3 = Content.first :id => editable2.contents.first.id
          # published3.must_equal editable2.contents.first
          assert_content_equal(published3, editable2.contents.first, :revision)
        end
      end

      it "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
        Revision.patch(Content, @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, :revision)
          editable1.entry_store.must_equal published1.entry_store
        end
      end

      it "choose a sensible position for entry into the parent of a newly added page" 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
        Revision.patch(Content, @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.must_be_nil
          assert_content_equal(published3, new_page2, :revision)
          editable1.entry_store.reject { |e| e[0] == new_page1.id }.must_equal published1.entry_store
        end
      end

      it "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
        Revision.patch(Content, @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.must_be_nil
          assert_content_equal(published3, new_page2, :revision)
          assert_content_equal(published1, editable1, :revision)
        end
      end

      it "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
        Revision.patch(Content, @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.must_equal page.reload
          published_piece.must_be_nil
          published_page.must_be_nil
        end
      end
    end
  end

  describe "reverting changes" do
    it "reverts page fields"
    it "reverts piece fields"
    it "removes added pieces"
    it "restores removed pieces"
    it "restores deleted pages"
  end

  describe "content hashes" do
    before do
      @revision = 1
      Revision.delete(Content, @revision+1)
    end
    it "starts with a published_content_hash of nil" do
      first = Content.first
      first.published_content_hash.must_equal nil
      first.content_hash.wont_equal nil
      first.content_hash.length.must_equal 32
    end

    it "sets the published_content_hash on first publish" do
      first = Content.first
      content_hash = first.content_hash
      first.reload.published_content_hash.must_be_nil
      first.content_hash_changed.must_equal true
      Revision.create(Content, @revision)
      first.reload.published_content_hash.must_equal  content_hash
      first.content_hash_changed.must_equal false
      Content.with_editable do
        first.reload.published_content_hash.must_equal content_hash
        first.content_hash_changed.must_equal false
      end
      Content.with_revision(@revision) do
        first.reload.published_content_hash.must_equal content_hash
      end
    end

    it "updates the published_content_hash on later publishes" do
      first = Page.first
      content_hash = first.content_hash
      Revision.create(Content, @revision)
      first.reload.published_content_hash.must_equal content_hash
      first.update(title: "not the same")
      content_hash2 = first.content_hash
      content_hash2.wont_equal content_hash
      added = Page.create
      added.published_content_hash.must_be_nil
      added.content_hash_changed.must_equal true
      content_hash_added = added.content_hash
      Revision.create(Content, @revision+1)
      first.reload.published_content_hash.must_equal content_hash2
      first.content_hash_changed.must_equal false
      Content.with_editable do
        c = Page.first :id => added.id
        c.published_content_hash.must_equal content_hash_added
        c.content_hash_changed.must_equal false
        c = Page.first :id => first.id
        c.published_content_hash.must_equal content_hash2
        c.content_hash_changed.must_equal false
      end
      Content.with_revision(@revision+1) do
        c = Page.first :id => added.id
        c.published_content_hash.must_equal content_hash_added
        c.content_hash_changed.must_equal false
        c = Page.first :id => first.id
        c.published_content_hash.must_equal content_hash2
        c.content_hash_changed.must_equal false
      end
    end

    it "doesn't set published_content_hash for items not published" do
      Revision.create(Content, @revision)
      page = Page.first
      page.title = "changed"
      page.save
      page.content_hash_changed.must_equal true
      content_hash = page.content_hash
      added = Page.create
      added.published_content_hash.must_be_nil
      added.reload.content_hash_changed.must_equal true
      Revision.patch(Content, @revision+1, [page])
      page.reload.published_content_hash.must_equal content_hash
      page.content_hash_changed.must_equal false
      added.reload.published_content_hash.must_be_nil
      added.reload.content_hash_changed.must_equal true
    end

    it "doesn't set published_content_hash if exception raised in passed block" do
      Content.first.published_content_hash.must_be_nil
      begin
        Revision.create(Content, @revision) do
          raise "Fail"
        end
      rescue Exception; end
      Content.first.published_content_hash.must_be_nil
    end
  end

  describe "publication timestamps" do
    before do
      @revision = 1
      Revision.delete(Content, @revision+1)
    end

    it "set correct timestamps on first publish" do
      first = Content.first
      first.reload.first_published_at.must_be_nil
      first.reload.last_published_at.must_be_nil
      Revision.create(Content, @revision)
      first.reload.first_published_at.to_i.must_equal @now.to_i
      first.reload.last_published_at.to_i.must_equal @now.to_i
      first.reload.first_published_revision.must_equal @revision
      Content.with_editable do
        first.reload.first_published_at.to_i.must_equal @now.to_i
        first.reload.last_published_at.to_i.must_equal @now.to_i
        first.reload.first_published_revision.must_equal @revision
      end
      Content.with_revision(@revision) do
        first.reload.first_published_at.to_i.must_equal @now.to_i
        first.reload.last_published_at.to_i.must_equal @now.to_i
        first.reload.first_published_revision.must_equal @revision
      end
    end

    it "set correct timestamps on later publishes" do
      first = Content.first
      first.first_published_at.must_be_nil
      Revision.create(Content, @revision)
      first.reload.first_published_at.to_i.must_equal @now.to_i
      c = Page.create
      c.first_published_at.must_be_nil
      stub_time(@now + 100)
      Revision.create(Content, @revision+1)
      first.reload.first_published_at.to_i.must_equal @now.to_i
      first.reload.last_published_at.to_i.must_equal @now.to_i + 100
      Content.with_editable do
        c = Content.first :id => c.id
        c.first_published_at.to_i.must_equal @now.to_i + 100
      end
      Content.with_revision(@revision+1) do
        c = Content.first :id => c.id
        c.first_published_at.to_i.must_equal @now.to_i + 100
      end
    end

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

    it "not set publishing dates if exception raised in passed block" do
      Content.first.first_published_at.must_be_nil
      begin
        Revision.create(Content, @revision) do
          raise "Fail"
        end
      rescue Exception; end
      Content.first.first_published_at.must_be_nil
    end

    it "delete revision tables if exception raised in passed block" do
      refute Revision.exists?(Content, @revision)
      begin
        Revision.create(Content, @revision) do
          assert Revision.exists?(Content, @revision)
          Content.revision.must_equal @revision
          raise "Fail"
        end
      rescue Exception; end
      refute Revision.exists?(Content, @revision)
    end

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