# frozen_string_literal: true

require_relative "../../helper"
require "versions/v2/rate_query"

module Versions
  describe V2::RateQuery do
    it "raises on invalid date" do
      _ { V2::RateQuery.new(date: "not-a-date") }.must_raise(V2::RateQuery::ValidationError)
    end

    it "raises on conflicting date params" do
      date = Fixtures.latest_date.to_s

      _ { V2::RateQuery.new(date:, from: (Fixtures.latest_date - 30).to_s) }.must_raise(V2::RateQuery::ValidationError)
    end

    it "raises on invalid group" do
      _ { V2::RateQuery.new(group: "day") }.must_raise(V2::RateQuery::ValidationError)
    end

    it "accepts valid group" do
      range_start = (Fixtures.latest_date - 90).to_s
      range_end = Fixtures.latest_date.to_s
      query = V2::RateQuery.new(from: range_start, to: range_end, group: "month")

      _(query.to_a).wont_be_empty
    end

    it "filters by quotes" do
      query = V2::RateQuery.new(quotes: "USD,GBP")
      quotes = query.to_a.map { |r| r[:quote] }.uniq.sort

      _(quotes).must_equal(["GBP", "USD"])
    end

    it "returns empty array for dates before dataset" do
      query = V2::RateQuery.new(date: "1901-01-01")

      _(query.to_a).must_be_empty
    end

    it "raises on invalid base currency" do
      _ { V2::RateQuery.new(base: "FOO") }.must_raise(V2::RateQuery::ValidationError)
    end

    it "raises on invalid quote currency" do
      _ { V2::RateQuery.new(quotes: "USD,FOO") }.must_raise(V2::RateQuery::ValidationError)
    end

    it "raises on invalid base and quotes together" do
      error = _ { V2::RateQuery.new(base: "FOO", quotes: "BAR") }.must_raise(V2::RateQuery::ValidationError)
      _(error.message).must_include("FOO")
      _(error.message).must_include("BAR")
    end

    it "snaps to nearest business day" do
      sunday = Fixtures.recent_sunday
      friday = Fixtures.preceding_friday(sunday)
      query = V2::RateQuery.new(date: sunday.to_s)

      _(query.to_a.first[:date]).must_equal(friday.to_s)
    end

    it "stamps each row with its own observation date on latest path" do
      # Carry-forward on the latest path should report each pair's actual observation date,
      # not flatten everything to the batch max.
      stale_date = Fixtures.latest_date - 5
      Rate.dataset.insert(date: stale_date, base: "EUR", quote: "RON", rate: 4.97, provider: "ECB")

      query = V2::RateQuery.new(date: Fixtures.latest_date.to_s)
      results = query.to_a

      ron = results.find { |r| r[:base] == "EUR" && r[:quote] == "RON" }
      usd = results.find { |r| r[:base] == "EUR" && r[:quote] == "USD" }

      _(ron[:date]).must_equal(stale_date.to_s)
      _(usd[:date]).must_equal(Fixtures.latest_date.to_s)
    end

    it "does not carry forward in range queries" do
      # A pair published only on a single date inside the range should appear once on that date,
      # not be carried forward into subsequent days.
      from = Fixtures.latest_date - 6
      to = Fixtures.latest_date
      stale_date = Fixtures.latest_date - 5
      Rate.dataset.insert(date: stale_date, base: "EUR", quote: "RON", rate: 4.97, provider: "ECB")

      query = V2::RateQuery.new(from: from.to_s, to: to.to_s)
      ron_rows = query.to_a.select { |r| r[:base] == "EUR" && r[:quote] == "RON" }

      _(ron_rows.size).must_equal(1)
      _(ron_rows.first[:date]).must_equal(stale_date.to_s)
    end

    describe "with expand=providers" do
      it "is omitted by default" do
        query = V2::RateQuery.new(date: Fixtures.latest_date.to_s, quotes: "USD")
        results = query.to_a

        _(results).wont_be_empty
        _(results.first.key?(:providers)).must_equal(false)
      end

      it "adds providers list to blended rows as {key, rate} objects" do
        query = V2::RateQuery.new(date: Fixtures.latest_date.to_s, quotes: "USD", expand: "providers")
        results = query.to_a

        _(results).wont_be_empty
        providers = results.first[:providers]

        _(providers).must_be_kind_of(Array)
        _(providers).wont_be_empty
        _(providers.first).must_be_kind_of(Hash)
        _(providers.first.keys.sort).must_equal([:key, :rate])
        _(providers.first[:key]).must_be_kind_of(String)
        _(providers.first[:rate]).must_be_kind_of(Numeric)
      end

      it "marks all providers excluded on peg-snapped rows" do
        date = Fixtures.latest_date
        Rate.dataset.insert(provider: "ECB", date:, base: "EUR", quote: "AED", rate: 3.97)

        query = V2::RateQuery.new(date: date.to_s, base: "USD", quotes: "AED", expand: "providers")
        results = query.to_a

        _(results).wont_be_empty
        _(results.first[:rate]).must_equal(3.6725)
        providers = results.first[:providers]

        _(providers).wont_be_empty
        _(providers.all? { |p| p[:excluded] == true }).must_equal(true)
      end

      it "works with rollup queries" do
        range_start = (Fixtures.latest_date - 90).to_s
        range_end = Fixtures.latest_date.to_s
        query = V2::RateQuery.new(from: range_start, to: range_end, group: "week", quotes: "USD", expand: "providers")
        results = query.to_a

        _(results).wont_be_empty
        providers = results.first[:providers]

        _(providers).must_be_kind_of(Array)
        _(providers).wont_be_empty
        _(providers.first.keys.sort).must_equal([:key, :rate])
      end

      it "raises on unknown expand value" do
        _ { V2::RateQuery.new(expand: "weights") }.must_raise(V2::RateQuery::ValidationError)
      end
    end

    describe "peg gap filling" do
      # BTN is pegged 1:1 to INR (since 1974). Fixtures have ECB providing INR.
      # Add a provider that starts covering BTN only recently, leaving older dates
      # to be filled by the peg.
      before do
        cutoff = Fixtures.business_day(30)
        days = []
        date = Fixtures.latest_date
        while date >= cutoff
          days << date unless date.saturday? || date.sunday?
          date -= 1
        end

        records = days.map do |date|
          { provider: "TEST", date:, base: "EUR", quote: "BTN", rate: 90.0 }
        end
        Rate.dataset.multi_insert(records)
      end

      it "fills dates before provider coverage with peg-derived rates" do
        early_date = Fixtures.business_day(60)
        query = V2::RateQuery.new(date: early_date.to_s, quotes: "BTN")
        results = query.to_a

        _(results).wont_be_empty
        _(results.first[:quote]).must_equal("BTN")
      end

      it "uses provider rates when available" do
        recent_date = Fixtures.latest_date.to_s
        query = V2::RateQuery.new(date: recent_date, quotes: "BTN")
        results = query.to_a

        _(results).wont_be_empty
        _(results.first[:quote]).must_equal("BTN")
      end
    end

    describe "#scale_for_pegged_base" do
      it "drops the base->base row that PegAnchor synthesizes for the user's pegged base" do
        date = Date.parse("2024-01-15")
        query = V2::RateQuery.new(base: "AED")
        rows = [
          { date:, base: "USD", quote: "EUR", rate: 0.93 },
          { date:, base: "USD", quote: "AED", rate: 3.6725 },
        ]

        result = query.send(:scale_for_pegged_base, rows)

        _(result.find { |r| r[:quote] == "AED" }).must_be_nil
        _(result.find { |r| r[:quote] == "USD" }[:rate]).must_be_close_to(1.0 / 3.6725)
      end
    end

    describe "?providers= with pegged base" do
      it "returns empty (peg layer is bypassed when source set is restricted)" do
        recent_date = Fixtures.latest_date.to_s
        query = V2::RateQuery.new(date: recent_date, providers: "ECB", base: "AED", quotes: "USD")

        _(query.to_a).must_be_empty
      end
    end

    describe "#derive" do
      let(:date) { Date.parse("2024-01-15") }

      def call_derive(rows, target:)
        V2::RateQuery.allocate.send(:derive, rows, target: target)
      end

      it "returns [] when target is not in input" do
        rows = [{ date:, base: "USD", quote: "EUR", rate: 0.93 }]

        _(call_derive(rows, target: "GBP")).must_equal([])
      end

      it "returns [] when input is empty" do
        _(call_derive([], target: "GBP")).must_equal([])
      end

      it "rebases each row to target by division and appends target->pivot row" do
        rows = [
          { date:, base: "USD", quote: "EUR", rate: 0.93, providers: [{ key: "ECB", rate: 0.93 }] },
          { date:, base: "USD", quote: "GBP", rate: 0.79, providers: [{ key: "ECB", rate: 0.79 }] },
        ]

        result = call_derive(rows, target: "EUR")
        gbp = result.find { |r| r[:quote] == "GBP" }
        usd = result.find { |r| r[:quote] == "USD" }

        _(gbp[:base]).must_equal("EUR")
        _(gbp[:rate]).must_be_close_to(0.79 / 0.93)
        _(gbp[:providers].first[:rate]).must_be_close_to(0.79 / 0.93)
        _(usd[:base]).must_equal("EUR")
        _(usd[:rate]).must_be_close_to(1.0 / 0.93)
        _(usd[:providers].first[:rate]).must_be_close_to(1.0 / 0.93)
      end

      it "drops the target row from output (no base->base row)" do
        rows = [
          { date:, base: "USD", quote: "EUR", rate: 0.93 },
          { date:, base: "USD", quote: "GBP", rate: 0.79 },
        ]

        result = call_derive(rows, target: "EUR")

        _(result.find { |r| r[:quote] == "EUR" }).must_be_nil
      end

      it "produces exact reciprocals between any two non-pivot quotes" do
        rows = [
          { date:, base: "USD", quote: "EUR", rate: 0.93 },
          { date:, base: "USD", quote: "GBP", rate: 0.79 },
        ]

        eur_view = call_derive(rows, target: "EUR")
        gbp_view = call_derive(rows, target: "GBP")
        eur_to_gbp = eur_view.find { |r| r[:quote] == "GBP" }[:rate]
        gbp_to_eur = gbp_view.find { |r| r[:quote] == "EUR" }[:rate]

        _(eur_to_gbp * gbp_to_eur).must_be_close_to(1.0, 1e-12)
      end
    end

    describe "#fast_path?" do
      let(:date) { Date.parse("2024-01-15") }

      def call_fast_path(query, rows)
        query.send(:fast_path?, rows)
      end

      it "is true when every row's :base equals effective_base" do
        query = V2::RateQuery.new(base: "EUR")
        rows = [
          { date:, base: "EUR", quote: "USD", rate: 1.08, provider: "ECB" },
          { date:, base: "EUR", quote: "GBP", rate: 0.86, provider: "ECB" },
        ]

        _(call_fast_path(query, rows)).must_equal(true)
      end

      it "is false when any row has a different :base" do
        query = V2::RateQuery.new(base: "EUR")
        rows = [
          { date:, base: "EUR", quote: "USD", rate: 1.08, provider: "ECB" },
          { date:, base: "CAD", quote: "USD", rate: 0.74, provider: "BOC" },
        ]

        _(call_fast_path(query, rows)).must_equal(false)
      end

      it "uses the peg's base as effective_base for a pegged request base" do
        query = V2::RateQuery.new(base: "AED")
        rows = [
          { date:, base: "USD", quote: "EUR", rate: 0.93, provider: "ECB" },
        ]

        _(call_fast_path(query, rows)).must_equal(true)
      end

      it "is true on empty input (vacuous)" do
        query = V2::RateQuery.new(base: "EUR")

        _(call_fast_path(query, [])).must_equal(true)
      end
    end
  end
end
