# frozen_string_literal: true

RSpec.describe Dispatch::Adapter::Claude::Cloaking do
  describe ".billing_header" do
    it "returns a string starting with 'x-anthropic-billing-header:'" do
      result = described_class.billing_header({})
      expect(result).to start_with("x-anthropic-billing-header:")
    end

    it "ends with a semicolon" do
      result = described_class.billing_header({})
      expect(result).to end_with(";")
    end

    it "contains cc_entrypoint=cli" do
      result = described_class.billing_header({})
      expect(result).to include("cc_entrypoint=cli")
    end

    it "contains cc_version starting with the CLAUDE_CODE_VERSION" do
      result = described_class.billing_header({})
      version = Dispatch::Adapter::Claude::Headers::CLAUDE_CODE_VERSION
      expect(result).to match(/cc_version=#{Regexp.escape(version)}\.[0-9a-f]{3}/)
    end

    it "cc_version suffix is exactly 3 hex characters" do
      result = described_class.billing_header({})
      match = result.match(/cc_version=[^;]+\.([0-9a-f]+);/)
      expect(match).not_to be_nil
      expect(match[1].length).to eq(3)
    end

    it "cch= is exactly 5 hex characters" do
      result = described_class.billing_header({})
      match = result.match(/cch=([0-9a-f]+);/)
      expect(match).not_to be_nil
      expect(match[1].length).to eq(5)
    end

    it "cch is stable for the same input" do
      payload = { "model" => "claude-3-5-sonnet-20241022", "messages" => [] }
      result1 = described_class.billing_header(payload)
      result2 = described_class.billing_header(payload)

      cch1 = result1.match(/cch=([0-9a-f]+)/)[1]
      cch2 = result2.match(/cch=([0-9a-f]+)/)[1]
      expect(cch1).to eq(cch2)
    end

    it "cch differs for different inputs" do
      result1 = described_class.billing_header({ "a" => 1 })
      result2 = described_class.billing_header({ "b" => 2 })

      cch1 = result1.match(/cch=([0-9a-f]+)/)[1]
      cch2 = result2.match(/cch=([0-9a-f]+)/)[1]
      expect(cch1).not_to eq(cch2)
    end

    it "cc_version suffix varies between calls (random build hash)" do
      results = 10.times.map { described_class.billing_header({}) }
      suffixes = results.map { |r| r.match(/cc_version=[^;]+\.([0-9a-f]{3});/)[1] }
      expect(suffixes.uniq.size).to be > 1
    end

    it "handles nil payload gracefully" do
      expect { described_class.billing_header(nil) }.not_to raise_error
      result = described_class.billing_header(nil)
      expect(result).to start_with("x-anthropic-billing-header:")
    end
  end

  describe ".apply_prefix" do
    it "prefixes a regular tool name with 'proxy_'" do
      expect(described_class.apply_prefix("grep")).to eq("proxy_grep")
    end

    it "does not prefix builtin tool names" do
      Dispatch::Adapter::Claude::Cloaking::BUILTINS.each do |builtin|
        expect(described_class.apply_prefix(builtin)).to eq(builtin)
      end
    end

    it "specifically does not prefix web_search" do
      expect(described_class.apply_prefix("web_search")).to eq("web_search")
    end

    it "specifically does not prefix code_execution" do
      expect(described_class.apply_prefix("code_execution")).to eq("code_execution")
    end

    it "is idempotent — does not double-prefix 'proxy_x'" do
      expect(described_class.apply_prefix("proxy_x")).to eq("proxy_x")
    end

    it "is idempotent for already-prefixed names" do
      expect(described_class.apply_prefix("proxy_grep")).to eq("proxy_grep")
    end

    it "handles mixed case builtin check case-insensitively" do
      expect(described_class.apply_prefix("Web_Search")).to eq("Web_Search")
    end

    it "handles case-insensitive already-prefixed check" do
      expect(described_class.apply_prefix("PROXY_foo")).to eq("PROXY_foo")
    end
  end

  describe ".strip_prefix" do
    it "strips 'proxy_' prefix from a prefixed name" do
      expect(described_class.strip_prefix("proxy_grep")).to eq("grep")
    end

    it "returns the name unchanged when not prefixed" do
      expect(described_class.strip_prefix("grep")).to eq("grep")
    end

    it "returns builtin names unchanged" do
      Dispatch::Adapter::Claude::Cloaking::BUILTINS.each do |builtin|
        expect(described_class.strip_prefix(builtin)).to eq(builtin)
      end
    end

    it "is case-insensitive for the prefix check" do
      expect(described_class.strip_prefix("PROXY_foo")).to eq("foo")
    end

    it "only strips the leading prefix, not occurrences elsewhere in the name" do
      expect(described_class.strip_prefix("proxy_proxy_foo")).to eq("proxy_foo")
    end
  end

  describe ".cloaking_user_id?" do
    it "returns true for a freshly generated cloaking user id" do
      id = described_class.generate_cloaking_user_id
      expect(described_class.cloaking_user_id?(id)).to be(true)
    end

    it "returns false for a plain string" do
      expect(described_class.cloaking_user_id?("custom")).to be(false)
    end

    it "returns false for nil" do
      expect(described_class.cloaking_user_id?(nil)).to be(false)
    end

    it "returns false for an integer" do
      expect(described_class.cloaking_user_id?(42)).to be(false)
    end

    it "returns false for a partial match" do
      expect(described_class.cloaking_user_id?("user_abc")).to be(false)
    end
  end

  describe ".generate_cloaking_user_id" do
    it "generates a string matching the USER_ID_REGEX" do
      id = described_class.generate_cloaking_user_id
      expect(id).to match(Dispatch::Adapter::Claude::Cloaking::USER_ID_REGEX)
    end

    it "generates unique ids on consecutive calls" do
      id1 = described_class.generate_cloaking_user_id
      id2 = described_class.generate_cloaking_user_id
      expect(id1).not_to eq(id2)
    end
  end

  describe ".resolve_user_id" do
    it "returns a generated id when provided is nil and is_oauth is true" do
      result = described_class.resolve_user_id(nil, true)
      expect(result).not_to be_nil
      expect(described_class.cloaking_user_id?(result)).to be(true)
    end

    it "returns a generated id when provided is 'custom' and is_oauth is true" do
      result = described_class.resolve_user_id("custom", true)
      expect(result).not_to eq("custom")
      expect(described_class.cloaking_user_id?(result)).to be(true)
    end

    it "returns the provided id when it already matches cloaking format and is_oauth is true" do
      id = described_class.generate_cloaking_user_id
      expect(described_class.resolve_user_id(id, true)).to eq(id)
    end

    it "returns 'custom' when is_oauth is false (passthrough)" do
      expect(described_class.resolve_user_id("custom", false)).to eq("custom")
    end

    it "returns nil when provided is nil and is_oauth is false" do
      expect(described_class.resolve_user_id(nil, false)).to be_nil
    end
  end

  describe ".build_system_blocks" do
    let(:opus_model)  { "claude-opus-4-5" }
    let(:haiku_model) { "claude-3-5-haiku-20241022" }
    let(:user_text)   { "You are a helpful assistant." }

    context "OAuth + non-haiku model (e.g. opus)" do
      it "returns 3 blocks: billing, agent, user" do
        blocks = described_class.build_system_blocks(
          user_text, is_oauth: true, model_id: opus_model
        )
        expect(blocks.size).to eq(3)
        expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:")
        expect(blocks[1]["text"]).to include("Claude agent")
        expect(blocks[2]["text"]).to eq(user_text)
      end

      it "all blocks have type: 'text'" do
        blocks = described_class.build_system_blocks(
          user_text, is_oauth: true, model_id: opus_model
        )
        blocks.each { |b| expect(b["type"]).to eq("text") }
      end
    end

    context "OAuth + haiku-3-5 model" do
      it "returns 2 blocks: billing, user — skips agent instruction" do
        blocks = described_class.build_system_blocks(
          user_text, is_oauth: true, model_id: haiku_model
        )
        expect(blocks.size).to eq(2)
        expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:")
        expect(blocks[1]["text"]).to eq(user_text)
      end
    end

    context "API-key (non-OAuth)" do
      it "returns 1 block: just the user system" do
        blocks = described_class.build_system_blocks(
          user_text, is_oauth: false, model_id: opus_model
        )
        expect(blocks).to be_a(Array)
        expect(blocks.size).to eq(1)
        expect(blocks[0]["text"]).to eq(user_text)
      end

      it "returns nil when user_system is nil and not OAuth" do
        result = described_class.build_system_blocks(
          nil, is_oauth: false, model_id: opus_model
        )
        expect(result).to be_nil
      end
    end

    context "OAuth with nil user_system" do
      it "returns 2 blocks (billing + agent) when model is not haiku" do
        blocks = described_class.build_system_blocks(
          nil, is_oauth: true, model_id: opus_model
        )
        expect(blocks.size).to eq(2)
        expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:")
        expect(blocks[1]["text"]).to include("Claude agent")
      end

      it "returns 1 block (billing only) for haiku model with nil user_system" do
        blocks = described_class.build_system_blocks(
          nil, is_oauth: true, model_id: haiku_model
        )
        expect(blocks.size).to eq(1)
        expect(blocks[0]["text"]).to start_with("x-anthropic-billing-header:")
      end
    end

    context "cache_control" do
      it "attaches cache_control to the last block only" do
        cc = { "type" => "ephemeral" }
        blocks = described_class.build_system_blocks(
          user_text, is_oauth: true, model_id: opus_model, cache_control: cc
        )
        expect(blocks.last["cache_control"]).to eq(cc)
        blocks[0..-2].each { |b| expect(b).not_to have_key("cache_control") }
      end

      it "does not mutate the original blocks when attaching cache_control" do
        cc = { "type" => "ephemeral" }
        original_blocks = [{ "type" => "text", "text" => "original" }]
        described_class.build_system_blocks(
          original_blocks, is_oauth: false, model_id: opus_model, cache_control: cc
        )
        expect(original_blocks.last).not_to have_key("cache_control")
      end
    end

    context "pre-existing billing header short-circuit" do
      it "forwards user_system as-is when it already contains billing header" do
        existing_billing = "x-anthropic-billing-header: cc_version=2.0.0.aaa; cc_entrypoint=cli; cch=abcde;"
        user_blocks = [
          { "type" => "text", "text" => existing_billing },
          { "type" => "text", "text" => "System prompt." }
        ]
        blocks = described_class.build_system_blocks(
          user_blocks, is_oauth: true, model_id: opus_model
        )
        expect(blocks.size).to eq(2)
        expect(blocks[0]["text"]).to eq(existing_billing)
        expect(blocks[1]["text"]).to eq("System prompt.")
      end
    end

    context "Array<TextBlock> as user_system" do
      it "converts TextBlock structs to Hash blocks" do
        text_block = Dispatch::Adapter::TextBlock.new(text: "Structured system prompt")
        blocks = described_class.build_system_blocks(
          [text_block], is_oauth: false, model_id: opus_model
        )
        expect(blocks.last["text"]).to eq("Structured system prompt")
        expect(blocks.last["type"]).to eq("text")
      end
    end
  end
end
