# frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

describe Stats do
  context Stats::Counter do
    def check_stats_with_matchers(c, empty, size, max, min, sum, mean, var, stddev, histogram)
      expect(c.empty?).to empty
      expect(c.size).to size
      expect(c.count).to size
      expect(c.max).to max
      expect(c.min).to min
      expect(c.sum).to sum
      expect(c.total).to sum
      expect(c.mean).to mean
      expect(c.avg).to mean
      expect(c.var).to var
      expect(c.variance).to var
      expect(c.stddev).to stddev
      expect(c.standard_deviation).to stddev
      expect(c.histogram).to histogram
    end

    def check_stats(c, size, max, min, sum, mean, var, histogram)
      check_stats_with_matchers c,
                                ((size > 0) ? be_falsey : be_truthy),
                                eql(size),
                                eql(max),
                                eql(min),
                                eql(sum),
                                be_within(0.0000000001).of(mean),
                                be_within(0.0000000001).of(var),
                                be_within(0.0000000001).of(Math.sqrt(var)),
                                eql(histogram)
    end

    it "is able to initialize with an array" do
      expect { Stats::Counter.new([1, 2, 4, 6, 9]) }.not_to raise_error
    end

    it "returns some basic statistics" do
      c = Stats::Counter.new([1, 2, 4, 9])
      check_stats(c, 4, 9, 1, 16, 4.0, 9.5, { bin_width: 1.0, bin_base: 0.0, data: { 1.0 => 1, 2.0 => 1, 4.0 => 1, 9.0 => 1 } })
      c << 6
      check_stats(c, 5, 9, 1, 22, 4.4, 8.24, { bin_width: 1.0, bin_base: 0.0, data: { 1.0 => 1, 2.0 => 1, 4.0 => 1, 9.0 => 1, 6.0 => 1 } })
      c << -1
      check_stats(c, 6, 9, -1, 21, 3.5, (139.0 / 6) - 12.25, { bin_width: 1.0, bin_base: 0.0, data: { 1.0 => 1, 2.0 => 1, 4.0 => 1, 9.0 => 1, 6.0 => 1, -1.0 => 1 } })
      c << 3
      check_stats(c, 7, 9, -1, 24, 24.0 / 7, (148.0 / 7) - (576.0 / 49), { bin_width: 1.0, bin_base: 0.0, data: { 1.0 => 1, 2.0 => 1, 4.0 => 1, 9.0 => 1, 6.0 => 1, -1.0 => 1, 3.0 => 1 } })
      c << 21
      check_stats(c, 8, 21, -1, 45, 5.625, 41.984375, { bin_width: 1.0, bin_base: 0.0, data: { 1.0 => 1, 2.0 => 1, 4.0 => 1, 9.0 => 1, 6.0 => 1, -1.0 => 1, 3.0 => 1, 21.0 => 1 } })
    end

    it "determines standard deviation" do
      c = Stats::Counter.new([9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4])
      stddev = c.stddev
      expect("%.2f" % stddev).to eq "2.98"

      c = Stats::Counter.new([0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004,
                              0.30000000000000004])
      stddev = c.stddev
      expect("%.2f" % stddev).to eq "0.00"

      c = Stats::Counter.new
      expect(c.stddev).to be_nil
    end

    it "returns the right things with no values" do
      c = Stats::Counter.new
      check_stats_with_matchers c,
                                be_truthy,
                                eql(0),
                                be_nil,
                                be_nil,
                                eql(0),
                                be_nil,
                                be_nil,
                                be_nil,
                                eql({ bin_width: 1.0, bin_base: 0.0, data: {} })

      c << -5
      check_stats(c, 1, -5, -5, -5, -5.0, 0, { bin_width: 1.0, bin_base: 0.0, data: { -5.0 => 1 } })
      c << 5
      check_stats(c, 2, 5, -5, 0, 0.0, 25.0, { bin_width: 1.0, bin_base: 0.0, data: { -5.0 => 1, 5.0 => 1 } })
    end

    it "supports .each, .<<, and .push" do
      c = Stats::Counter.new([1, 2, 3])
      test = []
      c.each { |item| test << item } # rubocop:disable Style/MapIntoArray
      c << 4
      c.push 5
      c.each { |item| test << item }
      expect(test).to eq [1, 2, 3, 1, 2, 3, 4, 5]
    end

    it "puts negative numbers in the proper bin in histograms" do
      c = Stats::Counter.new([-1, -0.5, 0, 0.5, 1])
      h = c.histogram
      expect(h).to eq({ bin_width: 1.0, bin_base: 0.0, data: { -1.0 => 2, 0.0 => 2, 1.0 => 1 } })
    end

    it "works with strange bin widths in histogram" do
      c = Stats::Counter.new([-7, -3, 0, 1, 2, 3, 4, 5, 6])
      h = c.histogram(2.5, 0.0)
      expect(h).to eq({ bin_width: 2.5, bin_base: 0.0, data: { 0.0 => 3, -5.0 => 1, 5.0 => 2, -7.5 => 1, 2.5 => 2 } })
    end

    it "works with strange bin bases in histogram" do
      c = Stats::Counter.new([-7, -3, 0, 1, 2, 3, 4, 5, 6])
      h = c.histogram(2.5, 1.5)
      expect(h).to eq({ bin_width: 2.5, bin_base: 1.5, data: { 1.5 => 2, 4.0 => 3, -3.5 => 1, -8.5 => 1, -1.0 => 2 } })
    end

    it "returns quarties properly" do
      c = Stats::Counter.new([6, 4, 2, -7, 0, 1, 3, 5, -3, 20])
      q = c.quartiles
      expect(q).to eq [-0.75, 2.5, 5.25]
    end

    it "returns nils for quartiles when there is no data" do
      c = Stats::Counter.new([])
      q = c.quartiles
      expect(q).to eq [nil, nil, nil]
    end

    it "returns a single number for quartiles if that is the only thing in the data" do
      c = Stats::Counter.new([5])
      q = c.quartiles
      expect(q).to eq [5, 5, 5]
    end

    it "returns properly for a dataset of length 3" do
      c = Stats::Counter.new([1, 2, 10])
      q = c.quartiles
      expect(q).to eq [1, 2, 10]
    end

    it "returns properly for a dataset of length 2" do
      c = Stats::Counter.new([1, 10])
      q = c.quartiles
      expect(q).to eq [1, 5.5, 10]
    end
  end
end
