# frozen_string_literal: true

require "rails_helper"

RSpec.describe WebhookDeliveryService do
  let(:server) { create(:server) }
  let(:webhook) { create(:webhook, server: server) }
  let(:webhook_request) { create(:webhook_request, :locked, webhook: webhook) }

  subject(:service) { described_class.new(webhook_request: webhook_request) }

  let(:response_status) { 200 }
  let(:response_body) { "OK" }

  before do
    allow(Resolv).to receive(:getaddresses).with("example.com").and_return(["93.184.216.34"])
    stub_request(:post, webhook.url).to_return(status: response_status, body: response_body)
  end

  describe "#call" do
    it "sends a request to the webhook's url" do
      service.call
      expect(WebMock).to have_requested(:post, webhook.url).with({
        body: {
          event: webhook_request.event,
          timestamp: webhook_request.created_at.to_f,
          payload: webhook_request.payload,
          uuid: webhook_request.uuid
        }.to_json,
        headers: {
          "Content-Type" => "application/json",
          "X-Postal-Signature" => /\A[a-z0-9\/+]+=*\z/i,
          "X-Postal-Signature-256" => /\A[a-z0-9\/+]+=*\z/i,
          "X-Postal-Signature-KID" => /\A[a-f0-9\/+]{64}\z/i
        }
      })
    end

    context "when the endpoint returns a 200 OK" do
      it "creates a webhook request for the server" do
        service.call
        expect(server.message_db.webhooks.list(1)[:total]).to eq(1)
        webhook_request = server.message_db.webhooks.list(1)[:records].first
        expect(webhook_request).to have_attributes(
          event: webhook_request.event,
          url: webhook_request.url,
          status_code: 200,
          body: "OK",
          uuid: webhook_request.uuid,
          will_retry?: false,
          payload: webhook_request.payload,
          attempt: 1,
          timestamp: webhook_request.timestamp
        )
      end

      it "deletes the webhook request" do
        service.call
        expect { webhook_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end

      it "updates the last used at time on the webhook" do
        frozen_time = Time.current.change(usec: 0)
        Timecop.freeze(frozen_time) do
          service.call
          expect(webhook.reload.last_used_at).to eq(frozen_time)
        end
      end
    end

    context "when the request returns a 500 Internal Server Error for the first time" do
      let(:response_status) { 500 }
      let(:response_body) { "internal server error!" }

      it "unlocks the webhook request if locked" do
        expect { service.call }.to change { webhook_request.reload.locked? }.from(true).to(false)
      end

      it "updates the retry time and attempt counter" do
        service.call
        expect(webhook_request.reload.attempts).to eq(1)
        expect(webhook_request.retry_after).to be_within(1.second).of(2.minutes.from_now)
      end
    end

    context "when the request returns a 500 Internal Server Error for the second time" do
      let(:webhook_request) { create(:webhook_request, :locked, webhook: webhook, attempts: 1) }
      let(:response_status) { 500 }
      let(:response_body) { "internal server error!" }

      it "updates the retry time and attempt counter" do
        service.call
        expect(webhook_request.reload.attempts).to eq(2)
        expect(webhook_request.retry_after).to be_within(1.second).of(3.minutes.from_now)
      end
    end

    context "when the request returns a 500 Internal Server Error for the sixth time" do
      let(:webhook_request) { create(:webhook_request, :locked, webhook: webhook, attempts: 5) }
      let(:response_status) { 500 }
      let(:response_body) { "internal server error!" }

      it "creates a webhook request for the server" do
        service.call
        expect(server.message_db.webhooks.list(1)[:total]).to eq(1)
        webhook_request = server.message_db.webhooks.list(1)[:records].first
        expect(webhook_request).to have_attributes(
          status_code: 500,
          body: "internal server error!",
          will_retry?: false,
          attempt: 6
        )
      end

      it "deletes the webhook request" do
        service.call
        expect { webhook_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    context "when the webhook URL resolves to a blocked (private) address" do
      let(:webhook_request) do
        create(:webhook_request, :locked, webhook: webhook, url: "http://internal.example.com/hook")
      end

      before do
        allow(Resolv).to receive(:getaddresses).with("internal.example.com").and_return(["127.0.0.1"])
      end

      it "does not make a request to the destination" do
        service.call
        expect(WebMock).not_to have_requested(:post, "http://internal.example.com/hook")
      end

      it "records the failure and schedules a retry" do
        service.call
        expect(webhook_request.reload.attempts).to eq(1)
        expect(webhook_request.retry_after).to be_present
      end
    end
  end
end
