# frozen_string_literal: true

require "webmock/rspec"

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

  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")]
    )]
  end

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

  # ── Fixtures ────────────────────────────────────────────────────────────────

  let(:grammar_error_body) do
    JSON.generate(
      "type" => "error",
      "error" => {
        "type" => "invalid_request_error",
        "message" => "The compiled grammar is too large. Please simplify your tool schema."
      }
    )
  end

  let(:complex_schema_error_body) do
    JSON.generate(
      "type" => "error",
      "error" => {
        "type" => "invalid_request_error",
        "message" => "Schema is too complex to compil for this request."
      }
    )
  end

  let(:unrelated_400_body) do
    JSON.generate(
      "type" => "error",
      "error" => {
        "type" => "invalid_request_error",
        "message" => "messages: roles must alternate between \"user\" and \"assistant\""
      }
    )
  end

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

  # ── Non-streaming: grammar-too-large fallback ────────────────────────────────

  describe "#chat (non-streaming) — grammar error → fallback" do
    context "when the first request returns 'compiled grammar too large'" do
      before do
        stub_request(:post, "#{base_url}/v1/messages")
          .to_return(
            { status: 400, body: grammar_error_body,
              headers: { "Content-Type" => "application/json" } },
            { status: 200, body: JSON.generate(success_response),
              headers: { "Content-Type" => "application/json" } }
          )
      end

      it "retries and returns a successful Response" do
        response = adapter.chat(messages)
        expect(response).to be_a(Dispatch::Adapter::Response)
        expect(response.stop_reason).to eq(:end_turn)
      end

      it "sets @strict_disabled to true after the error" do
        adapter.chat(messages)
        expect(adapter.instance_variable_get(:@strict_disabled)).to be(true)
      end

      it "makes exactly two HTTP requests" do
        adapter.chat(messages)
        expect(WebMock).to have_requested(:post, "#{base_url}/v1/messages").twice
      end
    end

    context "when the first request returns 'schema too complex to compil'" do
      before do
        stub_request(:post, "#{base_url}/v1/messages")
          .to_return(
            { status: 400, body: complex_schema_error_body,
              headers: { "Content-Type" => "application/json" } },
            { status: 200, body: JSON.generate(success_response),
              headers: { "Content-Type" => "application/json" } }
          )
      end

      it "retries and returns a successful Response" do
        response = adapter.chat(messages)
        expect(response).to be_a(Dispatch::Adapter::Response)
      end

      it "sets @strict_disabled to true" do
        adapter.chat(messages)
        expect(adapter.instance_variable_get(:@strict_disabled)).to be(true)
      end
    end

    context "when both first and retry requests fail with grammar error" do
      before do
        stub_request(:post, "#{base_url}/v1/messages")
          .to_return(
            status: 400, body: grammar_error_body,
            headers: { "Content-Type" => "application/json" }
          )
      end

      it "raises RequestError after the retry also fails" do
        expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RequestError)
      end
    end
  end

  # ── Non-streaming: @strict_disabled persists for subsequent calls ────────────

  describe "#chat (non-streaming) — @strict_disabled persists" do
    before do
      # First call: 400 grammar error → retry → success
      # Second call: success immediately (strict already disabled)
      stub_request(:post, "#{base_url}/v1/messages")
        .to_return(
          { status: 400, body: grammar_error_body,
            headers: { "Content-Type" => "application/json" } },
          { status: 200, body: JSON.generate(success_response),
            headers: { "Content-Type" => "application/json" } },
          { status: 200, body: JSON.generate(success_response),
            headers: { "Content-Type" => "application/json" } }
        )
    end

    it "@strict_disabled remains true after the first fallback call" do
      adapter.chat(messages) # triggers fallback → sets @strict_disabled = true
      expect(adapter.instance_variable_get(:@strict_disabled)).to be(true)

      adapter.chat(messages) # subsequent call — should still be true
      expect(adapter.instance_variable_get(:@strict_disabled)).to be(true)
    end

    it "does not re-trigger the grammar fallback on the second call (only 3 requests total)" do
      adapter.chat(messages)
      adapter.chat(messages)
      # 1st call: [400 → retry 200] = 2 requests; 2nd call: [200] = 1 request → 3 total
      expect(WebMock).to have_requested(:post, "#{base_url}/v1/messages").times(3)
    end
  end

  # ── Non-streaming: non-grammar 400 errors are NOT retried ───────────────────

  describe "#chat (non-streaming) — unrelated 400 is NOT retried" do
    before do
      stub_request(:post, "#{base_url}/v1/messages")
        .to_return(
          status: 400, body: unrelated_400_body,
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "raises RequestError without retrying" do
      expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RequestError)
    end

    it "makes only one HTTP request" do
      adapter.chat(messages)
    rescue Dispatch::Adapter::RequestError
      nil
    ensure
      expect(WebMock).to have_requested(:post, "#{base_url}/v1/messages").once
    end

    it "does NOT set @strict_disabled" do
      begin
        adapter.chat(messages)
      rescue Dispatch::Adapter::RequestError
        nil
      end
      expect(adapter.instance_variable_get(:@strict_disabled)).to be(false)
    end
  end

  # ── Streaming: grammar-too-large fallback ────────────────────────────────────

  describe "#chat (streaming) — grammar error → fallback" do
    let(:complete_sse_stream) do
      <<~SSE
        event: message_start
        data: {"type":"message_start","message":{"id":"msg_01","model":"#{model_id}","usage":{"input_tokens":10,"output_tokens":0}}}

        event: content_block_start
        data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

        event: content_block_delta
        data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"}}

        event: content_block_stop
        data: {"type":"content_block_stop","index":0}

        event: message_delta
        data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}

        event: message_stop
        data: {"type":"message_stop"}

      SSE
    end

    before do
      allow(adapter).to receive(:sleep) # suppress real sleeps

      stub_request(:post, "#{base_url}/v1/messages")
        .to_return(
          { status: 400, body: grammar_error_body,
            headers: { "Content-Type" => "application/json" } },
          { status: 200, body: complete_sse_stream,
            headers: { "Content-Type" => "text/event-stream" } }
        )
    end

    it "retries and returns a successful Response" do
      response = adapter.chat(messages, stream: true)
      expect(response).to be_a(Dispatch::Adapter::Response)
      expect(response.stop_reason).to eq(:end_turn)
    end

    it "sets @strict_disabled to true" do
      adapter.chat(messages, stream: true)
      expect(adapter.instance_variable_get(:@strict_disabled)).to be(true)
    end

    it "makes exactly two HTTP requests" do
      adapter.chat(messages, stream: true)
      expect(WebMock).to have_requested(:post, "#{base_url}/v1/messages").twice
    end
  end

  # ── Streaming: non-grammar 400 is NOT retried ────────────────────────────────

  describe "#chat (streaming) — unrelated 400 is NOT retried" do
    before do
      allow(adapter).to receive(:sleep)

      stub_request(:post, "#{base_url}/v1/messages")
        .to_return(
          status: 400, body: unrelated_400_body,
          headers: { "Content-Type" => "application/json" }
        )
    end

    it "raises RequestError without retrying" do
      expect { adapter.chat(messages, stream: true) }.to raise_error(Dispatch::Adapter::RequestError)
    end

    it "does NOT set @strict_disabled" do
      begin
        adapter.chat(messages, stream: true)
      rescue Dispatch::Adapter::RequestError
        nil
      end
      expect(adapter.instance_variable_get(:@strict_disabled)).to be(false)
    end
  end

  # ── strict_grammar_error? unit tests ─────────────────────────────────────────

  describe "#strict_grammar_error? (private)" do
    def grammar_error(msg)
      Dispatch::Adapter::RequestError.new(msg, status_code: 400, provider: "Anthropic (Claude)")
    end

    it "returns true for 'compiled grammar ... too large'" do
      err = grammar_error("The compiled grammar is too large.")
      expect(adapter.send(:strict_grammar_error?, err)).to be(true)
    end

    it "returns true for 'schema ... too complex ... compil'" do
      err = grammar_error("Schema is too complex to compil for this tool.")
      expect(adapter.send(:strict_grammar_error?, err)).to be(true)
    end

    it "returns false for an unrelated 400" do
      err = grammar_error("roles must alternate between user and assistant")
      expect(adapter.send(:strict_grammar_error?, err)).to be(false)
    end

    it "returns false for a non-400 RequestError" do
      err = Dispatch::Adapter::RequestError.new("Bad request", status_code: 422,
                                                               provider: "Anthropic (Claude)")
      expect(adapter.send(:strict_grammar_error?, err)).to be(false)
    end

    it "returns false for a non-RequestError exception" do
      expect(adapter.send(:strict_grammar_error?, RuntimeError.new("boom"))).to be(false)
    end

    it "returns false for nil" do
      expect(adapter.send(:strict_grammar_error?, nil)).to be(false)
    end
  end
end
