require "spec_helper"

describe Work do
  # see lib/collectible_spec for collection-related tests

  it "creates a minimal work" do
    expect(create(:work)).to be_valid
  end

  context "when posted" do
    it "posts the first chapter" do
      work = create(:work)
      expect(work.first_chapter.posted).to eq true
    end
  end

  context "create_stat_counter" do
    it "creates a stat counter for that work id" do
      expect do
        @work = build(:work)
        @work.save!
      end.to change { StatCounter.all.count }
        .by(1)
      expect(StatCounter.where(work_id: @work.id)).to exist
    end
  end

  context "invalid title" do
    it { expect(build(:work, title: nil)).to be_invalid }

    it "cannot be shorter than ArchiveConfig.TITLE_MIN" do
      expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.TITLE_MIN - 1))).to be_invalid
    end

    it "cannot be longer than ArchiveConfig.TITLE_MAX" do
      expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.TITLE_MAX + 1))).to be_invalid
    end
  end

  context "clean_and_validate_title" do
    before do
      ArchiveConfig.TITLE_MIN = 10
    end
    it "strips out leading spaces from the title" do
      @work = create(:work, title: "    Has Leading Spaces")
      @work.reload
      expect(@work.title).to eq("Has Leading Spaces")
    end

    let(:too_short) { ArchiveConfig.TITLE_MIN - 1 }
    it "errors if the title without leading spaces is shorter than #{ArchiveConfig.TITLE_MIN}" do
      expect do
        @work = create(:work, title: "     #{too_short}")
        @work.reload
      end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Title must be at least #{ArchiveConfig.TITLE_MIN} characters long without leading spaces.")
    end

    # Reset the min characters in the title, so that the factory is valid
    after do
      ArchiveConfig.TITLE_MIN = 1
    end
  end

  context "invalid summary" do
    it "cannot be longer than ArchiveConfig.SUMMARY_MAX" do
      expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.SUMMARY_MAX + 1))).to be_invalid
    end
  end

  context "invalid notes" do
    it "cannot be longer than ArchiveConfig.NOTES_MAX" do
      expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.NOTES_MAX + 1))).to be_invalid
    end
  end

  context "invalid endnotes" do
    it "cannot be longer than ArchiveConfig.NOTES_MAX" do
      expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.NOTES_MAX + 1))).to be_invalid
    end
  end

  context "invalid language" do
    let(:deleted_language_id) do
      briefly_lived_language = create(:language)
      deleted_language_id = briefly_lived_language.id
      briefly_lived_language.destroy
      deleted_language_id
    end

    it "is valid with a supported language" do
      work = build(:work, language_id: Language.default.id)
      expect(work).to be_valid
    end

    it "is not valid with a language we don't support" do
      work = build(:work, language_id: deleted_language_id)

      expect(work).not_to be_valid
      expect(work.errors.messages[:base]).to include("Language cannot be blank.")
    end

    it "is not valid without a language" do
      work = build(:work, language_id: "")

      expect(work).not_to be_valid
      expect(work.errors.messages[:base]).to include("Language cannot be blank.")
    end
  end

  context "invalid rating" do
    it "cannot have more than one rating" do
      expect(build(:work, rating_string: "Not Rated, General Audiences")).to be_invalid
    end
  end

  context "validate authors" do
    let(:invalid_work) { build(:no_authors) }

    it "does not save if author is blank" do
      expect(invalid_work.save).to be_falsey
      expect(invalid_work.errors[:base]).to include "Work must have at least one creator."
    end
  end

  describe "reindexing" do
    let!(:work) { create(:work) }

    context "when draft status is changed" do
      it "enqueues tags for reindex" do
        expect do
          work.update!(posted: false)
        end.to add_to_reindex_queue(work.fandoms.last, :main)
      end
    end

    context "when hidden by an admin" do
      it "enqueues tags for reindex" do
        expect do
          work.update!(hidden_by_admin: true)
        end.to add_to_reindex_queue(work.fandoms.last, :main)
      end
    end

    context "when put in an unrevealed collection" do
      it "enqueues tags for reindex" do
        expect do
          work.update!(in_unrevealed_collection: true)
        end.to add_to_reindex_queue(work.fandoms.last, :main)
      end
    end
  end

  describe "#crossover" do
    it "is not crossover with one fandom" do
      fandom = create(:canonical_fandom, name: "nge")
      work = create(:work, fandoms: [fandom])
      expect(work.crossover).to be_falsy
    end

    it "is not crossover with one fandom and one of its synonyms" do
      rel = create(:canonical_fandom, name: "evanescence")
      syn = create(:fandom, name: "can't wake up (wake me up inside)", merger: rel)
      work = create(:work, fandoms: [rel, syn])
      expect(work.crossover).to be_falsy
    end

    it "is not crossover with multiple synonyms of one fandom" do
      rel = create(:canonical_fandom, name: "nge")
      syn1 = create(:fandom, name: "eva", merger: rel)
      syn2 = create(:fandom, name: "end of eva", merger: rel)
      work = create(:work, fandoms: [syn1, syn2])
      expect(work.crossover).to be_falsy
    end

    it "is not crossover with fandoms sharing a direct meta tag" do
      rel1 = create(:canonical_fandom, name: "rebuild")
      rel2 = create(:canonical_fandom, name: "campus apocalypse")
      meta_tag = create(:canonical_fandom, name: "nge")
      meta_tag.update_attribute(:sub_tag_string, "#{rel1.name},#{rel2.name}")
      rel1.reload
      rel2.reload

      work = create(:work, fandoms: [rel1, rel2])
      expect(work.crossover).to be_falsy
    end

    it "is not a crossover between fandoms sharing an indirect meta tag" do
      grand = create(:canonical_fandom)
      parent1 = create(:canonical_fandom)
      parent2 = create(:canonical_fandom)
      child1 = create(:canonical_fandom)
      child2 = create(:canonical_fandom)

      grand.update_attribute(:sub_tag_string, "#{parent1.name},#{parent2.name}")
      child1.update_attribute(:meta_tag_string, parent1.name)
      child2.update_attribute(:meta_tag_string, parent2.name)

      work = create(:work, fandom_string: "#{child1.name},#{child2.name}")
      expect(work.crossover).to be_falsey
    end

    it "is crossover with fandoms in different meta tag trees" do
      rel1 = create(:canonical_fandom, name: "rebuild again eventually")
      rel2 = create(:canonical_fandom, name: "evanescence")
      meta_tag = create(:canonical_fandom, name: "rebuild")
      meta_tag.update_attribute(:sub_tag_string, rel1.name)
      super_meta_tag = create(:canonical_fandom, name: "nge")
      super_meta_tag.update_attribute(:sub_tag_string, meta_tag.name)

      rel1.reload
      rel2.reload
      meta_tag.reload
      super_meta_tag.reload

      work = create(:work, fandoms: [rel1, rel2])
      expect(work.crossover).to be_truthy

      work = create(:work, fandoms: [meta_tag, super_meta_tag])
      expect(work.crossover).to be_falsy
    end

    it "is crossover with unrelated fandoms" do
      ships = [create(:canonical_fandom, name: "nge"), create(:canonical_fandom, name: "evanescence")]
      work = create(:work, fandoms: ships)
      expect(work.crossover).to be_truthy
    end

    it "is a crossover when missing meta-taggings" do
      f1 = create(:canonical_fandom)
      f2 = create(:canonical_fandom)
      f3 = create(:canonical_fandom)
      unrelated = create(:canonical_fandom)

      f2.update_attribute(:meta_tag_string, f3.name)
      f2.update_attribute(:sub_tag_string, f1.name)
      f1.meta_tags.delete(f3)

      work = create(:work, fandom_string: "#{f1.name}, #{unrelated.name}")
      expect(work.crossover).to be_truthy
    end

    context "when one tagged fandom has two unrelated meta tags" do
      let(:meta1) { create(:canonical_fandom) }
      let(:meta2) { create(:canonical_fandom) }
      let(:fandom) { create(:canonical_fandom) }

      before do
        fandom.update_attribute(:meta_tag_string, "#{meta1.name},#{meta2.name}")
      end

      it "is not a crossover with the fandom's synonym" do
        syn = create(:fandom, merger: fandom)
        work = create(:work, fandom_string: "#{fandom.name},#{syn.name}")
        expect(work.crossover).to be_falsey
      end

      it "is not a crossover with the fandom's meta tag" do
        work = create(:work, fandom_string: "#{fandom.name},#{meta1.name}")
        expect(work.crossover).to be_falsey
      end

      it "is not a crossover with another subtag of the fandom's meta tag" do
        sub = create(:canonical_fandom)
        sub.update_attribute(:meta_tag_string, meta1.name)
        work = create(:work, fandom_string: "#{fandom.name},#{sub.name}")
        expect(work.crossover).to be_falsey
      end

      it "is not a crossover with another fandom sharing the same two meta tags" do
        other = create(:canonical_fandom)
        other.update_attribute(:meta_tag_string, "#{meta1.name},#{meta2.name}")
        work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
        expect(work.crossover).to be_falsey
      end

      it "is not a crossover with another fandom with two unrelated meta tags, only one of which is shared by both fandoms" do
        # The tag fandom and the tag other share one meta tag (meta2), but
        # fandom has a meta tag meta1 completely unrelated to other, and other
        # has a meta tag meta3 completely unrelated to fandom. However, the
        # shared meta tag means that they are related, and thus a work tagged
        # with both is not a crossover.
        meta3 = create(:canonical_fandom)
        other = create(:canonical_fandom)
        other.update_attribute(:meta_tag_string, "#{meta2.name},#{meta3.name}")
        work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
        expect(work.crossover).to be_falsey
      end

      it "is a crossover with another fandom with two unrelated meta tags, when none of the meta tags are shared" do
        meta3 = create(:canonical_fandom)
        meta4 = create(:canonical_fandom)
        other = create(:canonical_fandom)
        other.update_attribute(:meta_tag_string, "#{meta3.name},#{meta4.name}")
        work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
        expect(work.crossover).to be_truthy
      end
    end
  end

  describe "#otp" do
    it "is not otp with no relationship" do
      work = create(:work)
      expect(work.relationships).to be_empty
      expect(work.otp).to be_falsy
    end

    it "is otp with only one relationship" do
      rel = create(:relationship, name: "asushin")
      work = create(:work, relationships: [rel])
      expect(work.otp).to be_truthy
    end

    it "is otp with one canonical relationship and one of its synonyms" do
      rel = create(:canonical_relationship, name: "kawoshin")
      syn = create(:relationship, name: "shinkawo", merger: rel)
      work = create(:work, relationships: [rel, syn])
      expect(work.otp).to be_truthy
    end

    it "is otp with multiple synonyms of the same canonical relationship" do
      rel = create(:canonical_relationship, name: "kawoshin")
      syn1 = create(:relationship, name: "shinkawo", merger: rel)
      syn2 = create(:relationship, name: "kaworu/shinji", merger: rel)
      work = create(:work, relationships: [syn1, syn2])
      expect(work.otp).to be_truthy
    end

    it "is not otp with unrelated relationships, one of which is canonical" do
      ships = [create(:relationship, name: "shinrei"), create(:canonical_relationship, name: "asurei")]
      work = create(:work, relationships: ships)
      expect(work.otp).to be_falsy
    end

    it "is not otp with unrelated relationships" do
      ships = [create(:relationship, name: "asushin"), create(:relationship, name: "asurei")]
      work = create(:work, relationships: ships)
      expect(work.otp).to be_falsy
    end

    it "is not otp with relationships sharing a meta tag" do
      rel1 = create(:canonical_relationship, name: "shinrei")
      rel2 = create(:canonical_relationship, name: "asurei")
      meta_tag = create(:canonical_relationship)
      meta_tag.update_attribute(:sub_tag_string, "#{rel1.name},#{rel2.name}")
      rel1.reload
      rel2.reload

      work = create(:work, relationships: [rel1, rel2])
      expect(work.otp).to be_falsy
    end
  end

  describe "#authors_to_sort_on" do
    let(:work) { build(:work) }

    context "when the pseuds start with special characters" do
      it "removes those characters" do
        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "-jolyne")]
        expect(work.authors_to_sort_on).to eq "jolyne"

        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "_hermes")]
        expect(work.authors_to_sort_on).to eq "hermes"
      end
    end

    context "when the pseuds start with numbers" do
      it "does not remove numbers" do
        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "007james")]
        expect(work.authors_to_sort_on).to eq "007james"
      end
    end

    context "when the work is anonymous" do
      it "returns Anonymous" do
        work.in_anon_collection = true
        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "stealthy")]
        expect(work.authors_to_sort_on).to eq "Anonymous"
      end
    end

    context "when the work has multiple pseuds" do
      it "sorts them like the byline then joins them with commas" do
        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "diavolo"), Pseud.new(name: "doppio")]
        expect(work.authors_to_sort_on).to eq "diavolo,  doppio"

        allow(work).to receive(:pseuds).and_return [Pseud.new(name: "Tiziano"), Pseud.new(name: "squalo")]
        expect(work.authors_to_sort_on).to eq "squalo,  tiziano"
      end
    end
  end

  describe "work_skin_allowed" do
    context "public skin"

    context "private skin" do
      before :each do
        @skin_author = create(:user)
        @second_author = create(:user)
        @private_skin = create(:work_skin, :private, author_id: @skin_author.id)
      end

      let(:work_author) { @skin_author }
      let(:work) { build(:custom_work_skin, authors: [work_author.pseuds.first], work_skin_id: @private_skin.id) }
      it "can be used by the work skin author" do
        expect(work.save).to be_truthy
      end

      let(:work) { build(:custom_work_skin, authors: [@second_author.pseuds.first], work_skin_id: @private_skin.id) }
      it "cannot be used by another user" do
        work.work_skin_allowed
        expect(work.errors[:base]).to include("You do not have permission to use that custom work stylesheet.")
      end
    end
  end

  describe "new gifts virtual attribute" do
    let(:recipient1) { create(:user).pseuds.first.name }
    let(:recipient2) { create(:user).pseuds.first.name }
    let(:recipient3) { create(:user).pseuds.first.name }

    let(:work) { build(:work) }

    before do
      work.recipients = "#{recipient1},#{recipient2}"
    end

    it "contains gifts for the same recipients when they are first added" do
      expect(work.new_gifts.collect(&:recipient)).to eq([recipient1, recipient2])
    end

    it "only contains a gift for the new recipient if replacing the previous recipients" do
      work.recipients = recipient3
      expect(work.new_gifts.collect(&:recipient)).to eq([recipient3])
    end

    it "simple assignment works" do
      work.recipients = recipient2
      expect(work.new_gifts.collect(&:recipient)).to eq([recipient2])
    end

    it "only contains one gift if the same recipient is entered twice" do
      work.recipients = "#{recipient2},#{recipient2}"
      expect(work.new_gifts.collect(&:recipient)).to eq([recipient2])
    end
  end

  describe "#find_by_url" do
    it "should find imported works with various URL formats" do
      %w[http://foo.com/bar.html
         http://foo.com/bar
         http://lj-site.com/bar/foo?color=blue
         https://www.lj-site.com/bar/foo?color=blue
         http://www.foo.com/bar https://www.foo.com/bar].each do |url|
        work = create(:work, imported_from_url: url)
        expect(Work.find_by_url(url)).to eq(work)
        work.destroy
      end
    end

    it "should not mix up imported works with similar URLs or significant query parameters" do
      {
        "http://foo.com/12345" => "http://foo.com/123",
        "http://efiction-site.com/viewstory.php?sid=123" => "http://efiction-site.com/viewstory.php?sid=456",
        "http://www.foo.com/i-am-something" => "http://foo.com/i-am-something/else"
      }.each do |import_url, find_url|
        work = create(:work, imported_from_url: import_url)
        expect(Work.find_by_url(find_url)).to_not eq(work)
        work.destroy
      end
    end

    it "should find works imported with irrelevant query parameters" do
      work = create(:work, imported_from_url: "http://lj-site.com/thing1?style=mine")
      expect(Work.find_by_url("http://lj-site.com/thing1?style=other")).to eq(work)
      work.destroy
    end

    it "finds works imported with HTTP protocol and irrelevant query parameters" do
      work = create(:work, imported_from_url: "http://lj-site.com/thing1?style=mine")
      expect(Work.find_by_url("https://lj-site.com/thing1?style=other")).to eq(work)
      work.destroy
    end

    it "finds works imported with HTTPS protocol and irrelevant query parameters" do
      work = create(:work, imported_from_url: "https://lj-site.com/thing1?style=mine")
      expect(Work.find_by_url("http://lj-site.com/thing1?style=other")).to eq(work)
      work.destroy
    end

    it "gets the work from cache when searching for an imported work by URL" do
      url = "http://lj-site.com/thing2"
      work = create(:work, imported_from_url: url)
      expect(Rails.cache.read(Work.find_by_url_cache_key(url))).to be_nil
      expect(Work.find_by_url(url)).to eq(work)
      expect(Rails.cache.read(Work.find_by_url_cache_key(url))).to eq(work)
      work.destroy
    end
  end

  describe "#update_complete_status" do
    it "marks a work complete when it's been completed" do
      work = create(:work, expected_number_of_chapters: 1)
      expect(work.complete).to be_truthy
    end

    it "marks a work incomplete when it's no longer completed" do
      work = create(:work, expected_number_of_chapters: 1)
      work.update!(expected_number_of_chapters: nil)
      expect(work.reload.complete).to be_falsey
    end
  end

  describe "#wip_length" do
    it "updating chapter count via wip_length sets a sensible expected_number_of_chapters value" do
      work = create(:work)
      create(:chapter, work: work)
      work.reload

      work.wip_length = 1
      expect(work.expected_number_of_chapters).to be_nil
      expect(work.wip_length).to eq("?")

      work.wip_length = 2
      expect(work.expected_number_of_chapters).to eq(2)
      expect(work.wip_length).to eq(work.expected_number_of_chapters)

      work.wip_length = 3
      expect(work.expected_number_of_chapters).to eq(3)
      expect(work.wip_length).to eq(work.expected_number_of_chapters)
    end
  end

  describe "#hide_spam" do
    before do
      @admin_setting = AdminSetting.first || AdminSetting.create
      @work = create(:work)
    end
    context "when the admin setting is enabled" do
      before do
        @admin_setting.update_attribute(:hide_spam, true)
      end
      it "automatically hides spam works and sends an email" do
        expect { @work.update!(spam: true) }
          .to change { ActionMailer::Base.deliveries.count }
          .by(1)
        expect(@work.reload.hidden_by_admin).to be_truthy
        expect(ActionMailer::Base.deliveries.last.subject).to eq("[AO3] Your work was hidden as spam")
      end
    end
    context "when the admin setting is disabled" do
      before do
        @admin_setting.update_attribute(:hide_spam, false)
      end
      it "does not automatically hide spam works and does not send an email" do
        expect { @work.update!(spam: true) }
          .not_to change { ActionMailer::Base.deliveries.count }
        expect(@work.reload.hidden_by_admin).to be_falsey
      end
    end
  end

  describe "co-creator permissions" do
    let(:creator) { create(:user) }
    let(:co_creator) { create(:user) }
    let(:no_co_creator) { create(:user) }

    before do
      # In order to enable co-creator checks (instead of just having everything
      # be automatically approved), we need to make sure that User.current_user
      # is not nil.
      User.current_user = creator
      co_creator.preference.update!(allow_cocreator: true)
      no_co_creator.preference.update!(allow_cocreator: false)
    end

    it "allows normal users to invite others as chapter co-creators" do
      work = create(:work, authors: creator.pseuds)
      work.author_attributes = { byline: co_creator.login }
      expect(work).to be_valid
      expect(work.save).to be_truthy
      expect(work.user_has_creator_invite?(co_creator)).to be_truthy
    end

    it "doesn't allow users to invite others who disallow co-creators" do
      work = create(:work, authors: creator.pseuds)
      work.author_attributes = { byline: no_co_creator.login }
      expect(work).to be_invalid
      expect(work.save).to be_falsey
      expect(work.user_has_creator_invite?(no_co_creator)).to be_falsey
    end
  end

  describe "#remove_author" do
    let(:to_remove) { create(:user) }
    let(:other) { create(:user) }

    context "when all the pseuds on the work are owned by one user" do
      let(:pseud1) { create(:pseud, user: to_remove) }
      let(:pseud2) { create(:pseud, user: to_remove) }
      let(:pseud3) { create(:pseud, user: to_remove) }

      let!(:work) do
        create(:work, authors: [pseud1, pseud2, pseud3])
      end

      let!(:solo1) { create(:chapter, work: work, authors: [pseud1]) }
      let!(:solo2) { create(:chapter, work: work, authors: [pseud2]) }
      let!(:solo3) { create(:chapter, work: work, authors: [pseud3]) }

      before { work.reload }

      it "raises an error" do
        expect { work.remove_author(to_remove) }
          .to raise_exception(
            "Sorry, we can't remove all creators of a work."
          )
      end
    end

    context "when the work has a chapter whose sole creator is being removed" do
      let!(:work) do
        create(:work, authors: [to_remove.default_pseud, other.default_pseud])
      end

      let!(:solo_chapter) do
        create(:chapter, work: work, authors: [to_remove.default_pseud])
      end

      # Make sure we see the newest chapter:
      before { work.reload }

      it "sets the chapter's creators equal to the work's" do
        work.remove_author(to_remove)
        expect(work.pseuds.reload).to contain_exactly(other.default_pseud)
        expect(solo_chapter.pseuds.reload).to contain_exactly(other.default_pseud)
      end
    end
  end

  describe "#destroy" do
    let(:work) { create(:work) }

    it "does not save an original creator record" do
      expect { work.destroy }
        .not_to change { WorkOriginalCreator.count }
    end

    context "when an original creator exists" do
      let!(:original_creator) { create(:work_original_creator, work: work) }

      it "deletes the original creator" do
        work.destroy
        expect { original_creator.reload }
          .to raise_error(ActiveRecord::RecordNotFound)
      end
    end
  end

  describe "#allow_collection_invitation?" do
    let(:creator1) { create(:user) }
    let(:creator2) { create(:user) }
    let(:work) { create(:work, authors: [creator1.default_pseud, creator2.default_pseud]) }

    context "when all creators allow collection invitations" do
      before do
        creator1.preference.update!(allow_collection_invitation: true)
        creator2.preference.update!(allow_collection_invitation: true)
      end

      it "returns true" do
        expect(work.allow_collection_invitation?).to be true
      end
    end

    context "when all creators disallow collection invitations" do
      it "returns false" do
        expect(work.allow_collection_invitation?).to be false
      end
    end

    context "when creators have a mix of collection invitation preferences" do
      before do
        creator1.preference.update!(allow_collection_invitation: true)
      end

      it "returns true" do
        expect(work.allow_collection_invitation?).to be true
      end
    end
  end
end
