# frozen_string_literal: true

require "webmock/rspec"

RSpec.describe Dispatch::Adapter::Claude, "#list_models" do
  let(:model_id)  { "claude-sonnet-4-6" }
  let(:api_key)   { "sk-ant-api03-test" }
  let(:base_url)  { "https://api.anthropic.com" }
  let(:models_url) { "#{base_url}/v1/models?limit=200" }

  subject(:adapter) do
    described_class.new(
      model: model_id,
      api_key: api_key,
      base_url: base_url
    )
  end

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

  # A known bundled model ID (must exist in pricing table)
  let(:known_id)   { "claude-sonnet-4-6" }
  # A fake runtime-only model
  let(:unknown_id) { "claude-future-99" }

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

  # ── Runtime overlay adds new entries ─────────────────────────────────────

  describe "runtime entries added to bundled list" do
    let(:runtime_entries) do
      [
        { "id" => known_id,   "display_name" => "Claude Sonnet 4.6 (Latest)" },
        { "id" => unknown_id, "display_name" => "Claude Future 99" }
      ]
    end

    before { stub_models_api(runtime_entries) }

    it "returns an Array of ModelInfo objects" do
      result = adapter.list_models
      expect(result).to all(be_a(Dispatch::Adapter::ModelInfo))
    end

    it "includes the known model with display_name override" do
      result = adapter.list_models
      info   = result.find { |m| m.id == known_id }
      expect(info).not_to be_nil
      expect(info.name).to eq("Claude Sonnet 4.6 (Latest)")
    end

    it "known model retains bundled pricing" do
      result = adapter.list_models
      info   = result.find { |m| m.id == known_id }
      expect(info.pricing).not_to be_nil
    end

    it "includes the unknown (unrated) model" do
      result = adapter.list_models
      info   = result.find { |m| m.id == unknown_id }
      expect(info).not_to be_nil
    end

    it "unknown model has nil pricing" do
      result = adapter.list_models
      info   = result.find { |m| m.id == unknown_id }
      expect(info.pricing).to be_nil
    end

    it "unknown model has '(unrated)' prefix in name" do
      result = adapter.list_models
      info   = result.find { |m| m.id == unknown_id }
      expect(info.name).to start_with("(unrated)")
    end

    it "unknown model has a default max_context_tokens of 200_000" do
      result = adapter.list_models
      info   = result.find { |m| m.id == unknown_id }
      expect(info.max_context_tokens).to eq(200_000)
    end
  end

  # ── Failure falls back to bundled list ────────────────────────────────────

  describe "network failure falls back to bundled list" do
    before do
      stub_request(:get, models_url).to_raise(Errno::ECONNREFUSED)
    end

    it "does not raise" do
      expect { adapter.list_models }.not_to raise_error
    end

    it "returns an Array of ModelInfo objects" do
      expect(adapter.list_models).to all(be_a(Dispatch::Adapter::ModelInfo))
    end

    it "returns the bundled models" do
      result = adapter.list_models
      ids    = result.map(&:id)
      expect(ids).to include(known_id)
    end
  end

  describe "HTTP error falls back to bundled list" do
    before do
      stub_request(:get, models_url)
        .to_return(
          status: 500,
          body: JSON.generate({ "error" => { "message" => "oops" } }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "does not raise on server error" do
      expect { adapter.list_models }.not_to raise_error
    end

    it "returns bundled models on server error" do
      result = adapter.list_models
      expect(result.map(&:id)).to include(known_id)
    end
  end

  # ── In-memory cache ───────────────────────────────────────────────────────

  describe "in-memory cache" do
    before do
      stub_models_api([
                        { "id" => known_id, "display_name" => "Cached Name" }
                      ])
    end

    it "returns the same array object on a second call (cached)" do
      first  = adapter.list_models
      second = adapter.list_models
      expect(second).to equal(first) # object identity
    end

    it "only calls the API once within the TTL" do
      adapter.list_models
      adapter.list_models
      expect(WebMock).to have_requested(:get, models_url).once
    end

    it "re-fetches when the cache has expired" do
      # Force cache expiry by backdating the cache timestamp
      adapter.list_models
      adapter.instance_variable_set(:@models_cache_at,
                                    adapter.send(:current_time_ms) -
                                    described_class::MODELS_CACHE_TTL_MS - 1)
      adapter.list_models
      expect(WebMock).to have_requested(:get, models_url).twice
    end
  end

  # ── Bundled-only models appended if missing from runtime ──────────────────

  describe "bundled models not in runtime response are appended" do
    # Runtime only knows about one model; bundled knows more
    before do
      stub_models_api([
                        { "id" => unknown_id, "display_name" => "Future Model" }
                      ])
    end

    it "appends bundled models not present in the runtime response" do
      result = adapter.list_models
      ids    = result.map(&:id)
      expect(ids).to include(known_id)   # bundled model added
      expect(ids).to include(unknown_id) # runtime model present
    end
  end

  # ── ModelCatalog.build_from_api ───────────────────────────────────────────

  describe "ModelCatalog.build_from_api" do
    let(:catalog) { Dispatch::Adapter::Claude::ModelCatalog }

    context "with a known model id" do
      let(:entry) { { "id" => known_id, "display_name" => "My Name" } }

      it "uses the display_name" do
        info = catalog.build_from_api(entry)
        expect(info.name).to eq("My Name")
      end

      it "assigns bundled pricing" do
        info = catalog.build_from_api(entry)
        expect(info.pricing).not_to be_nil
      end

      it "falls back to id when display_name is empty" do
        info = catalog.build_from_api("id" => known_id, "display_name" => "")
        expect(info.name).to eq(known_id)
      end
    end

    context "with an unknown model id" do
      let(:entry) { { "id" => "totally-new-model", "display_name" => "New Model" } }

      it "prefixes name with '(unrated)'" do
        info = catalog.build_from_api(entry)
        expect(info.name).to eq("(unrated) New Model")
      end

      it "has nil pricing" do
        info = catalog.build_from_api(entry)
        expect(info.pricing).to be_nil
      end

      it "has default max_context_tokens of 200_000" do
        info = catalog.build_from_api(entry)
        expect(info.max_context_tokens).to eq(200_000)
      end
    end
  end
end
