# frozen_string_literal: true

require "webmock/rspec"

CHAT_NON_STREAMING_FIXTURES_DIR = File.expand_path("../../../fixtures/responses", __dir__)

RSpec.describe Dispatch::Adapter::Claude, "#chat (non-streaming integration)" do
  def load_fixture(filename)
    JSON.parse(File.read(File.join(CHAT_NON_STREAMING_FIXTURES_DIR, filename)))
  end

  let(:model_id) { "claude-sonnet-4-5-20250929" }
  let(:base_url) { "https://api.anthropic.com" }

  let(:messages) do
    [Dispatch::Adapter::Message.new(
      role: "user",
      content: [Dispatch::Adapter::TextBlock.new(text: "What is the capital of France?")]
    )]
  end

  def make_adapter(api_key: "sk-ant-api03-test")
    described_class.new(
      model: model_id,
      api_key: api_key,
      base_url: base_url
    )
  end

  def make_oauth_adapter
    # Build a temporary token store with a fake OAuth credential
    tmpdir = Dir.mktmpdir
    token_path = File.join(tmpdir, "oauth.json")
    creds = {
      "access_token" => "sk-ant-oat01-fake-oauth-token",
      "refresh_token" => "rt-fake",
      "expires_at_ms" => ((Time.now.to_f * 1000) + 3_600_000).to_i,
      "account_id" => nil,
      "email" => nil
    }
    File.write(token_path, JSON.generate(creds))

    described_class.new(
      model: model_id,
      token_path: token_path,
      base_url: base_url
    )
  end

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

  def stub_messages_fixture(filename, status: 200, extra_headers: {})
    body = File.read(File.join(CHAT_NON_STREAMING_FIXTURES_DIR, filename))
    stub_request(:post, "#{base_url}/v1/messages")
      .to_return(
        status: status,
        body: body,
        headers: { "Content-Type" => "application/json" }.merge(extra_headers)
      )
  end

  def stub_error(status:, message:, error_type: "invalid_request_error", extra_headers: {})
    stub_request(:post, "#{base_url}/v1/messages")
      .to_return(
        status: status,
        body: JSON.generate({ "type" => "error", "error" => { "type" => error_type, "message" => message } }),
        headers: { "Content-Type" => "application/json" }.merge(extra_headers)
      )
  end

  # ── Scenario 1: text-only ────────────────────────────────────────────────────

  describe "scenario 1: text-only (messages-text.json)" do
    let(:fixture)  { load_fixture("messages-text.json") }
    let(:adapter)  { make_adapter }

    before { stub_messages_fixture("messages-text.json") }

    it "returns a Response" do
      expect(adapter.chat(messages)).to be_a(Dispatch::Adapter::Response)
    end

    it "has stop_reason :end_turn" do
      expect(adapter.chat(messages).stop_reason).to eq(:end_turn)
    end

    it "has content with one TextBlock" do
      content = adapter.chat(messages).content
      expect(content.length).to eq(1)
      expect(content.first).to be_a(Dispatch::Adapter::TextBlock)
    end

    it "has the correct text from the fixture" do
      expect(adapter.chat(messages).content.first.text).to eq(fixture["content"][0]["text"])
    end

    it "has non-zero input_tokens" do
      expect(adapter.chat(messages).usage.input_tokens).to eq(fixture["usage"]["input_tokens"])
    end

    it "has non-zero output_tokens" do
      expect(adapter.chat(messages).usage.output_tokens).to eq(fixture["usage"]["output_tokens"])
    end

    it "usage.cost is a UsageCost with positive total" do
      cost = adapter.chat(messages).usage.cost
      expect(cost).to be_a(Dispatch::Adapter::UsageCost)
      expect(cost.total).to be > 0
    end
  end

  # ── Scenario 2: tool_use with proxy_ stripping ───────────────────────────────

  describe "scenario 2: tool_use (messages-tool-use.json, OAuth → proxy_ stripped)" do
    let(:fixture)  { load_fixture("messages-tool-use.json") }
    let(:adapter)  { make_oauth_adapter }

    before { stub_messages_fixture("messages-tool-use.json") }

    it "has stop_reason :tool_use" do
      expect(adapter.chat(messages).stop_reason).to eq(:tool_use)
    end

    it "has one tool_call in tool_calls" do
      expect(adapter.chat(messages).tool_calls.length).to eq(1)
    end

    it "tool_call is a ToolUseBlock" do
      tc = adapter.chat(messages).tool_calls.first
      expect(tc).to be_a(Dispatch::Adapter::ToolUseBlock)
    end

    it "tool_call id matches fixture" do
      tc = adapter.chat(messages).tool_calls.first
      expect(tc.id).to eq(fixture["content"][0]["id"])
    end

    it "tool_call name has proxy_ stripped (OAuth mode)" do
      tc = adapter.chat(messages).tool_calls.first
      # fixture has "proxy_bash", OAuth stripping removes "proxy_"
      expect(tc.name).to eq("bash")
      expect(tc.name).not_to start_with("proxy_")
    end

    it "tool_call arguments match fixture input" do
      tc = adapter.chat(messages).tool_calls.first
      expect(tc.arguments).to eq(fixture["content"][0]["input"])
    end

    it "has empty content (tool_use goes to tool_calls)" do
      expect(adapter.chat(messages).content).to be_empty
    end
  end

  # ── Scenario 3: with-thinking ────────────────────────────────────────────────

  describe "scenario 3: with-thinking (messages-with-thinking.json)" do
    let(:fixture)  { load_fixture("messages-with-thinking.json") }
    let(:adapter)  { make_adapter }

    before { stub_messages_fixture("messages-with-thinking.json") }

    it "has stop_reason :end_turn" do
      expect(adapter.chat(messages).stop_reason).to eq(:end_turn)
    end

    it "has 2 content items (thinking + text)" do
      expect(adapter.chat(messages).content.length).to eq(2)
    end

    it "first content item is a ThinkingBlock" do
      content = adapter.chat(messages).content
      expect(content[0]).to be_a(Dispatch::Adapter::ThinkingBlock)
    end

    it "thinking block has the correct thinking text" do
      content = adapter.chat(messages).content
      expect(content[0].thinking).to eq(fixture["content"][0]["thinking"])
    end

    it "thinking block has the correct signature" do
      content = adapter.chat(messages).content
      expect(content[0].signature).to eq(fixture["content"][0]["signature"])
    end

    it "second content item is a TextBlock" do
      content = adapter.chat(messages).content
      expect(content[1]).to be_a(Dispatch::Adapter::TextBlock)
    end

    it "text block has the correct text" do
      content = adapter.chat(messages).content
      expect(content[1].text).to eq(fixture["content"][1]["text"])
    end
  end

  # ── Scenario 4: error mapping ────────────────────────────────────────────────

  describe "scenario 4: error mapping" do
    let(:adapter) { make_adapter }

    it "401 → AuthenticationError" do
      stub_error(status: 401, message: "Invalid API key")
      expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::AuthenticationError)
    end

    it "401 AuthenticationError has status_code 401" do
      stub_error(status: 401, message: "Invalid API key")
      begin
        adapter.chat(messages)
      rescue Dispatch::Adapter::AuthenticationError => e
        expect(e.status_code).to eq(401)
      end
    end

    it "429 → RateLimitError" do
      stub_error(status: 429, message: "Rate limit exceeded")
      expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RateLimitError)
    end

    it "429 with Retry-After: 30 → RateLimitError(retry_after: 30)" do
      stub_error(status: 429, message: "Rate limit exceeded",
                 extra_headers: { "Retry-After" => "30" })
      begin
        adapter.chat(messages)
      rescue Dispatch::Adapter::RateLimitError => e
        expect(e.retry_after).to eq(30)
      end
    end

    it "529 → OverloadedError" do
      stub_error(status: 529, message: "Overloaded")
      expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::OverloadedError)
    end

    it "529 with Retry-After: 10 → OverloadedError(retry_after: 10)" do
      stub_error(status: 529, message: "Overloaded",
                 extra_headers: { "Retry-After" => "10" })
      begin
        adapter.chat(messages)
      rescue Dispatch::Adapter::OverloadedError => e
        expect(e.retry_after).to eq(10)
      end
    end

    it "500 → ServerError" do
      stub_error(status: 500, message: "Internal server error")
      expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ServerError)
    end

    it "WebMock prevents any real network access" do
      stub_error(status: 200, message: "OK")
      # WebMock is already enabled; just confirm the adapter works without hitting the real API
      expect { stub_error(status: 200, message: "OK") }.not_to raise_error
    end
  end
end
