# frozen_string_literal: true

RSpec.describe Dispatch::Adapter::Claude::RequestBuilder::CacheControl do
  # Build a minimal user message hash
  def user_msg(content)
    { role: "user", content: content }
  end

  # Build a minimal assistant message hash
  def asst_msg(content)
    { role: "assistant", content: content }
  end

  # Build a minimal text block hash (symbol keys)
  def text_block(text)
    { type: "text", text: text }
  end

  # Build a minimal tool hash
  def tool_hash(name)
    { name: name, description: "A tool", input_schema: { "type" => "object" } }
  end

  # Build a system block hash
  def sys_block(text)
    { "type" => "text", "text" => text }
  end

  # Count cache_control markers across the whole params hash
  def count_markers(params)
    count = 0
    Array(params[:tools]).each   { |b| count += 1 if has_cc?(b) }
    Array(params[:system]).each  { |b| count += 1 if has_cc?(b) }
    Array(params[:messages]).each do |msg|
      next unless msg[:content].is_a?(Array)

      msg[:content].each { |b| count += 1 if has_cc?(b) }
    end
    count
  end

  def has_cc?(block)
    return false unless block.is_a?(Hash)

    block.key?(:cache_control) || block.key?("cache_control")
  end

  def get_cc(block)
    block[:cache_control] || block["cache_control"]
  end

  # ── resolve_cache_control ─────────────────────────────────────────────────

  describe ".resolve_cache_control" do
    let(:api_url) { "https://api.anthropic.com" }
    let(:proxy_url) { "https://my.proxy.example.com" }

    it "returns nil for :none" do
      expect(described_class.resolve_cache_control(:none, api_url)).to be_nil
    end

    it "defaults to :short when nil is given" do
      cc = described_class.resolve_cache_control(nil, api_url)
      expect(cc).to eq({ "type" => "ephemeral", "ttl" => "5m" })
    end

    it "returns ttl: '5m' for :short on api.anthropic.com" do
      cc = described_class.resolve_cache_control(:short, api_url)
      expect(cc["ttl"]).to eq("5m")
    end

    it "returns ttl: '1h' for :long on api.anthropic.com" do
      cc = described_class.resolve_cache_control(:long, api_url)
      expect(cc["ttl"]).to eq("1h")
    end

    it "returns ttl: '5m' for :long on a non-Anthropic base URL" do
      cc = described_class.resolve_cache_control(:long, proxy_url)
      expect(cc["ttl"]).to eq("5m")
    end

    it "returns type: 'ephemeral' in all non-nil cases" do
      %i[short long].each do |r|
        cc = described_class.resolve_cache_control(r, api_url)
        expect(cc["type"]).to eq("ephemeral")
      end
    end
  end

  # ── auto-placement: no-op on :none ────────────────────────────────────────

  describe "with cache_retention: :none" do
    it "places no markers" do
      params = {
        tools: [tool_hash("bash")],
        system: [sys_block("You are helpful.")],
        messages: [user_msg("hello")]
      }
      described_class.apply(params, cache_retention: :none)
      expect(count_markers(params)).to eq(0)
    end
  end

  # ── auto-placement: typical 4-position case ───────────────────────────────

  describe "marker placement" do
    let(:base_params) do
      {
        tools: [tool_hash("bash"), tool_hash("find")],
        system: [sys_block("Block 1"), sys_block("Block 2")],
        messages: [
          user_msg([text_block("user turn 1")]),
          asst_msg("assistant reply"),
          user_msg([text_block("user turn 2")]),
          asst_msg("assistant reply 2"),
          user_msg([text_block("user turn 3")]) # last user
        ]
      }
    end

    it "places at most 4 markers total" do
      described_class.apply(base_params)
      expect(count_markers(base_params)).to be <= 4
    end

    it "places a marker on the last tool" do
      described_class.apply(base_params)
      last_tool = base_params[:tools].last
      expect(has_cc?(last_tool)).to be true
    end

    it "places a marker on the last system block" do
      described_class.apply(base_params)
      last_sys = base_params[:system].last
      expect(has_cc?(last_sys)).to be true
    end

    it "places a marker on the last-user-message's last text block" do
      described_class.apply(base_params)
      last_user_msg = base_params[:messages].select { |m| m[:role] == "user" }.last
      blocks = last_user_msg[:content]
      expect(blocks.any? { |b| has_cc?(b) }).to be true
    end

    it "places a marker on the penultimate-user-message's last text block" do
      described_class.apply(base_params)
      user_msgs = base_params[:messages].select { |m| m[:role] == "user" }
      penultimate = user_msgs[-2]
      blocks = penultimate[:content]
      expect(blocks.any? { |b| has_cc?(b) }).to be true
    end

    it "does not mark assistant messages" do
      described_class.apply(base_params)
      asst_messages = base_params[:messages].select { |m| m[:role] == "assistant" }
      asst_messages.each do |msg|
        content = msg[:content]
        content.each { |b| expect(has_cc?(b)).to be false } if content.is_a?(Array)
      end
    end
  end

  # ── string content promotion ──────────────────────────────────────────────

  describe "string content promotion" do
    it "converts a user message with String content to [{type:text,text:…}] and marks it" do
      params = {
        tools: [],
        system: [],
        messages: [user_msg("hello there")]
      }
      described_class.apply(params)
      content = params[:messages].first[:content]
      expect(content).to be_an(Array)
      expect(content.first[:type]).to eq("text")
      expect(has_cc?(content.first)).to be true
    end
  end

  # ── caller-placed markers short-circuit ───────────────────────────────────

  describe "caller-placed markers" do
    it "does NOT auto-place markers when any message block already has cache_control" do
      params = {
        tools: [tool_hash("bash")],
        system: [sys_block("system")],
        messages: [
          user_msg([
                     { type: "text", text: "hi", cache_control: { "type" => "ephemeral" } }
                   ])
        ]
      }
      # Record initial state — system and tools are not yet marked
      described_class.apply(params)
      # The auto-placement should not touch system or tool since caller placed a marker
      expect(has_cc?(params[:tools].last)).to be false
      expect(has_cc?(params[:system].last)).to be false
    end
  end

  # ── enforce_limit: never exceed 4 breakpoints ────────────────────────────

  describe "enforce_limit" do
    it "strips excess markers beyond 4 when more are placed externally" do
      # Build a params with 6 markers already placed
      params = {
        tools: [
          { name: "t1", input_schema: {}, cache_control: { "type" => "ephemeral" } },
          { name: "t2", input_schema: {}, cache_control: { "type" => "ephemeral" } }
        ],
        system: [
          { "type" => "text", "text" => "s1", "cache_control" => { "type" => "ephemeral" } },
          { "type" => "text", "text" => "s2", "cache_control" => { "type" => "ephemeral" } }
        ],
        messages: [
          user_msg([
                     { type: "text", text: "u1", cache_control: { "type" => "ephemeral" } },
                     { type: "text", text: "u2", cache_control: { "type" => "ephemeral" } }
                   ])
        ]
      }
      described_class.apply(params, cache_retention: :none) # skip auto-placement
      expect(count_markers(params)).to be <= 4
    end
  end

  # ── TTL ordering normalization ─────────────────────────────────────────────

  describe "TTL ordering" do
    it "downgrades a later '1h' block to plain ephemeral when an earlier '5m' block exists" do
      params = {
        tools: [{ name: "t1", input_schema: {}, cache_control: { "type" => "ephemeral", "ttl" => "5m" } }],
        system: [{ "type" => "text", "text" => "sys", "cache_control" => { "type" => "ephemeral", "ttl" => "1h" } }],
        messages: []
      }
      described_class.apply(params, cache_retention: :none)
      sys_cc = get_cc(params[:system].last)
      expect(sys_cc["ttl"]).to be_nil
      expect(sys_cc[:ttl]).to be_nil
    end

    it "preserves '1h' on system when tools are not '5m' first" do
      params = {
        tools: [{ name: "t1", input_schema: {}, cache_control: { "type" => "ephemeral", "ttl" => "1h" } }],
        system: [{ "type" => "text", "text" => "sys", "cache_control" => { "type" => "ephemeral", "ttl" => "1h" } }],
        messages: []
      }
      described_class.apply(params, cache_retention: :none)
      sys_cc = get_cc(params[:system].last)
      expect(sys_cc["ttl"]).to eq("1h")
    end

    it "does not mutate an 'ephemeral' block without ttl" do
      params = {
        tools: [],
        system: [{ "type" => "text", "text" => "sys", "cache_control" => { "type" => "ephemeral" } }],
        messages: []
      }
      described_class.apply(params, cache_retention: :none)
      sys_cc = get_cc(params[:system].last)
      expect(sys_cc["type"]).to eq("ephemeral")
    end
  end

  # ── no tools/system ────────────────────────────────────────────────────────

  describe "minimal params (no tools, no system)" do
    it "still marks the last user message when only messages are present" do
      params = {
        messages: [user_msg([text_block("hi")])]
      }
      described_class.apply(params)
      blocks = params[:messages].last[:content]
      expect(blocks.any? { |b| has_cc?(b) }).to be true
    end

    it "does not raise when params has only messages with no blocks" do
      params = { messages: [user_msg([])] }
      expect { described_class.apply(params) }.not_to raise_error
    end
  end

  # ── idempotency on :none after markers exist ───────────────────────────────

  describe "when called multiple times" do
    it "does not add duplicate markers on a second call with :short" do
      params = {
        tools: [tool_hash("bash")],
        messages: [user_msg([text_block("hi")])]
      }
      described_class.apply(params, cache_retention: :short)
      first_count = count_markers(params)

      # The second call: the first user message now has a marker, so caller_placed_markers?
      # returns true — auto-placement skipped. Counts should be stable.
      described_class.apply(params, cache_retention: :short)
      second_count = count_markers(params)

      expect(second_count).to be <= 4
      expect(second_count).to eq(first_count)
    end
  end
end
