# frozen_string_literal: true

require_relative "helper"
require "provider"
require "weekly_rate"
require "monthly_rate"
require "versions/v2/rate_query"

describe "Rollup tables" do
  describe "parity with downsample" do
    let(:interval) { (Fixtures.latest_date - 366)..Fixtures.latest_date }

    it "weekly rollup covers the same providers and currencies as downsample" do
      expected_keys = Rate.between(interval).downsample("week").all
        .map { |r| [r[:provider], r[:base], r[:quote]] }.uniq.sort

      actual_keys = WeeklyRate.between(interval).all
        .map { |r| [r[:provider], r[:base], r[:quote]] }.uniq.sort

      _(actual_keys).must_equal(expected_keys)
    end

    it "monthly rollup covers the same providers and currencies as downsample" do
      expected_keys = Rate.between(interval).downsample("month").all
        .map { |r| [r[:provider], r[:base], r[:quote]] }.uniq.sort

      actual_keys = MonthlyRate.between(interval).all
        .map { |r| [r[:provider], r[:base], r[:quote]] }.uniq.sort

      _(actual_keys).must_equal(expected_keys)
    end

    it "weekly rollup rates match downsample for inner buckets" do
      # Skip boundary buckets where partial-bucket averages differ
      ds_rows = Rate.between(interval).downsample("week").all
        .sort_by { |r| [r[:date].to_s, r[:provider], r[:base], r[:quote]] }
      bucket_dates = ds_rows.map { |r| r[:date].to_s }.uniq.sort
      inner_dates = bucket_dates[1...-1]

      expected = ds_rows.select { |r| inner_dates.include?(r[:date].to_s) }
      actual = WeeklyRate.between(interval).all
        .select { |r| inner_dates.include?(r[:bucket_date].to_s) }
        .sort_by { |r| [r[:bucket_date].to_s, r[:provider], r[:base], r[:quote]] }

      _(actual.size).must_equal(expected.size)
      actual.zip(expected).each do |a, e|
        _(a[:rate]).must_be_close_to(e[:rate], 0.0001)
      end
    end
  end

  describe "incremental refresh" do
    it "rebuilds affected weekly buckets after new rates" do
      provider = Provider.find(key: "ECB")
      date = Fixtures.latest_date

      WeeklyRate.where(provider: "ECB")
        .order(Sequel.desc(:bucket_date)).first

      Rate.dataset.insert(date:, base: "EUR", quote: "XTS", rate: 42.0, provider: "ECB")
      provider.send(:refresh_rollups, [date])

      week_row = WeeklyRate.where(provider: "ECB", quote: "XTS").first

      _(week_row).wont_be_nil
      _(week_row[:rate]).must_be_close_to(42.0, 0.01)

      # Unrelated provider buckets untouched
      boc_count_after = WeeklyRate.where(provider: "BOC").count

      _(boc_count_after).must_be(:>, 0)
    end

    it "rebuilds affected monthly buckets after new rates" do
      provider = Provider.find(key: "ECB")
      date = Fixtures.latest_date

      Rate.dataset.insert(date:, base: "EUR", quote: "XTS", rate: 99.0, provider: "ECB")
      provider.send(:refresh_rollups, [date])

      month_row = MonthlyRate.where(provider: "ECB", quote: "XTS").first

      _(month_row).wont_be_nil
      _(month_row[:rate]).must_be_close_to(99.0, 0.01)
    end
  end

  describe "shared scopes" do
    describe WeeklyRate do
      it "filters by provider with ecb" do
        data = WeeklyRate.ecb.all
        providers = data.map(&:provider).uniq

        _(providers).must_equal(["ECB"])
      end

      it "filters by date range with between" do
        start_date = Fixtures.latest_date - 60
        end_date = Fixtures.latest_date
        data = WeeklyRate.between(start_date..end_date)

        _(data).wont_be_empty
        data.each { |r| _(r.bucket_date).must_be(:>=, start_date - 7) }
      end

      it "filters currencies with only" do
        data = WeeklyRate.ecb.only("USD", "GBP").all

        data.each do |r|
          _([r.base, r.quote].any? { |c| ["USD", "GBP"].include?(c) }).must_equal(true)
        end
      end
    end

    describe MonthlyRate do
      it "filters by provider with ecb" do
        data = MonthlyRate.ecb.all

        _(data.map(&:provider).uniq).must_equal(["ECB"])
      end

      it "filters by date range with between" do
        start_date = Fixtures.latest_date - 180
        end_date = Fixtures.latest_date
        data = MonthlyRate.between(start_date..end_date)

        _(data).wont_be_empty
      end

      it "filters currencies with only" do
        data = MonthlyRate.ecb.only("USD").all

        data.each do |r|
          _([r.base, r.quote].any?("USD")).must_equal(true)
        end
      end
    end
  end

  describe "boundary bucket inclusion" do
    it "monthly grouped query includes partial first-month bucket" do
      # A 40-day range is guaranteed to cross at least one month boundary
      start_date = Fixtures.latest_date - 40
      end_date = Fixtures.latest_date
      query = Versions::V2::RateQuery.new(from: start_date.to_s, to: end_date.to_s, group: "month")
      dates = query.to_a.map { |r| r[:date] }.uniq

      _(dates.size).must_be(:>=, 2, "expected at least 2 monthly buckets for a cross-month range")
    end

    it "weekly grouped query includes partial first-week bucket" do
      start_date = Fixtures.latest_date - 20
      end_date = Fixtures.latest_date
      query = Versions::V2::RateQuery.new(from: start_date.to_s, to: end_date.to_s, group: "week")
      dates = query.to_a.map { |r| r[:date] }.uniq

      _(dates.size).must_be(:>=, 3, "expected at least 3 weekly buckets for a 20-day range")
    end
  end

  describe "single-date grouped query" do
    it "group=month with single date does not raise" do
      query = Versions::V2::RateQuery.new(date: Fixtures.latest_date.to_s, group: "month")

      _(query.to_a).wont_be_empty
    end

    it "group=week with single date does not raise" do
      query = Versions::V2::RateQuery.new(date: Fixtures.latest_date.to_s, group: "week")

      _(query.to_a).wont_be_empty
    end
  end

  describe "cache key freshness" do
    it "derives cache key from raw rates, not rollup bucket_date" do
      start_date = (Fixtures.latest_date - 30).to_s
      end_date = Fixtures.latest_date.to_s
      query_before = Versions::V2::RateQuery.new(from: start_date, to: end_date, group: "month")
      key_before = query_before.cache_key

      # Insert a new rate on the existing latest date and refresh rollups;
      # the raw max date is unchanged so the cache key should be stable
      Rate.dataset.insert(
        date: Fixtures.latest_date, base: "EUR", quote: "XTS", rate: 42.0, provider: "ECB",
      )
      Provider.find(key: "ECB").send(:refresh_rollups, [Fixtures.latest_date])

      query_after = Versions::V2::RateQuery.new(from: start_date, to: end_date, group: "month")
      key_after = query_after.cache_key

      _(key_before).must_equal(key_after, "cache key derives from raw max date, stays stable when rollup content changes within same day")
    end
  end
end
