# frozen_string_literal: true

require "webmock/rspec"

RSpec.describe Dispatch::Adapter::Claude, "#usage_report" do
  let(:api_key)   { "sk-ant-api03-test" }
  let(:oauth_key) { "sk-ant-oat-test-token" }
  let(:base_url)  { "https://api.anthropic.com" }
  let(:usage_url) { "#{base_url}/api/oauth/usage" }
  let(:profile_url) { "#{base_url}/api/oauth/profile" }

  before { WebMock.disable_net_connect! }
  after  { WebMock.reset! }

  # ── Helpers ───────────────────────────────────────────────────────────────

  def make_adapter(key: api_key, oauth: false)
    described_class.new(
      model: "claude-sonnet-4-6",
      api_key: key,
      base_url: base_url,
      is_oauth: oauth
    )
  end

  def stub_usage(body, status: 200)
    stub_request(:get, usage_url)
      .to_return(
        status: status,
        body: JSON.generate(body),
        headers: { "Content-Type" => "application/json" }
      )
  end

  # A realistic usage payload with all four buckets
  let(:full_payload) do
    {
      "five_hour" => { "utilization" => 45.0,  "resets_at" => "2025-01-01T12:00:00Z" },
      "seven_day" => { "utilization" => 92.0,  "resets_at" => "2025-01-07T00:00:00Z" },
      "seven_day_opus" => { "utilization" => 95.5, "resets_at" => "2025-01-07T00:00:00Z" },
      "seven_day_sonnet" => { "utilization" => 100.0, "resets_at" => "2025-01-07T00:00:00Z" }
    }
  end

  # ── API-key mode returns nil immediately ──────────────────────────────────

  describe "API-key (non-OAuth) mode" do
    subject(:adapter) { make_adapter(key: api_key, oauth: false) }

    it "returns nil without making any HTTP call" do
      result = adapter.usage_report
      expect(result).to be_nil
      expect(WebMock).not_to have_requested(:get, usage_url)
    end
  end

  # ── OAuth mode — successful response ─────────────────────────────────────

  describe "OAuth mode — full payload" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      stub_usage(full_payload)
      stub_request(:get, profile_url).to_return(
        status: 200,
        body: JSON.generate({ "email" => "u@example.com", "account_id" => "acct_123" }),
        headers: { "Content-Type" => "application/json" }
      )
    end

    it "returns a UsageReport" do
      expect(adapter.usage_report).to be_a(Dispatch::Adapter::UsageReport)
    end

    it "sets provider to 'Anthropic (Claude)'" do
      expect(adapter.usage_report.provider).to eq("Anthropic (Claude)")
    end

    it "returns 4 limits (one per bucket)" do
      expect(adapter.usage_report.limits.size).to eq(4)
    end

    it "all limits are UsageLimitEntry objects" do
      expect(adapter.usage_report.limits).to all(be_a(Dispatch::Adapter::UsageLimitEntry))
    end

    it "five_hour entry has :ok status (45%)" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry).not_to be_nil
      expect(entry.status).to eq(:ok)
    end

    it "seven_day entry has :warning status (92%)" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d" }
      expect(entry).not_to be_nil
      expect(entry.status).to eq(:warning)
    end

    it "seven_day_opus entry has :warning status (95.5%)" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:opus" }
      expect(entry).not_to be_nil
      expect(entry.status).to eq(:warning)
    end

    it "seven_day_sonnet entry has :exhausted status (100%)" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:sonnet" }
      expect(entry).not_to be_nil
      expect(entry.status).to eq(:exhausted)
    end

    it "amount.used reflects utilization" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.amount.used).to be_within(0.001).of(45.0)
    end

    it "amount.remaining = 100 - used" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.amount.remaining).to be_within(0.001).of(55.0)
    end

    it "amount.unit is :percent" do
      adapter.usage_report.limits.each do |entry|
        expect(entry.amount.unit).to eq(:percent)
      end
    end

    it "amount.limit is 100" do
      adapter.usage_report.limits.each do |entry|
        expect(entry.amount.limit).to eq(100)
      end
    end

    it "window duration_ms for 5h is 18_000_000" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.window.duration_ms).to eq(18_000_000)
    end

    it "window duration_ms for 7d is 604_800_000" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d" }
      expect(entry.window.duration_ms).to eq(604_800_000)
    end

    it "window.resets_at is a Time for five_hour" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.window.resets_at).to be_a(Time)
    end

    it "five_hour scope has shared: true" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.scope[:shared]).to be true
    end

    it "seven_day_opus scope has shared: false" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:opus" }
      expect(entry.scope[:shared]).to be false
    end

    it "seven_day_opus scope has tier: 'opus'" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:opus" }
      expect(entry.scope[:tier]).to eq("opus")
    end

    it "five_hour label is 'Claude 5 Hour'" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:5h" }
      expect(entry.label).to eq("Claude 5 Hour")
    end

    it "seven_day_sonnet label is 'Claude 7 Day (Sonnet)'" do
      entry = adapter.usage_report.limits.find { |e| e.id == "anthropic:7d:sonnet" }
      expect(entry.label).to eq("Claude 7 Day (Sonnet)")
    end
  end

  # ── Status thresholds ─────────────────────────────────────────────────────

  describe "status thresholds" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      stub_request(:get, profile_url).to_return(
        status: 200,
        body: JSON.generate({ "email" => "u@example.com", "account_id" => "acct_123" }),
        headers: { "Content-Type" => "application/json" }
      )
    end

    [
      [89.9, :ok],
      [90.0, :warning],
      [99.9, :warning],
      [100.0, :exhausted],
      [105.0, :exhausted]
    ].each do |utilization, expected_status|
      it "#{utilization}% → #{expected_status}" do
        stub_usage({ "five_hour" => { "utilization" => utilization, "resets_at" => nil } })
        entry = adapter.usage_report.limits.first
        expect(entry.status).to eq(expected_status)
      end
    end
  end

  # ── Empty payload returns nil ─────────────────────────────────────────────

  describe "empty payload (no recognised buckets)" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before { stub_usage({ "other_field" => "value" }) }

    it "returns nil when no recognised buckets are present" do
      expect(adapter.usage_report).to be_nil
    end
  end

  # ── Profile fetch for missing metadata ────────────────────────────────────

  describe "profile fetch for email/account_id" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      stub_usage({ "five_hour" => { "utilization" => 50.0 } })
      stub_request(:get, profile_url)
        .to_return(
          status: 200,
          body: JSON.generate({ "email" => "user@example.com",
                                "account_id" => "acct_123" }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "fetches profile to populate email" do
      report = adapter.usage_report
      expect(report.metadata[:email]).to eq("user@example.com")
    end

    it "fetches profile to populate account_id" do
      report = adapter.usage_report
      expect(report.metadata[:account_id]).to eq("acct_123")
    end
  end

  describe "email/account_id already in usage payload" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      payload = full_payload.merge("email" => "me@example.com",
                                   "account_id" => "acct_456")
      stub_usage(payload)
    end

    it "uses email from usage payload without calling profile" do
      report = adapter.usage_report
      expect(report.metadata[:email]).to eq("me@example.com")
      expect(WebMock).not_to have_requested(:get, profile_url)
    end
  end

  # ── Failure returns nil ───────────────────────────────────────────────────

  describe "network failure returns nil" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      stub_request(:get, usage_url).to_raise(Errno::ECONNREFUSED)
    end

    it "returns nil without raising" do
      expect(adapter.usage_report).to be_nil
    end
  end

  describe "HTTP 500 returns nil after retries" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      # Suppress sleep in UsageClient retry loop (it's a module_function,
      # sleep is called via Kernel; stub at the UsageClient module level)
      allow(Dispatch::Adapter::Claude::UsageClient).to receive(:sleep)
      stub_request(:get, usage_url)
        .to_return(
          status: 500,
          body: JSON.generate({ "error" => { "message" => "oops" } }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "returns nil after exhausting retries" do
      expect(adapter.usage_report).to be_nil
    end
  end

  describe "HTTP 401 returns nil immediately" do
    subject(:adapter) { make_adapter(key: oauth_key, oauth: true) }

    before do
      stub_request(:get, usage_url)
        .to_return(
          status: 401,
          body: JSON.generate({ "error" => { "message" => "Unauthorized" } }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "returns nil on auth error without raising" do
      expect(adapter.usage_report).to be_nil
    end
  end
end
