# frozen_string_literal: true

RSpec.describe Dispatch::Adapter::Claude::ResponseBuilder do
  let(:model_id)   { "claude-sonnet-4-6" }
  let(:model_info) { Dispatch::Adapter::Claude::ModelCatalog.build(model_id) }

  def build(json, is_oauth: false, info: model_info)
    described_class.build(json, model_info: info, is_oauth: is_oauth)
  end

  # ── Basic structure ────────────────────────────────────────────────────────

  describe ".build" do
    let(:minimal_json) do
      {
        "id" => "msg_01",
        "type" => "message",
        "model" => model_id,
        "role" => "assistant",
        "stop_reason" => "end_turn",
        "content" => [{ "type" => "text", "text" => "Hello" }],
        "usage" => {
          "input_tokens" => 10,
          "output_tokens" => 5
        }
      }
    end

    it "returns a Response" do
      expect(build(minimal_json)).to be_a(Dispatch::Adapter::Response)
    end

    it "sets the model from the JSON body" do
      expect(build(minimal_json).model).to eq(model_id)
    end

    it "sets stop_reason to :end_turn" do
      expect(build(minimal_json).stop_reason).to eq(:end_turn)
    end
  end

  # ── Content block parsing ──────────────────────────────────────────────────

  describe "content parsing" do
    context "with a text block" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "end_turn",
          "content" => [{ "type" => "text", "text" => "Hello world" }],
          "usage" => { "input_tokens" => 5, "output_tokens" => 3 }
        }
      end

      it "returns a TextBlock in content" do
        response = build(json)
        expect(response.content).to include(an_instance_of(Dispatch::Adapter::TextBlock))
      end

      it "has the correct text" do
        response = build(json)
        expect(response.content.first.text).to eq("Hello world")
      end

      it "has no tool_calls" do
        expect(build(json).tool_calls).to be_empty
      end
    end

    context "with a thinking block" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "end_turn",
          "content" => [{
            "type" => "thinking",
            "thinking" => "Step 1: think...",
            "signature" => "sig_abc123"
          }],
          "usage" => { "input_tokens" => 5, "output_tokens" => 3 }
        }
      end

      it "returns a ThinkingBlock in content" do
        response = build(json)
        expect(response.content).to include(an_instance_of(Dispatch::Adapter::ThinkingBlock))
      end

      it "has the correct thinking text" do
        block = build(json).content.find { |b| b.is_a?(Dispatch::Adapter::ThinkingBlock) }
        expect(block.thinking).to eq("Step 1: think...")
      end

      it "has the correct signature" do
        block = build(json).content.find { |b| b.is_a?(Dispatch::Adapter::ThinkingBlock) }
        expect(block.signature).to eq("sig_abc123")
      end

      it "sets signature to nil when absent" do
        json_no_sig = json.dup
        json_no_sig["content"] = [{ "type" => "thinking", "thinking" => "hmm" }]
        block = build(json_no_sig).content.find { |b| b.is_a?(Dispatch::Adapter::ThinkingBlock) }
        expect(block.signature).to be_nil
      end
    end

    context "with a redacted_thinking block" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "end_turn",
          "content" => [{ "type" => "redacted_thinking", "data" => "REDACTED_DATA" }],
          "usage" => { "input_tokens" => 5, "output_tokens" => 3 }
        }
      end

      it "returns a RedactedThinkingBlock in content" do
        response = build(json)
        expect(response.content).to include(an_instance_of(Dispatch::Adapter::RedactedThinkingBlock))
      end

      it "has the correct data" do
        block = build(json).content.find { |b| b.is_a?(Dispatch::Adapter::RedactedThinkingBlock) }
        expect(block.data).to eq("REDACTED_DATA")
      end
    end

    context "with a tool_use block" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "tool_use",
          "content" => [{
            "type" => "tool_use",
            "id" => "toolu_01",
            "name" => "bash",
            "input" => { "command" => "ls -la" }
          }],
          "usage" => { "input_tokens" => 10, "output_tokens" => 8 }
        }
      end

      it "puts tool_use blocks in tool_calls, not content" do
        response = build(json)
        expect(response.tool_calls).not_to be_empty
        expect(response.content).to be_empty
      end

      it "creates a ToolUseBlock with the correct id, name, arguments" do
        tc = build(json).tool_calls.first
        expect(tc).to be_a(Dispatch::Adapter::ToolUseBlock)
        expect(tc.id).to eq("toolu_01")
        expect(tc.name).to eq("bash")
        expect(tc.arguments).to eq({ "command" => "ls -la" })
      end
    end

    context "with a tool_use block when is_oauth: true" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "tool_use",
          "content" => [{
            "type" => "tool_use",
            "id" => "toolu_01",
            "name" => "proxy_bash",
            "input" => { "command" => "echo hi" }
          }],
          "usage" => { "input_tokens" => 5, "output_tokens" => 5 }
        }
      end

      it "strips the proxy_ prefix from tool name" do
        tc = build(json, is_oauth: true).tool_calls.first
        expect(tc.name).to eq("bash")
      end

      it "does NOT strip prefix when is_oauth: false" do
        tc = build(json, is_oauth: false).tool_calls.first
        expect(tc.name).to eq("proxy_bash")
      end
    end

    context "with mixed content (text + tool_use)" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "tool_use",
          "content" => [
            { "type" => "text",     "text" => "I'll call a tool." },
            { "type" => "tool_use", "id" => "toolu_02", "name" => "find",
              "input" => { "path" => "/tmp" } }
          ],
          "usage" => { "input_tokens" => 12, "output_tokens" => 10 }
        }
      end

      it "separates text blocks and tool_calls" do
        response = build(json)
        expect(response.content.size).to eq(1)
        expect(response.content.first).to be_a(Dispatch::Adapter::TextBlock)
        expect(response.tool_calls.size).to eq(1)
        expect(response.tool_calls.first).to be_a(Dispatch::Adapter::ToolUseBlock)
      end
    end

    context "with empty text blocks" do
      let(:json) do
        {
          "model" => model_id, "stop_reason" => "end_turn",
          "content" => [{ "type" => "text", "text" => "" }],
          "usage" => { "input_tokens" => 2, "output_tokens" => 0 }
        }
      end

      it "omits empty text blocks" do
        expect(build(json).content).to be_empty
      end
    end
  end

  # ── Stop-reason mapping ────────────────────────────────────────────────────

  describe "stop_reason mapping" do
    {
      "end_turn" => :end_turn,
      "max_tokens" => :max_tokens,
      "tool_use" => :tool_use,
      "pause_turn" => :pause_turn,
      "refusal" => :refusal,
      "sensitive" => :sensitive,
      "stop_sequence" => :end_turn,
      "unknown_new" => :end_turn # unknown falls back to :end_turn
    }.each do |raw, expected|
      it "maps #{raw.inspect} to #{expected.inspect}" do
        json = {
          "model" => model_id,
          "stop_reason" => raw,
          "content" => [],
          "usage" => { "input_tokens" => 1, "output_tokens" => 1 }
        }
        expect(build(json).stop_reason).to eq(expected)
      end
    end
  end

  # ── Usage ──────────────────────────────────────────────────────────────────

  describe "usage population" do
    let(:json) do
      {
        "model" => model_id,
        "stop_reason" => "end_turn",
        "content" => [],
        "usage" => {
          "input_tokens" => 100,
          "output_tokens" => 50,
          "cache_read_input_tokens" => 200,
          "cache_creation_input_tokens" => 400
        }
      }
    end

    it "sets input_tokens" do
      expect(build(json).usage.input_tokens).to eq(100)
    end

    it "sets output_tokens" do
      expect(build(json).usage.output_tokens).to eq(50)
    end

    it "sets cache_read_tokens from cache_read_input_tokens" do
      expect(build(json).usage.cache_read_tokens).to eq(200)
    end

    it "sets cache_creation_tokens from cache_creation_input_tokens" do
      expect(build(json).usage.cache_creation_tokens).to eq(400)
    end

    it "sets usage.cost as a UsageCost" do
      expect(build(json).usage.cost).to be_a(Dispatch::Adapter::UsageCost)
    end

    it "usage.cost.total >= 0" do
      expect(build(json).usage.cost.total).to be >= 0
    end

    it "usage.cost.total matches manual calculation for known pricing" do
      pricing = model_info.pricing
      usage   = build(json).usage
      cost    = usage.cost

      mtok = ->(tokens, rate) { (rate.to_f / 1_000_000.0) * tokens.to_i }
      expected_total = mtok.call(100, pricing.input_per_mtok) +
                       mtok.call(50,  pricing.output_per_mtok) +
                       mtok.call(200, pricing.cache_read_per_mtok) +
                       mtok.call(400, pricing.cache_write_per_mtok)

      expect(cost.total).to be_within(1e-9).of(expected_total)
    end
  end

  describe "usage when cache fields are absent" do
    let(:json) do
      {
        "model" => model_id,
        "stop_reason" => "end_turn",
        "content" => [],
        "usage" => { "input_tokens" => 10, "output_tokens" => 5 }
      }
    end

    it "defaults cache_read_tokens to 0" do
      expect(build(json).usage.cache_read_tokens).to eq(0)
    end

    it "defaults cache_creation_tokens to 0" do
      expect(build(json).usage.cache_creation_tokens).to eq(0)
    end
  end

  describe "usage when model has no pricing" do
    let(:unknown_info) do
      Dispatch::Adapter::ModelInfo.new(
        id: "unknown-model", name: "unknown-model",
        max_context_tokens: 200_000,
        supports_vision: true, supports_tool_use: true, supports_streaming: true,
        pricing: nil
      )
    end

    it "sets usage.cost to nil when model has no pricing" do
      json = {
        "model" => "unknown-model", "stop_reason" => "end_turn",
        "content" => [],
        "usage" => { "input_tokens" => 10, "output_tokens" => 5 }
      }
      response = described_class.build(json, model_info: unknown_info, is_oauth: false)
      expect(response.usage.cost).to be_nil
    end
  end

  # ── STOP_REASON_MAP constant ──────────────────────────────────────────────

  describe "STOP_REASON_MAP" do
    it "maps all seven documented Anthropic stop_reason strings" do
      expected_keys = %w[end_turn max_tokens tool_use pause_turn refusal sensitive stop_sequence]
      expect(described_class::STOP_REASON_MAP.keys).to include(*expected_keys)
    end
  end
end
