# frozen_string_literal: true

require "webmock/rspec"

RSpec.describe Dispatch::Adapter::Claude, "auth lifecycle" do
  let(:tmpdir)     { Dir.mktmpdir("auth_lifecycle_test") }
  let(:store_path) { File.join(tmpdir, "claude_oauth.json") }
  let(:store)      { Dispatch::Adapter::Claude::TokenStore.new(path: store_path) }

  after { FileUtils.rm_rf(tmpdir) }

  def make_adapter(api_key: nil)
    described_class.new(
      model: "claude-sonnet-4-6",
      api_key: api_key,
      token_store: store
    )
  end

  def future_ms(seconds_from_now = 3600)
    ((Time.now.to_f + seconds_from_now) * 1000).to_i
  end

  def past_ms(seconds_ago = 3600)
    ((Time.now.to_f - seconds_ago) * 1000).to_i
  end

  # ── authenticate! — explicit API key ─────────────────────────────────────

  describe "#authenticate! with an explicit API key" do
    subject(:adapter) { make_adapter(api_key: "sk-ant-api03-test") }

    it "returns :api_key" do
      expect(adapter.authenticate!).to eq(:api_key)
    end

    it "does not load the token store" do
      expect(store).not_to receive(:load)
      adapter.authenticate!
    end
  end

  # ── authenticate! — valid cached token ───────────────────────────────────

  describe "#authenticate! with a valid cached token" do
    before do
      store.save(
        "access_token" => "sk-ant-oat-cached",
        "refresh_token" => "rt-cached",
        "expires_at_ms" => future_ms
      )
    end

    subject(:adapter) { make_adapter }

    it "returns :cached" do
      expect(adapter.authenticate!).to eq(:cached)
    end

    it "does not call OAuth.refresh!" do
      expect(Dispatch::Adapter::Claude::OAuth).not_to receive(:refresh!)
      adapter.authenticate!
    end

    it "does not call OAuth.login" do
      expect(Dispatch::Adapter::Claude::OAuth).not_to receive(:login)
      adapter.authenticate!
    end
  end

  # ── authenticate! — expired token with refresh_token ─────────────────────

  describe "#authenticate! with an expired token and a refresh_token" do
    before do
      store.save(
        "access_token" => "sk-ant-oat-expired",
        "refresh_token" => "rt-old",
        "expires_at_ms" => past_ms
      )

      allow(Dispatch::Adapter::Claude::OAuth).to receive(:refresh!)
        .with("rt-old")
        .and_return(
          "access_token" => "sk-ant-oat-refreshed",
          "refresh_token" => "rt-new",
          "expires_at_ms" => future_ms
        )
    end

    subject(:adapter) { make_adapter }

    it "returns :refreshed" do
      expect(adapter.authenticate!).to eq(:refreshed)
    end

    it "calls OAuth.refresh! with the stored refresh_token" do
      expect(Dispatch::Adapter::Claude::OAuth).to receive(:refresh!).with("rt-old")
                                                                    .and_return(
                                                                      "access_token" => "sk-ant-oat-refreshed",
                                                                      "refresh_token" => "rt-new",
                                                                      "expires_at_ms" => future_ms
                                                                    )
      adapter.authenticate!
    end

    it "saves the refreshed credentials to the store" do
      adapter.authenticate!
      expect(store.load["access_token"]).to eq("sk-ant-oat-refreshed")
    end

    it "updates @api_key to the new access token" do
      adapter.authenticate!
      # Verify by calling authenticated? — it should return true
      expect(adapter.authenticated?).to be true
    end
  end

  # ── authenticate! — no credentials → interactive login ───────────────────

  describe "#authenticate! with no stored credentials" do
    subject(:adapter) { make_adapter }

    before do
      allow(Dispatch::Adapter::Claude::OAuth).to receive(:login)
        .and_return(
          "access_token" => "sk-ant-oat-fresh",
          "refresh_token" => "rt-fresh",
          "expires_at_ms" => future_ms
        )
    end

    it "returns :logged_in" do
      expect(adapter.authenticate!).to eq(:logged_in)
    end

    it "calls OAuth.login with the token_store" do
      expect(Dispatch::Adapter::Claude::OAuth).to receive(:login)
        .with(token_store: store)
      adapter.authenticate!
    end

    it "saves credentials after login" do
      adapter.authenticate!
      expect(store.load["access_token"]).to eq("sk-ant-oat-fresh")
    end
  end

  # ── authenticated? ────────────────────────────────────────────────────────

  describe "#authenticated?" do
    context "with an explicit API key" do
      subject(:adapter) { make_adapter(api_key: "sk-ant-api03-test") }

      it "returns true" do
        expect(adapter.authenticated?).to be true
      end
    end

    context "with a stored token" do
      before do
        store.save(
          "access_token" => "sk-ant-oat-stored",
          "refresh_token" => "rt-stored",
          "expires_at_ms" => future_ms
        )
      end

      subject(:adapter) { make_adapter }

      it "returns true" do
        expect(adapter.authenticated?).to be true
      end
    end

    context "with no credentials" do
      subject(:adapter) { make_adapter }

      it "returns false" do
        expect(adapter.authenticated?).to be false
      end
    end
  end

  # ── logout! ───────────────────────────────────────────────────────────────

  describe "#logout!" do
    before do
      store.save(
        "access_token" => "sk-ant-oat-live",
        "refresh_token" => "rt-live",
        "expires_at_ms" => future_ms
      )
    end

    subject(:adapter) { make_adapter }

    it "deletes the token file so authenticated? returns false" do
      expect(adapter.authenticated?).to be true
      adapter.logout!
      expect(adapter.authenticated?).to be false
    end

    it "returns nil" do
      expect(adapter.logout!).to be_nil
    end

    it "removes the token store file" do
      expect(File.exist?(store_path)).to be true
      adapter.logout!
      expect(File.exist?(store_path)).to be false
    end
  end

  # ── ensure_token! (lazy refresh) ─────────────────────────────────────────

  describe "ensure_token! lazy refresh" do
    before { WebMock.disable_net_connect! }
    after  { WebMock.reset! }

    context "with an expired token in the store" do
      before do
        store.save(
          "access_token" => "sk-ant-oat-expired",
          "refresh_token" => "rt-expired",
          "expires_at_ms" => past_ms
        )

        stub_request(:post, "https://api.anthropic.com/v1/oauth/token")
          .to_return(
            status: 200,
            body: JSON.generate(
              "access_token" => "sk-ant-oat-new",
              "refresh_token" => "rt-new",
              "expires_in" => 3600
            ),
            headers: { "Content-Type" => "application/json" }
          )

        # Stub the actual messages request
        stub_request(:post, "https://api.anthropic.com/v1/messages")
          .to_return(
            status: 200,
            body: JSON.generate(
              "id" => "msg_01",
              "type" => "message",
              "model" => "claude-sonnet-4-6",
              "role" => "assistant",
              "stop_reason" => "end_turn",
              "content" => [{ "type" => "text", "text" => "Hi" }],
              "usage" => { "input_tokens" => 5, "output_tokens" => 3 }
            ),
            headers: { "Content-Type" => "application/json" }
          )
      end

      subject(:adapter) { make_adapter }

      it "refreshes the token before making a chat request" do
        messages = [Dispatch::Adapter::Message.new(
          role: "user",
          content: [Dispatch::Adapter::TextBlock.new(text: "Hello")]
        )]
        expect { adapter.chat(messages) }.not_to raise_error
        # The token store should now hold the refreshed token
        expect(store.load["access_token"]).to eq("sk-ant-oat-new")
      end
    end

    context "with a non-expired token" do
      before do
        store.save(
          "access_token" => "sk-ant-oat-valid",
          "refresh_token" => "rt-valid",
          "expires_at_ms" => future_ms
        )
      end

      subject(:adapter) { make_adapter }

      it "does not refresh when the token is still valid" do
        expect(Dispatch::Adapter::Claude::OAuth).not_to receive(:refresh!)
        # Call ensure_token! indirectly via a method that uses it
        adapter.send(:ensure_token!)
      end
    end
  end
end
