require "rails_helper"

RSpec.describe AnalyticsService, type: :service do
  let(:user) { create(:user) }
  let(:organization) { create(:organization) }
  let(:article) { create(:article, user: user, published: true) }

  before do
    # We use Zonebie to fortify the code and spot time zone related problems but in this
    # particular case it can make these tests fail because,
    # for example, "2019-07-21T23:57:12-12:00" is the 22th in UTC, and since
    # the code delegates to DATE() in PostgreSQL which runs on UTC without time zone
    # info, there's no easy way for these tests to work correctly in a time zone on
    # the day line like "International Date Line West".
    # For this reason data created on "2019-07-21T23:57:12-12:00" will appear on the 22nd in the DB
    # and hence never be selected by the Analytics engine
    # In the meantime for a lack of a better solution, we force this tests to run at midday in UTC
    Timecop.freeze("2019-04-01T12:00:00Z")
  end

  after { Timecop.return }

  def format_date(datetime)
    # PostgreSQL DATE(..) function uses UTC.
    datetime.utc.to_date.iso8601
  end

  describe "initialization" do
    it "raises an error if start date is invalid" do
      expect { described_class.new(user, start_date: "2000-") }.to raise_error(ArgumentError)
    end

    it "raises an error if end date is invalid" do
      expect { described_class.new(user, end_date: "2000-") }.to raise_error(ArgumentError)
    end

    it "raises an error if an article does not belong to the user" do
      other_user = create(:user)
      article = create(:article, user: other_user)
      expect { described_class.new(user, article_id: article.id) }.to raise_error(ArgumentError)
    end
  end

  describe "#totals" do
    let(:analytics_service) { described_class.new(user) }

    it "returns totals stats for comments, reactions, follows and page views" do
      totals = analytics_service.totals
      expect(totals.keys.to_set).to eq(%i[comments reactions follows page_views].to_set)
    end

    it "returns totals stats for an org" do
      totals = described_class.new(organization).totals
      expect(totals.keys.to_set).to eq(%i[comments reactions follows page_views].to_set)
    end

    describe "comments stats" do
      it "returns stats" do
        stats = analytics_service.totals[:comments]
        expect(stats.keys).to eq(%i[total])
      end

      it "returns the total number of comments" do
        create(:comment, commentable: article, score: 1)
        expected_stats = { total: 1 }
        expect(analytics_service.totals[:comments]).to eq(expected_stats)
      end

      it "returns zero as total if there are no scored comments" do
        create(:comment, commentable: article, score: 0)
        expected_stats = { total: 0 }
        expect(analytics_service.totals[:comments]).to eq(expected_stats)
      end
    end

    describe "reactions stats" do
      before { Reaction.where(reactable: article).delete_all }

      it "returns stats" do
        stats = described_class.new(user).totals[:reactions]
        expect(stats.keys).to eq(%i[total like readinglist unicorn])
      end

      it "returns the total number of reactions" do
        create(:reaction, reactable: article)
        expect(analytics_service.totals[:reactions][:total]).to eq(1)
      end

      it "returns zero as total if there are no public category reactions" do
        reaction = create(:reaction, reactable: article)
        reaction.update_columns(category: "thumbsdown")
        expect(analytics_service.totals[:reactions][:total]).to eq(0)
      end

      it "returns the number of like reactions" do
        create(:reaction, reactable: article, category: :like)
        expect(analytics_service.totals[:reactions][:like]).to eq(1)
      end

      it "returns zero as the number of like reactions" do
        create(:reaction, reactable: article, category: :unicorn)
        expect(analytics_service.totals[:reactions][:like]).to eq(0)
      end

      it "returns the number of readinglist reactions" do
        create(:reaction, reactable: article, category: :readinglist)
        expect(analytics_service.totals[:reactions][:readinglist]).to eq(1)
      end

      it "returns zero as the number of readinglist reactions" do
        create(:reaction, reactable: article, category: :unicorn)
        expect(analytics_service.totals[:reactions][:readinglist]).to eq(0)
      end

      it "returns the number of unicorn reactions" do
        create(:reaction, reactable: article, category: :unicorn)
        expect(analytics_service.totals[:reactions][:unicorn]).to eq(1)
      end

      it "returns zero as the number of unicorn reactions" do
        create(:reaction, reactable: article, category: :like)
        expect(analytics_service.totals[:reactions][:unicorn]).to eq(0)
      end
    end

    describe "follows stats" do
      it "returns stats" do
        stats = analytics_service.totals[:follows]
        expect(stats.keys).to eq(%i[total])
      end

      it "returns the total number of follows" do
        create(:follow, followable: user)
        expected_stats = { total: 1 }
        expect(analytics_service.totals[:follows]).to eq(expected_stats)
      end

      it "returns zero as total if there are no follows" do
        expected_stats = { total: 0 }
        expect(analytics_service.totals[:follows]).to eq(expected_stats)
      end
    end

    describe "page views stats" do
      it "returns stats" do
        stats = analytics_service.totals[:page_views]
        expect(stats.keys).to eq(%i[total average_read_time_in_seconds total_read_time_in_seconds])
      end

      it "returns the total number of page views from page_views_count" do
        article.update_columns(page_views_count: 1)
        expect(analytics_service.totals[:page_views][:total]).to eq(1)
      end

      it "returns zero as total if there are no page views" do
        expect(analytics_service.totals[:page_views][:total]).to eq(0)
      end

      it "returns the average read time in seconds" do
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
        expect(analytics_service.totals[:page_views][:average_read_time_in_seconds]).to eq(30)
      end

      it "returns zero as the average read time in seconds without views" do
        expect(analytics_service.totals[:page_views][:average_read_time_in_seconds]).to eq(0)
      end

      it "returns the total read time in seconds" do
        article.update_columns(page_views_count: 1)
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
        # average read time * total_views
        expect(analytics_service.totals[:page_views][:total_read_time_in_seconds]).to eq(30)
      end

      it "returns zero as the total read time in seconds with no page views" do
        expect(analytics_service.totals[:page_views][:total_read_time_in_seconds]).to eq(0)
      end
    end
  end

  describe "#grouped_by_day" do
    it "returns stats grouped by day" do
      stats = described_class.new(
        user, start_date: "2019-04-01", end_date: "2019-04-04"
      ).grouped_by_day
      expect(stats.keys).to eq(%w[2019-04-01 2019-04-02 2019-04-03 2019-04-04])
    end

    it "returns stats for comments, reactions, follows and page views for a specific day" do
      stats = described_class.new(user, start_date: "2019-04-01").grouped_by_day
      expect(stats["2019-04-01"].keys.to_set).to eq(%i[comments reactions page_views follows].to_set)
    end

    it "returns stats for an org" do
      stats = described_class.new(
        organization, start_date: "2019-04-01", end_date: "2019-04-04"
      ).grouped_by_day
      expect(stats.keys).to eq(%w[2019-04-01 2019-04-02 2019-04-03 2019-04-04])
      expect(stats["2019-04-01"].keys.to_set).to eq(%i[comments reactions page_views follows].to_set)
    end

    describe "comments stats on a specific day" do
      it "returns stats" do
        analytics_service = described_class.new(user, start_date: "2019-04-01", end_date: "2019-04-04")
        stats = analytics_service.grouped_by_day["2019-04-01"][:comments]
        expect(stats.keys).to eq(%i[total])
      end

      it "returns the total number of comments" do
        comment = create(:comment, commentable: article, score: 1)
        expected_stats = { total: 1 }
        date = format_date(comment.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:comments]).to eq(expected_stats)
      end

      it "returns zero as total if there are no scored comments" do
        comment = create(:comment, commentable: article, score: 0)
        expected_stats = { total: 0 }
        date = format_date(comment.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:comments]).to eq(expected_stats)
      end
    end

    describe "reactions stats on a specific day" do
      before { Reaction.where(reactable: article).delete_all }

      it "returns stats" do
        analytics_service = described_class.new(user, start_date: "2019-04-01")
        stats = analytics_service.grouped_by_day["2019-04-01"][:reactions]
        expect(stats.keys).to eq(%i[total like readinglist unicorn])
      end

      it "returns the total number of reactions" do
        reaction = create(:reaction, reactable: article)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:total]).to eq(1)
      end

      it "returns zero as total if there are no public category reactions" do
        reaction = create(:reaction, reactable: article)
        reaction.update_columns(category: "thumbsdown")
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:total]).to eq(0)
      end

      it "returns the number of like reactions" do
        reaction = create(:reaction, reactable: article, category: :like)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:like]).to eq(1)
      end

      it "returns zero as the number of like reactions" do
        reaction = create(:reaction, reactable: article, category: :unicorn)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:like]).to eq(0)
      end

      it "returns the number of readinglist reactions" do
        reaction = create(:reaction, reactable: article, category: :readinglist)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:readinglist]).to eq(1)
      end

      it "returns zero as the number of readinglist reactions" do
        reaction = create(:reaction, reactable: article, category: :unicorn)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:readinglist]).to eq(0)
      end

      it "returns the number of unicorn reactions" do
        reaction = create(:reaction, reactable: article, category: :unicorn)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:unicorn]).to eq(1)
      end

      it "returns zero as the number of unicorn reactions" do
        reaction = create(:reaction, reactable: article, category: :like)
        date = format_date(reaction.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:reactions][:unicorn]).to eq(0)
      end
    end

    describe "follows stats for a specific day" do
      it "returns stats" do
        analytics_service = described_class.new(user, start_date: "2019-04-01")
        stats = analytics_service.grouped_by_day["2019-04-01"][:follows]
        expect(stats.keys).to eq(%i[total])
      end

      it "returns the total number of follows" do
        follow = create(:follow, followable: user)
        date = format_date(follow.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expected_stats = { total: 1 }
        expect(analytics_service.grouped_by_day[date][:follows]).to eq(expected_stats)
      end

      it "returns zero as total if there are no follows" do
        date = format_date(Time.current)
        analytics_service = described_class.new(user, start_date: date)
        expected_stats = { total: 0 }
        expect(analytics_service.grouped_by_day[date][:follows]).to eq(expected_stats)
      end
    end

    describe "page views stats for a specific day" do
      before { PageView.where(article: article).delete_all }

      it "returns stats" do
        analytics_service = described_class.new(user, start_date: "2019-04-01")
        stats = analytics_service.grouped_by_day["2019-04-01"][:page_views]
        expect(stats.keys).to eq(%i[total average_read_time_in_seconds total_read_time_in_seconds])
      end

      it "returns the total number of page views from counts_for_number_of_views" do
        pv = create(:page_view, user: user, article: article, counts_for_number_of_views: 5)
        date = format_date(pv.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(5)
      end

      it "returns zero as total if there are no page views" do
        date = format_date(Time.current)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(0)
      end

      it "returns the average read time in seconds" do
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
        pv = create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
        date = format_date(pv.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][
          :page_views][:average_read_time_in_seconds]).to eq(30)
      end

      it "returns zero as the average read time in seconds without views" do
        date = format_date(Time.current)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][
          :page_views][:average_read_time_in_seconds]).to eq(0)
      end

      it "returns the total read time in seconds" do
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
        create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
        date = format_date(article.created_at)
        analytics_service = described_class.new(user, start_date: date)
        # average read time (30) * total_views (2)
        expect(analytics_service.grouped_by_day[date][
          :page_views][:total_read_time_in_seconds]).to eq(60)
      end

      it "returns zero as the total read time in seconds with no page views" do
        date = format_date(Time.current)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][
          :page_views][:total_read_time_in_seconds]).to eq(0)
      end

      it "works correctly if the page views contain nil in time_tracked_in_seconds" do
        create(:page_view, user: user, article: article, time_tracked_in_seconds: nil)
        create(:page_view, user: user, article: article, time_tracked_in_seconds: nil)
        date = format_date(article.created_at)
        analytics_service = described_class.new(user, start_date: date)
        expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(2)
        expect(analytics_service.grouped_by_day[date][:page_views][:average_read_time_in_seconds]).to eq(0)
        expect(analytics_service.grouped_by_day[date][:page_views][:total_read_time_in_seconds]).to eq(0)
      end
    end
  end

  describe "#referrers" do
    let(:analytics_service) { described_class.new(user) }

    context "when working on domains" do
      before { PageView.where(article: article).delete_all }

      it "returns unique domains with a count" do
        url = Faker::Internet.url
        create(:page_view, user: user, article: article, referrer: url)
        create(:page_view, user: user, article: article, referrer: url)
        domains = analytics_service.referrers[:domains]
        expect(domains.first).to eq(domain: Addressable::URI.parse(url).domain, count: 2)
      end

      it "returns unique domains with a count when there is a date range" do
        url = Faker::Internet.url
        create(:page_view, user: user, article: article, referrer: url)
        create(:page_view, user: user, article: article, referrer: url)
        date = format_date(article.created_at)
        analytics_service = described_class.new(user, start_date: date)
        domains = analytics_service.referrers[:domains]
        expect(domains.first).to eq(domain: Addressable::URI.parse(url).domain, count: 2)
      end

      it "returns nothing if there is no data" do
        expect(analytics_service.referrers[:domains]).to be_empty
      end

      it "returns the domains ordered by number of views" do
        other_url = Faker::Internet.url
        create_list(:page_view, 2, user: user, article: article, referrer: other_url)
        top_url = Faker::Internet.url
        create_list(:page_view, 3, user: user, article: article, referrer: top_url, counts_for_number_of_views: 10)

        expected_result = [
          { domain: Addressable::URI.parse(top_url).domain, count: 30 },
          { domain: Addressable::URI.parse(other_url).domain, count: 2 },
        ]
        expect(analytics_service.referrers[:domains]).to eq(expected_result)
      end

      it "returns 20 domains at most by default" do
        21.times do |n|
          create(:page_view, user: user, article: article, referrer: "http://fakeurl#{n}.com")
        end
        expect(analytics_service.referrers[:domains].size).to eq(20)
      end

      it "returns the most visited domain if asked for only one result" do
        top_url = Faker::Internet.url
        create_list(:page_view, 3, user: user, article: article, referrer: top_url)
        # Faker url's generate colliding domain names about 1 to two times in 10,000
        # Faker domains never end in ".dev" so this is a safe choice
        other_url = Faker::Internet.url(host: "forem.dev")
        create_list(:page_view, 2, user: user, article: article, referrer: other_url)

        top_domain = Addressable::URI.parse(top_url).domain
        result = analytics_service.referrers(top: 1)[:domains]
        expect(result).to eq([{ domain: top_domain, count: 3 }])
      end
    end
  end
end
