# frozen_string_literal: true

RSpec.describe Dispatch::Adapter::Claude::RequestBuilder::Thinking do
  # Shorthand: build params and apply thinking config.
  def apply(model_id:, thinking:, tool_choice: nil,
            max_tokens: nil, max_output_tokens: nil)
    params = {}
    params[:max_tokens] = max_tokens if max_tokens
    described_class.apply(
      params,
      model_id:,
      thinking:,
      tool_choice:,
      max_output_tokens:
    )
    params
  end

  # ── nil / false → no-op ──────────────────────────────────────────────────

  describe "when thinking is nil or false" do
    it "does not add thinking or output_config keys for nil" do
      params = apply(model_id: "claude-opus-4-7", thinking: nil)
      expect(params).not_to have_key(:thinking)
      expect(params).not_to have_key(:output_config)
    end

    it "does not add thinking or output_config keys for false" do
      params = apply(model_id: "claude-opus-4-7", thinking: false)
      expect(params).not_to have_key(:thinking)
      expect(params).not_to have_key(:output_config)
    end
  end

  # ── Opus 4.7 adaptive with display ───────────────────────────────────────

  describe "Opus 4.7 (adaptive + display: summarized)" do
    let(:model_id) { "claude-opus-4-7" }

    it 'sets thinking: {type: "adaptive", display: "summarized"} for effort string "high"' do
      params = apply(model_id:, thinking: "high")
      expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" })
    end

    it "sets output_config.effort to the provided effort level" do
      params = apply(model_id:, thinking: "high")
      expect(params[:output_config]).to eq({ effort: "high" })
    end

    it 'sets effort "low" when thinking: "low"' do
      params = apply(model_id:, thinking: "low")
      expect(params[:output_config]).to eq({ effort: "low" })
    end

    it 'sets effort "medium" when thinking: "medium"' do
      params = apply(model_id:, thinking: "medium")
      expect(params[:output_config]).to eq({ effort: "medium" })
    end

    it 'sets effort "max" when thinking: "max"' do
      params = apply(model_id:, thinking: "max")
      expect(params[:output_config]).to eq({ effort: "max" })
    end

    it 'sets effort "xhigh" when thinking: "xhigh"' do
      params = apply(model_id:, thinking: "xhigh")
      expect(params[:output_config]).to eq({ effort: "xhigh" })
    end

    it "also works with the dated model ID claude-opus-4-7-20251018" do
      params = apply(model_id: "claude-opus-4-7-20251018", thinking: "high")
      expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" })
      expect(params[:output_config]).to eq({ effort: "high" })
    end

    it "does not set output_config when thinking kwarg is unrecognised junk" do
      params = apply(model_id:, thinking: "turbo")
      expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" })
      expect(params).not_to have_key(:output_config)
    end
  end

  # ── Opus 4.6 adaptive without display ────────────────────────────────────

  describe "Opus 4.6 (adaptive, no display)" do
    let(:model_id) { "claude-opus-4-6" }

    it 'sets thinking: {type: "adaptive"} without a display key' do
      params = apply(model_id:, thinking: "high")
      expect(params[:thinking]).to eq({ type: "adaptive" })
      expect(params[:thinking]).not_to have_key(:display)
    end

    it "sets output_config.effort = 'high'" do
      params = apply(model_id:, thinking: "high")
      expect(params[:output_config]).to eq({ effort: "high" })
    end
  end

  # ── Sonnet 4.6 adaptive without display ──────────────────────────────────

  describe "Sonnet 4.6 (adaptive, no display)" do
    let(:model_id) { "claude-sonnet-4-6" }

    it 'sets thinking: {type: "adaptive"} without a display key' do
      params = apply(model_id:, thinking: "medium")
      expect(params[:thinking]).to eq({ type: "adaptive" })
      expect(params[:thinking]).not_to have_key(:display)
    end

    it "sets output_config.effort = 'medium'" do
      params = apply(model_id:, thinking: "medium")
      expect(params[:output_config]).to eq({ effort: "medium" })
    end
  end

  # ── Sonnet 4.5 and older → enabled mode ──────────────────────────────────

  describe "Sonnet 4.5 (enabled mode)" do
    let(:model_id) { "claude-sonnet-4-5" }

    context "with a Hash {type: :enabled, budget_tokens: 8000}" do
      let(:thinking) { { type: :enabled, budget_tokens: 8000 } }

      it 'sets thinking: {type: "enabled", budget_tokens: 8000}' do
        params = apply(model_id:, thinking:)
        expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 8000 })
      end

      it "does not set output_config" do
        params = apply(model_id:, thinking:)
        expect(params).not_to have_key(:output_config)
      end

      it "raises max_tokens to at least budget_tokens + OUTPUT_FALLBACK_BUFFER" do
        params = apply(model_id:, thinking:, max_tokens: 100)
        expected = 8000 + described_class::OUTPUT_FALLBACK_BUFFER
        expect(params[:max_tokens]).to eq(expected)
      end

      it "does not lower an already-sufficient max_tokens" do
        big = 8000 + described_class::OUTPUT_FALLBACK_BUFFER + 1000
        params = apply(model_id:, thinking:, max_tokens: big)
        expect(params[:max_tokens]).to eq(big)
      end

      it "clamps raised max_tokens to max_output_tokens when provided" do
        # max_output_tokens = 9000 < budget + buffer = 12096
        params = apply(model_id:, thinking: { type: :enabled, budget_tokens: 8000 },
                       max_tokens: 100, max_output_tokens: 9000)
        expect(params[:max_tokens]).to eq(9000)
      end
    end

    context "with a string effort level (unusual for older models)" do
      it 'maps effort string to a proper budget: "high" => 10_000 tokens' do
        params = apply(model_id:, thinking: "high")
        expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 10_000 })
      end

      it 'maps "low" => 1_024 tokens' do
        params = apply(model_id:, thinking: "low")
        expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 1_024 })
      end

      it 'maps "medium" => 4_000 tokens' do
        params = apply(model_id:, thinking: "medium")
        expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 4_000 })
      end

      it 'maps unknown string to the "high" fallback budget (10_000)' do
        params = apply(model_id:, thinking: "superduper")
        expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 10_000 })
      end
    end
  end

  # ── Opus 4.5 (enabled mode, another older model) ──────────────────────────

  describe "Opus 4.5 (enabled mode)" do
    it "uses enabled mode for claude-opus-4-5" do
      params = apply(model_id: "claude-opus-4-5", thinking: { type: :enabled, budget_tokens: 2000 })
      expect(params[:thinking][:type]).to eq("enabled")
      expect(params[:thinking][:budget_tokens]).to eq(2000)
    end
  end

  # ── max_tokens guard: zero budget_tokens ──────────────────────────────────

  describe "max_tokens guard when budget_tokens is 0" do
    it "does NOT modify max_tokens when budget_tokens is 0" do
      params = apply(
        model_id: "claude-sonnet-4-5",
        thinking: { type: :enabled, budget_tokens: 0 },
        max_tokens: 100
      )
      expect(params[:max_tokens]).to eq(100)
    end
  end

  # ── tool_choice: :any strips thinking ────────────────────────────────────

  describe "tool_choice: :any strips thinking and output_config" do
    it "removes thinking and output_config for tool_choice: :any" do
      params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :any)
      expect(params).not_to have_key(:thinking)
      expect(params).not_to have_key(:output_config)
    end

    it 'removes thinking and output_config for tool_choice: {type: :tool, name: "edit"}' do
      params = apply(model_id: "claude-opus-4-7", thinking: "high",
                     tool_choice: { type: :tool, name: "edit" })
      expect(params).not_to have_key(:thinking)
      expect(params).not_to have_key(:output_config)
    end

    it 'removes thinking for tool_choice: {type: "any"}' do
      params = apply(model_id: "claude-opus-4-7", thinking: "high",
                     tool_choice: { type: "any" })
      expect(params).not_to have_key(:thinking)
    end

    it "does NOT remove thinking for tool_choice: :auto" do
      params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :auto)
      expect(params).to have_key(:thinking)
    end

    it "does NOT remove thinking for tool_choice: :none" do
      params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :none)
      expect(params).to have_key(:thinking)
    end
  end

  # ── Hash thinking kwarg with effort key ───────────────────────────────────

  describe "Hash thinking kwarg with effort: key (adaptive model)" do
    it "extracts effort from the hash and sets output_config" do
      params = apply(model_id: "claude-opus-4-7",
                     thinking: { effort: "medium" })
      expect(params[:output_config]).to eq({ effort: "medium" })
    end

    it "ignores effort from hash when value is unrecognised" do
      params = apply(model_id: "claude-opus-4-7",
                     thinking: { effort: "extreme" })
      expect(params).not_to have_key(:output_config)
    end
  end

  # ── OUTPUT_FALLBACK_BUFFER constant ──────────────────────────────────────

  describe "OUTPUT_FALLBACK_BUFFER" do
    it "equals 4096" do
      expect(described_class::OUTPUT_FALLBACK_BUFFER).to eq(4096)
    end
  end
end
