# frozen_string_literal: true

require "webmock/rspec"

RSpec.describe Dispatch::Adapter::Claude, "#count_tokens" do
  let(:model_id)  { "claude-sonnet-4-6" }
  let(:api_key)   { "sk-ant-api03-test" }
  let(:base_url)  { "https://api.anthropic.com" }
  let(:count_tokens_url) { "#{base_url}/v1/messages/count_tokens" }

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

  let(:messages) do
    [Dispatch::Adapter::Message.new(
      role: "user",
      content: [Dispatch::Adapter::TextBlock.new(text: "Hello, how many tokens?")]
    )]
  end

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

  # ── Happy path ────────────────────────────────────────────────────────────

  describe "successful count" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 200,
          body: JSON.generate({ "input_tokens" => 1234 }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "returns the integer token count" do
      expect(adapter.count_tokens(messages)).to eq(1234)
    end

    it "posts to /v1/messages/count_tokens (not /v1/messages)" do
      adapter.count_tokens(messages)
      expect(WebMock).to have_requested(:post, count_tokens_url)
      expect(WebMock).not_to have_requested(:post, "#{base_url}/v1/messages")
    end

    it "includes the model in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| JSON.parse(req.body)["model"] == model_id })
    end

    it "includes messages in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| JSON.parse(req.body)["messages"].is_a?(Array) })
    end
  end

  # ── Unsupported fields stripped ───────────────────────────────────────────

  describe "stripped fields" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 200,
          body: JSON.generate({ "input_tokens" => 42 }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "does NOT include 'stream' in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| !JSON.parse(req.body).key?("stream") })
    end

    it "does NOT include 'max_tokens' in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| !JSON.parse(req.body).key?("max_tokens") })
    end

    it "does NOT include 'metadata' in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| !JSON.parse(req.body).key?("metadata") })
    end

    it "does NOT include 'output_config' in the request body" do
      adapter.count_tokens(messages)
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with { |req| !JSON.parse(req.body).key?("output_config") })
    end
  end

  # ── With tools ────────────────────────────────────────────────────────────

  describe "with tools" do
    let(:tool) do
      Dispatch::Adapter::ToolDefinition.new(
        name: "bash",
        description: "Run a bash command",
        parameters: {
          "type" => "object",
          "properties" => { "command" => { "type" => "string" } },
          "required" => ["command"]
        }
      )
    end

    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 200,
          body: JSON.generate({ "input_tokens" => 500 }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "includes tools in the request body" do
      adapter.count_tokens(messages, tools: [tool])
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with do |req|
          body = JSON.parse(req.body)
          body.key?("tools") && body["tools"].is_a?(Array) && body["tools"].length == 1
        end)
    end

    it "returns the token count" do
      expect(adapter.count_tokens(messages, tools: [tool])).to eq(500)
    end
  end

  # ── With system prompt ────────────────────────────────────────────────────

  describe "with system prompt" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 200,
          body: JSON.generate({ "input_tokens" => 300 }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "includes system in the request body" do
      adapter.count_tokens(messages, system: "You are a helpful assistant")
      expect(WebMock).to(have_requested(:post, count_tokens_url)
        .with do |req|
          body = JSON.parse(req.body)
          body.key?("system")
        end)
    end

    it "returns the token count" do
      expect(adapter.count_tokens(messages, system: "Be helpful")).to eq(300)
    end
  end

  # ── Error handling ────────────────────────────────────────────────────────

  describe "network failure" do
    before do
      stub_request(:post, count_tokens_url).to_raise(Errno::ECONNREFUSED)
    end

    it "returns -1 without raising" do
      expect(adapter.count_tokens(messages)).to eq(-1)
    end
  end

  describe "HTTP error response" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 401,
          body: JSON.generate({ "error" => { "message" => "Unauthorized" } }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "returns -1 on AuthenticationError" do
      expect(adapter.count_tokens(messages)).to eq(-1)
    end
  end

  describe "server error" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(
          status: 500,
          body: JSON.generate({ "error" => { "message" => "Internal error" } }),
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "returns -1 on ServerError" do
      expect(adapter.count_tokens(messages)).to eq(-1)
    end
  end

  describe "malformed JSON response" do
    before do
      stub_request(:post, count_tokens_url)
        .to_return(status: 200, body: "not json", headers: {})
    end

    it "returns -1 on JSON parse error" do
      expect(adapter.count_tokens(messages)).to eq(-1)
    end
  end

  # ── Default -1 from Base ──────────────────────────────────────────────────

  describe "Base#count_tokens default" do
    it "returns -1 as the default implementation in Base" do
      base = Dispatch::Adapter::Base.new
      expect(base.count_tokens([])).to eq(-1)
    end
  end
end
