text:
package com.github.messenger4j.test.integration;

import static com.github.messenger4j.send.message.richmedia.RichMediaAsset.Type.IMAGE;
import static com.github.messenger4j.spi.MessengerHttpClient.HttpMethod.POST;
import static java.util.Collections.singletonList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.github.messenger4j.Messenger;
import com.github.messenger4j.common.WebviewHeightRatio;
import com.github.messenger4j.common.WebviewShareButtonState;
import com.github.messenger4j.exception.MessengerApiException;
import com.github.messenger4j.send.MessagePayload;
import com.github.messenger4j.send.MessageResponse;
import com.github.messenger4j.send.MessageTag;
import com.github.messenger4j.send.MessagingType;
import com.github.messenger4j.send.NotificationType;
import com.github.messenger4j.send.SenderActionPayload;
import com.github.messenger4j.send.message.RichMediaMessage;
import com.github.messenger4j.send.message.TemplateMessage;
import com.github.messenger4j.send.message.TextMessage;
import com.github.messenger4j.send.message.quickreply.LocationQuickReply;
import com.github.messenger4j.send.message.quickreply.QuickReply;
import com.github.messenger4j.send.message.quickreply.TextQuickReply;
import com.github.messenger4j.send.message.quickreply.UserEmailQuickReply;
import com.github.messenger4j.send.message.quickreply.UserPhoneNumberQuickReply;
import com.github.messenger4j.send.message.richmedia.ReusableRichMediaAsset;
import com.github.messenger4j.send.message.richmedia.UrlRichMediaAsset;
import com.github.messenger4j.send.message.template.ButtonTemplate;
import com.github.messenger4j.send.message.template.GenericTemplate;
import com.github.messenger4j.send.message.template.ListTemplate;
import com.github.messenger4j.send.message.template.ListTemplate.TopElementStyle;
import com.github.messenger4j.send.message.template.OpenGraphTemplate;
import com.github.messenger4j.send.message.template.ReceiptTemplate;
import com.github.messenger4j.send.message.template.button.Button;
import com.github.messenger4j.send.message.template.button.CallButton;
import com.github.messenger4j.send.message.template.button.LogInButton;
import com.github.messenger4j.send.message.template.button.LogOutButton;
import com.github.messenger4j.send.message.template.button.PostbackButton;
import com.github.messenger4j.send.message.template.button.ShareButton;
import com.github.messenger4j.send.message.template.button.UrlButton;
import com.github.messenger4j.send.message.template.common.DefaultAction;
import com.github.messenger4j.send.message.template.common.Element;
import com.github.messenger4j.send.message.template.opengraph.OpenGraphObject;
import com.github.messenger4j.send.message.template.receipt.Address;
import com.github.messenger4j.send.message.template.receipt.Adjustment;
import com.github.messenger4j.send.message.template.receipt.Item;
import com.github.messenger4j.send.message.template.receipt.Summary;
import com.github.messenger4j.send.recipient.IdRecipient;
import com.github.messenger4j.send.recipient.PhoneNumberRecipient;
import com.github.messenger4j.send.recipient.Recipient;
import com.github.messenger4j.send.recipient.UserRefRecipient;
import com.github.messenger4j.send.senderaction.SenderAction;
import com.github.messenger4j.spi.MessengerHttpClient;
import com.github.messenger4j.spi.MessengerHttpClient.HttpMethod;
import com.github.messenger4j.spi.MessengerHttpClient.HttpResponse;
import java.net.URL;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.skyscreamer.jsonassert.JSONAssert;

/**
 * @author Max Grabenhorst
 * @since 1.0.0
 */
@Slf4j
class SendTest {

  private static final String PAGE_ACCESS_TOKEN = "PAGE_ACCESS_TOKEN";

  private final MessengerHttpClient mockHttpClient = mock(MessengerHttpClient.class);
  private final HttpResponse fakeResponse =
      new HttpResponse(
          200,
          "{\n"
              + "  \"recipient_id\": \"USER_ID\",\n"
              + "  \"message_id\": \"mid.1473372944816:94f72b88c597657974\"\n"
              + "}");

  private final Messenger messenger =
      Messenger.create(PAGE_ACCESS_TOKEN, "test", "test", of(mockHttpClient));

  @BeforeEach
  void beforeEach() throws Exception {
    when(mockHttpClient.execute(any(HttpMethod.class), anyString(), anyString()))
        .thenReturn(fakeResponse);
  }

  @Test
  void shouldSendSenderAction() throws Exception {
    // tag::send-SenderAction[]
    final String recipientId = "USER_ID";
    final SenderAction senderAction = SenderAction.MARK_SEEN;

    final SenderActionPayload payload = SenderActionPayload.create(recipientId, senderAction);

    messenger.send(payload);
    // end::send-SenderAction[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"}," + "\"sender_action\":\"mark_seen\"}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessage() throws Exception {
    // tag::send-TextMessage[]
    final String recipientId = "USER_ID";
    final String text = "Hello Messenger Platform";

    final MessagePayload payload =
        MessagePayload.create(recipientId, MessagingType.RESPONSE, TextMessage.create(text));

    messenger.send(payload);
    // end::send-TextMessage[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"text\":\"Hello Messenger Platform\"}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithPhoneNumberRecipientWithName() throws Exception {
    final Recipient recipient = PhoneNumberRecipient.create("+1 (555) 857-6309", "Jenny", "Doe");
    final String text = "Your package has arrived in Lobby 2.";

    final MessagePayload payload =
        MessagePayload.create(recipient, MessagingType.RESPONSE, TextMessage.create(text));

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"phone_number\":\"+1 (555) 857-6309\", "
            + "\"name\":{\"first_name\":\"Jenny\", \"last_name\":\"Doe\"}}, \n"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"text\":\"Your package has arrived in Lobby 2.\"}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithPhoneNumberRecipientWithoutName() throws Exception {
    final Recipient recipient = PhoneNumberRecipient.create("+1 (555) 857-6309");
    final String text = "Your package has arrived in Lobby 2.";

    final MessagePayload payload =
        MessagePayload.create(recipient, MessagingType.RESPONSE, TextMessage.create(text));

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"phone_number\":\"+1 (555) 857-6309\"}, \n"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"text\":\"Your package has arrived in Lobby 2.\"}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithUserRefRecipient() throws Exception {
    final Recipient recipient = UserRefRecipient.create("<UNIQUE_REF_PARAM>");
    final String text = "hello, world!";

    final MessagePayload payload =
        MessagePayload.create(recipient, MessagingType.RESPONSE, TextMessage.create(text));

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\": {\n"
            + "    \"user_ref\":\"<UNIQUE_REF_PARAM>\"\n"
            + "  }, \n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\": {\n"
            + "    \"text\":\"hello, world!\"\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithNotificationTypeAndMessageTag() throws Exception {
    // tag::send-TextMessageNotificationTypeMessageTag[]
    final Recipient recipient = IdRecipient.create("USER_ID");
    final TextMessage message = TextMessage.create("Hello Messenger Platform");
    final NotificationType notificationType = NotificationType.SILENT_PUSH;
    final MessageTag messageTag = MessageTag.APPLICATION_UPDATE;

    final MessagePayload payload =
        MessagePayload.create(
            recipient, MessagingType.RESPONSE, message, of(notificationType), of(messageTag));

    messenger.send(payload);
    // end::send-TextMessageNotificationTypeMessageTag[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"notification_type\":\"SILENT_PUSH\","
            + "\"tag\":\"APPLICATION_UPDATE\","
            + "\"message\":{\"text\":\"Hello Messenger Platform\"}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithQuickReplies() throws Exception {
    // tag::send-TextMessageQuickReplies[]
    final IdRecipient recipient = IdRecipient.create("<PSID>");

    final String text = "Here is a quick reply!";

    final TextQuickReply quickReplyA =
        TextQuickReply.create(
            "Search", "<POSTBACK_PAYLOAD>", of(new URL("http://example.com/img/red.png")));
    final LocationQuickReply quickReplyB = LocationQuickReply.create();
    final TextQuickReply quickReplyC =
        TextQuickReply.create("Something Else", "<POSTBACK_PAYLOAD>");

    final List<QuickReply> quickReplies = Arrays.asList(quickReplyA, quickReplyB, quickReplyC);

    final TextMessage message = TextMessage.create(text, of(quickReplies), empty());
    final MessagePayload payload =
        MessagePayload.create(recipient, MessagingType.RESPONSE, message);

    messenger.send(payload);
    // end::send-TextMessageQuickReplies[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"<PSID>\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"text\": \"Here is a quick reply!\",\n"
            + "    \"quick_replies\":[\n"
            + "      {\n"
            + "        \"content_type\":\"text\",\n"
            + "        \"title\":\"Search\",\n"
            + "        \"payload\":\"<POSTBACK_PAYLOAD>\",\n"
            + "        \"image_url\":\"http://example.com/img/red.png\"\n"
            + "      },\n"
            + "      {\n"
            + "        \"content_type\":\"location\"\n"
            + "      },\n"
            + "      {\n"
            + "        \"content_type\":\"text\",\n"
            + "        \"title\":\"Something Else\",\n"
            + "        \"payload\":\"<POSTBACK_PAYLOAD>\"\n"
            + "      }\n"
            + "    ]\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithEmailAndPhoneNumberQuickReplies() throws Exception {
    final IdRecipient recipient = IdRecipient.create("<PSID>");

    final String text = "Please send us your email or phone number.";

    final UserEmailQuickReply userEmailQuickReply = UserEmailQuickReply.create();
    final UserPhoneNumberQuickReply userPhoneNumberQuickReply = UserPhoneNumberQuickReply.create();

    final List<QuickReply> quickReplies =
        Arrays.asList(userEmailQuickReply, userPhoneNumberQuickReply);

    final TextMessage message = TextMessage.create(text, of(quickReplies), empty());
    final MessagePayload payload =
        MessagePayload.create(recipient, MessagingType.RESPONSE, message);

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"<PSID>\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"text\": \"Please send us your email or phone number.\",\n"
            + "    \"quick_replies\":[\n"
            + "      {\n"
            + "        \"content_type\":\"user_email\"\n"
            + "      },\n"
            + "      {\n"
            + "        \"content_type\":\"user_phone_number\"\n"
            + "      }\n"
            + "    ]\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendTextMessageWithMetadata() throws Exception {
    // tag::send-TextMessageMetadata[]
    final IdRecipient recipient = IdRecipient.create("USER_ID");
    final NotificationType notificationType = NotificationType.SILENT_PUSH;
    final String text = "Hello Messenger Platform";
    final String metadata = "DEVELOPER_DEFINED_METADATA";

    final TextMessage textMessage = TextMessage.create(text, empty(), of(metadata));
    final MessagePayload payload =
        MessagePayload.create(
            recipient, MessagingType.RESPONSE, textMessage, of(notificationType), empty());

    messenger.send(payload);
    // end::send-TextMessageMetadata[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"notification_type\":\"SILENT_PUSH\","
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{"
            + "\"text\":\"Hello Messenger Platform\","
            + "\"metadata\":\"DEVELOPER_DEFINED_METADATA\""
            + "}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendImageAttachmentMessageWithUrl() throws Exception {
    // tag::send-ImageMessageUrl[]
    final String recipientId = "USER_ID";
    final String imageUrl = "https://petersapparel.com/img/shirt.png";

    final UrlRichMediaAsset richMediaAsset = UrlRichMediaAsset.create(IMAGE, new URL(imageUrl));
    final RichMediaMessage richMediaMessage = RichMediaMessage.create(richMediaAsset);
    final MessagePayload payload =
        MessagePayload.create(recipientId, MessagingType.RESPONSE, richMediaMessage);

    messenger.send(payload);
    // end::send-ImageMessageUrl[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{"
            + "\"type\":\"image\","
            + "\"payload\":{\"url\":\"https://petersapparel.com/img/shirt.png\"}"
            + "}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendReusableImageAttachmentMessageWithUrl() throws Exception {
    // tag::send-ImageMessageUrlReusable[]
    final IdRecipient recipient = IdRecipient.create("USER_ID");
    final NotificationType notificationType = NotificationType.NO_PUSH;
    final String imageUrl = "https://petersapparel.com/img/shirt.png";

    final UrlRichMediaAsset richMediaAsset =
        UrlRichMediaAsset.create(IMAGE, new URL(imageUrl), of(true));
    final RichMediaMessage richMediaMessage = RichMediaMessage.create(richMediaAsset);
    final MessagePayload payload =
        MessagePayload.create(
            recipient, MessagingType.RESPONSE, richMediaMessage, of(notificationType), empty());

    messenger.send(payload);
    // end::send-ImageMessageUrlReusable[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"notification_type\":\"NO_PUSH\","
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{"
            + "\"type\":\"image\","
            + "\"payload\":{\"url\":\"https://petersapparel.com/img/shirt.png\",\"is_reusable\":true}"
            + "}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendImageAttachmentMessageWithAttachmentId() throws Exception {
    // tag::send-ImageMessageAttachmentId[]
    final IdRecipient recipient = IdRecipient.create("USER_ID");
    final NotificationType notificationType = NotificationType.NO_PUSH;
    final String attachmentId = "1745504518999123";

    final ReusableRichMediaAsset richMediaAsset =
        ReusableRichMediaAsset.create(IMAGE, attachmentId);
    final RichMediaMessage richMediaMessage = RichMediaMessage.create(richMediaAsset);
    final MessagePayload payload =
        MessagePayload.create(
            recipient, MessagingType.RESPONSE, richMediaMessage, of(notificationType), empty());

    messenger.send(payload);
    // end::send-ImageMessageAttachmentId[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"notification_type\":\"NO_PUSH\","
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{"
            + "\"type\":\"image\","
            + "\"payload\":{\"attachment_id\":\"1745504518999123\"}"
            + "}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendButtonTemplateMessage() throws Exception {
    // tag::send-ButtonTemplate[]
    final String recipientId = "USER_ID";

    final UrlButton buttonA =
        UrlButton.create("Show Website", new URL("https://petersapparel.parseapp.com"));
    final PostbackButton buttonB = PostbackButton.create("Start Chatting", "USER_DEFINED_PAYLOAD");
    final UrlButton buttonC =
        UrlButton.create(
            "Show Website",
            new URL("https://petersapparel.parseapp.com"),
            of(WebviewHeightRatio.FULL),
            of(true),
            of(new URL("https://petersfancyapparel.com/fallback")),
            empty());

    final List<Button> buttons = Arrays.asList(buttonA, buttonB, buttonC);
    final ButtonTemplate buttonTemplate =
        ButtonTemplate.create("What do you want to do next?", buttons);

    final TemplateMessage templateMessage = TemplateMessage.create(buttonTemplate);
    final MessagePayload payload =
        MessagePayload.create(recipientId, MessagingType.RESPONSE, templateMessage);

    messenger.send(payload);
    // end::send-ButtonTemplate[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{\"type\":\"template\",\"payload\":{\"text\":\"What do you want to do next?\""
            + ",\"buttons\":[{\"url\":\"https://petersapparel.parseapp.com\",\"title\":\"Show Website\""
            + ",\"type\":\"web_url\"},{\"payload\":\"USER_DEFINED_PAYLOAD\",\"title\":\"Start Chatting\""
            + ",\"type\":\"postback\"},{\"url\":\"https://petersapparel.parseapp.com\",\"webview_height_ratio\":\"full\""
            + ",\"messenger_extensions\":true,\"fallback_url\":\"https://petersfancyapparel.com/fallback\""
            + ",\"title\":\"Show Website\",\"type\":\"web_url\"}],\"template_type\":\"button\"}}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendGenericTemplateMessageWithButtons() throws Exception {
    // tag::send-GenericTemplateButtons[]
    final String recipientId = "USER_ID";

    final List<Button> buttons =
        Arrays.asList(
            UrlButton.create(
                "Select Criteria",
                new URL("https://petersfancyapparel.com/criteria_selector"),
                of(WebviewHeightRatio.FULL),
                of(true),
                of(new URL("https://petersfancyapparel.com/fallback")),
                empty()),
            CallButton.create("Call Representative", "+15105551234"),
            PostbackButton.create("Start Chatting", "DEVELOPER_DEFINED_PAYLOAD"));

    final DefaultAction defaultAction =
        DefaultAction.create(
            new URL("https://peterssendreceiveapp.ngrok.io/view?item=103"),
            of(WebviewHeightRatio.TALL),
            of(true),
            of(new URL("https://peterssendreceiveapp.ngrok.io/")),
            of(WebviewShareButtonState.HIDE));

    final Element element =
        Element.create(
            "Welcome to Peters Hats",
            of("We have got the right hat for everyone."),
            of(new URL("https://petersfancybrownhats.com/company_image.png")),
            of(defaultAction),
            of(buttons));

    final GenericTemplate genericTemplate = GenericTemplate.create(singletonList(element));

    final MessagePayload payload =
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(genericTemplate));

    messenger.send(payload);
    // end::send-GenericTemplateButtons[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"USER_ID\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"attachment\":{\n"
            + "      \"type\":\"template\",\n"
            + "      \"payload\":{\n"
            + "        \"template_type\":\"generic\",\n"
            + "        \"elements\":[\n"
            + "           {\n"
            + "            \"title\":\"Welcome to Peters Hats\",\n"
            + "            \"image_url\":\"https://petersfancybrownhats.com/company_image.png\",\n"
            + "            \"subtitle\":\"We have got the right hat for everyone.\",\n"
            + "            \"default_action\": {\n"
            + "              \"type\": \"web_url\",\n"
            + "              \"url\": \"https://peterssendreceiveapp.ngrok.io/view?item=103\",\n"
            + "              \"messenger_extensions\": true,\n"
            + "              \"webview_height_ratio\": \"tall\",\n"
            + "              \"webview_share_button\": \"hide\",\n"
            + "              \"fallback_url\": \"https://peterssendreceiveapp.ngrok.io/\"\n"
            + "            },\n"
            + "            \"buttons\":[\n"
            + "              {\n"
            + "                   \"type\":\"web_url\",\n"
            + "                   \"url\":\"https://petersfancyapparel.com/criteria_selector\",\n"
            + "                   \"title\":\"Select Criteria\",\n"
            + "                   \"webview_height_ratio\": \"full\",\n"
            + "                   \"messenger_extensions\": true,  \n"
            + "                   \"fallback_url\": \"https://petersfancyapparel.com/fallback\"\n"
            + "              },"
            + "               {\n"
            + "                   \"type\":\"phone_number\",\n"
            + "                   \"title\":\"Call Representative\",\n"
            + "                   \"payload\":\"+15105551234\"\n"
            + "               },"
            + "               {\n"
            + "                \"type\":\"postback\",\n"
            + "                \"title\":\"Start Chatting\",\n"
            + "                \"payload\":\"DEVELOPER_DEFINED_PAYLOAD\"\n"
            + "              }              \n"
            + "            ]      \n"
            + "          }\n"
            + "        ]\n"
            + "      }\n"
            + "    }\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendGenericTemplateMessageWithLogInAndLogOutButtons() throws Exception {
    // tag::send-GenericTemplateLoginLogoutButtons[]
    final String recipientId = "USER_ID";

    final List<Button> buttons =
        Arrays.asList(
            LogInButton.create(new URL("https://www.example.com/authorize")),
            LogOutButton.create());

    final Element element =
        Element.create(
            "Welcome to M-Bank",
            empty(),
            of(new URL("http://www.example.com/images/m-bank.png")),
            empty(),
            of(buttons));

    final GenericTemplate genericTemplate = GenericTemplate.create(singletonList(element));

    messenger.send(
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(genericTemplate)));
    // end::send-GenericTemplateLoginLogoutButtons[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":"
            + "{\"attachment\":{\"type\":\"template\",\"payload\":{\"elements\":"
            + "[{\"title\":\"Welcome to M-Bank\",\"image_url\":\"http://www.example.com/images/m-bank.png\","
            + "\"buttons\":[{\"url\":\"https://www.example.com/authorize\",\"type\":\"account_link\"},"
            + "{\"type\":\"account_unlink\"}]}],\"template_type\":\"generic\"}}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendGenericTemplateMessageWithShareButton() throws Exception {
    final String recipientId = "<PSID>";
    final Element element =
        Element.create(
            "Breaking News: Record Thunderstorms",
            of("The local area is due for record thunderstorms over the weekend."),
            of(new URL("https://thechangreport.com/img/lightning.png")),
            empty(),
            of(singletonList(ShareButton.create())));

    final GenericTemplate genericTemplate = GenericTemplate.create(singletonList(element));

    final MessagePayload payload =
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(genericTemplate));

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"<PSID>\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"attachment\":{\n"
            + "      \"type\":\"template\",\n"
            + "      \"payload\":{\n"
            + "        \"template_type\":\"generic\",\n"
            + "        \"elements\":[\n"
            + "          {\n"
            + "            \"title\":\"Breaking News: Record Thunderstorms\",\n"
            + "            \"subtitle\":\"The local area is due for record thunderstorms over the weekend.\",\n"
            + "            \"image_url\":\"https://thechangreport.com/img/lightning.png\",\n"
            + "            \"buttons\": [\n"
            + "              {\n"
            + "                \"type\": \"element_share\"\n"
            + "              }\n"
            + "            ]\n"
            + "          }\n"
            + "        ]\n"
            + "      }\n"
            + "    }\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendGenericTemplateMessageWithShareButtonWithShareContents() throws Exception {
    final String recipientId = "<PSID>";

    final GenericTemplate shareGenericTemplate =
        GenericTemplate.create(
            singletonList(
                Element.create(
                    "I took the hat quiz",
                    of("My result: Fez"),
                    of(new URL("https://bot.peters-hats.com/img/hats/fez.jpg")),
                    of(
                        DefaultAction.create(
                            new URL("http://m.me/petershats?ref=invited_by_24601"))),
                    of(
                        singletonList(
                            UrlButton.create(
                                "Take Quiz",
                                new URL("http://m.me/petershats?ref=invited_by_24601")))))));

    final Element element =
        Element.create(
            "Breaking News: Record Thunderstorms",
            of("The local area is due for record thunderstorms over the weekend."),
            of(new URL("https://thechangreport.com/img/lightning.png")),
            empty(),
            of(singletonList(ShareButton.create(of(shareGenericTemplate)))));

    final GenericTemplate genericTemplate = GenericTemplate.create(singletonList(element));

    final MessagePayload payload =
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(genericTemplate));

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"<PSID>\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"attachment\":{\n"
            + "      \"type\":\"template\",\n"
            + "      \"payload\":{\n"
            + "        \"template_type\":\"generic\",\n"
            + "        \"elements\":[\n"
            + "          {\n"
            + "            \"title\":\"Breaking News: Record Thunderstorms\",\n"
            + "            \"subtitle\":\"The local area is due for record thunderstorms over the weekend.\",\n"
            + "            \"image_url\":\"https://thechangreport.com/img/lightning.png\",\n"
            + "            \"buttons\": [\n"
            + "              {\n"
            + "                \"type\": \"element_share\",\n"
            + "                \"share_contents\": { \n"
            + "                  \"attachment\": {\n"
            + "                    \"type\": \"template\",\n"
            + "                    \"payload\": {\n"
            + "                      \"template_type\": \"generic\",\n"
            + "                      \"elements\": [\n"
            + "                        {\n"
            + "                          \"title\": \"I took the hat quiz\",\n"
            + "                          \"subtitle\": \"My result: Fez\",\n"
            + "                          \"image_url\": \"https://bot.peters-hats.com/img/hats/fez.jpg\",\n"
            + "                          \"default_action\": {\n"
            + "                            \"type\": \"web_url\",\n"
            + "                            \"url\": \"http://m.me/petershats?ref=invited_by_24601\"\n"
            + "                          },\n"
            + "                          \"buttons\": [\n"
            + "                            {\n"
            + "                              \"type\": \"web_url\",\n"
            + "                              \"url\": \"http://m.me/petershats?ref=invited_by_24601\", \n"
            + "                              \"title\": \"Take Quiz\"\n"
            + "                            }\n"
            + "                          ]\n"
            + "                        }\n"
            + "                      ]\n"
            + "                    }\n"
            + "                  }\n"
            + "                }\n"
            + "              }\n"
            + "            ]\n"
            + "          }\n"
            + "        ]\n"
            + "      }\n"
            + "    }\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendReceiptTemplateMessage() throws Exception {
    // tag::send-ReceiptTemplate[]
    final String recipientId = "USER_ID";

    final Adjustment adjustment1 = Adjustment.create("New Customer Discount", 20.00F);
    final Adjustment adjustment2 = Adjustment.create("$10 Off Coupon", 10.00F);

    final Item item1 =
        Item.create(
            "Classic White T-Shirt",
            50F,
            of("100% Soft and Luxurious Cotton"),
            of(2),
            of("USD"),
            of(new URL("http://petersapparel.parseapp.com/img/whiteshirt.png")));

    final Item item2 =
        Item.create(
            "Classic Gray T-Shirt",
            25F,
            of("100% Soft and Luxurious Cotton"),
            of(1),
            of("USD"),
            of(new URL("http://petersapparel.parseapp.com/img/grayshirt.png")));

    final Address address =
        Address.create("1 Hacker Way", of(""), "Menlo Park", "94025", "CA", "US");
    final Summary summary = Summary.create(56.14F, of(75.00F), of(6.19F), of(4.95F));

    final ReceiptTemplate receiptTemplate =
        ReceiptTemplate.create(
            "Stephane Crozatier",
            "12345678902",
            "Visa 2345",
            "USD",
            summary,
            of(address),
            of(Arrays.asList(item1, item2)),
            of(Arrays.asList(adjustment1, adjustment2)),
            empty(),
            of(new URL("http://petersapparel.parseapp.com/order?order_id=123456")),
            empty(),
            of(ZonedDateTime.of(2015, 4, 7, 22, 14, 12, 0, ZoneOffset.UTC).toInstant()));

    final MessagePayload payload =
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(receiptTemplate));

    messenger.send(payload);
    // end::send-ReceiptTemplate[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{\"type\":\"template\",\"payload\":{\"recipient_name\":\"Stephane Crozatier\","
            + "\"order_number\":\"12345678902\",\"currency\":\"USD\",\"payment_method\":\"Visa 2345\","
            + "\"timestamp\":\"1428444852\",\"order_url\":\"http://petersapparel.parseapp.com/order?order_id\\u003d123456\","
            + "\"elements\":[{\"title\":\"Classic White T-Shirt\",\"subtitle\":\"100% Soft and Luxurious Cotton\","
            + "\"quantity\":2,\"price\":50.00,\"currency\":\"USD\","
            + "\"image_url\":\"http://petersapparel.parseapp.com/img/whiteshirt.png\"},{\"title\":\"Classic Gray T-Shirt\","
            + "\"subtitle\":\"100% Soft and Luxurious Cotton\",\"quantity\":1,\"price\":25.00,\"currency\":\"USD\","
            + "\"image_url\":\"http://petersapparel.parseapp.com/img/grayshirt.png\"}],"
            + "\"address\":{\"street_1\":\"1 Hacker Way\",\"street_2\":\"\",\"city\":\"Menlo Park\",\"postal_code\":\"94025\","
            + "\"state\":\"CA\",\"country\":\"US\"},\"summary\":{\"total_cost\":56.14,\"total_tax\":6.19,"
            + "\"shipping_cost\":4.95,\"subtotal\":75.00},\"adjustments\":[{\"name\":\"New Customer Discount\","
            + "\"amount\":20.00},{\"name\":\"$10 Off Coupon\",\"amount\":10.00}],\"template_type\":\"receipt\"}}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendListTemplateMessage() throws Exception {
    // tag::send-ListTemplate[]
    final String recipientId = "USER_ID";

    final Element element1 =
        Element.create(
            "Classic T-Shirt Collection",
            of("See all our colors"),
            of(new URL("https://peterssendreceiveapp.ngrok.io/img/collection.png")),
            of(
                DefaultAction.create(
                    new URL("https://peterssendreceiveapp.ngrok.io/shop_collection"),
                    of(WebviewHeightRatio.TALL),
                    of(true),
                    of(new URL("https://peterssendreceiveapp.ngrok.io/fallback")),
                    empty())),
            of(
                singletonList(
                    UrlButton.create(
                        "View",
                        new URL("https://peterssendreceiveapp.ngrok.io/collection"),
                        of(WebviewHeightRatio.TALL),
                        empty(),
                        empty(),
                        empty()))));

    final Element element2 =
        Element.create(
            "Classic White T-Shirt",
            of("100% Cotton, 200% Comfortable"),
            of(new URL("https://peterssendreceiveapp.ngrok.io/img/white-t-shirt.png")),
            of(
                DefaultAction.create(
                    new URL("https://peterssendreceiveapp.ngrok.io/view?item=100"),
                    of(WebviewHeightRatio.TALL),
                    empty(),
                    empty(),
                    empty())),
            of(
                singletonList(
                    UrlButton.create(
                        "Shop Now",
                        new URL("https://peterssendreceiveapp.ngrok.io/shop?item=100"),
                        of(WebviewHeightRatio.TALL),
                        empty(),
                        empty(),
                        empty()))));

    final Element element3 =
        Element.create(
            "Classic Blue T-Shirt",
            of("100% Cotton, 200% Comfortable"),
            of(new URL("https://peterssendreceiveapp.ngrok.io/img/blue-t-shirt.png")),
            of(
                DefaultAction.create(
                    new URL("https://peterssendreceiveapp.ngrok.io/view?item=101"),
                    of(WebviewHeightRatio.TALL),
                    empty(),
                    empty(),
                    empty())),
            of(
                singletonList(
                    UrlButton.create(
                        "Shop Now",
                        new URL("https://peterssendreceiveapp.ngrok.io/shop?item=101"),
                        of(WebviewHeightRatio.TALL),
                        empty(),
                        empty(),
                        empty()))));

    final Element element4 =
        Element.create(
            "Classic Black T-Shirt",
            of("100% Cotton, 200% Comfortable"),
            of(new URL("https://peterssendreceiveapp.ngrok.io/img/black-t-shirt.png")),
            of(
                DefaultAction.create(
                    new URL("https://peterssendreceiveapp.ngrok.io/view?item=102"),
                    of(WebviewHeightRatio.TALL),
                    empty(),
                    empty(),
                    empty())),
            of(
                singletonList(
                    UrlButton.create(
                        "Shop Now",
                        new URL("https://peterssendreceiveapp.ngrok.io/shop?item=102"),
                        of(WebviewHeightRatio.TALL),
                        empty(),
                        empty(),
                        empty()))));

    final ListTemplate listTemplate =
        ListTemplate.create(
            Arrays.asList(element1, element2, element3, element4),
            of(TopElementStyle.LARGE),
            of(singletonList(PostbackButton.create("View More", "payload"))));

    messenger.send(
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(listTemplate)));
    // end::send-ListTemplate[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\"recipient\":{\"id\":\"USER_ID\"},"
            + "\"messaging_type\":\"RESPONSE\","
            + "\"message\":{\"attachment\":{\"type\":\"template\","
            + "\"payload\":{\"top_element_style\":\"large\",\"buttons\":[{\"payload\":\"payload\",\"title\":\"View More\","
            + "\"type\":\"postback\"}],\"elements\":[{\"title\":\"Classic T-Shirt Collection\","
            + "\"subtitle\":\"See all our colors\",\"image_url\":\"https://peterssendreceiveapp.ngrok.io/img/collection.png\","
            + "\"buttons\":[{\"url\":\"https://peterssendreceiveapp.ngrok.io/collection\",\"webview_height_ratio\":\"tall\","
            + "\"title\":\"View\",\"type\":\"web_url\"}],\"default_action\":{\"type\":\"web_url\","
            + "\"url\":\"https://peterssendreceiveapp.ngrok.io/shop_collection\",\"webview_height_ratio\":\"tall\","
            + "\"messenger_extensions\":true,\"fallback_url\":\"https://peterssendreceiveapp.ngrok.io/fallback\"}},"
            + "{\"title\":\"Classic White T-Shirt\",\"subtitle\":\"100% Cotton, 200% Comfortable\","
            + "\"image_url\":\"https://peterssendreceiveapp.ngrok.io/img/white-t-shirt.png\","
            + "\"buttons\":[{\"url\":\"https://peterssendreceiveapp.ngrok.io/shop?item\\u003d100\",\"webview_height_ratio\":\"tall\","
            + "\"title\":\"Shop Now\",\"type\":\"web_url\"}],\"default_action\":{\"type\":\"web_url\","
            + "\"url\":\"https://peterssendreceiveapp.ngrok.io/view?item\\u003d100\",\"webview_height_ratio\":\"tall\"}},"
            + "{\"title\":\"Classic Blue T-Shirt\",\"subtitle\":\"100% Cotton, 200% Comfortable\","
            + "\"image_url\":\"https://peterssendreceiveapp.ngrok.io/img/blue-t-shirt.png\","
            + "\"buttons\":[{\"url\":\"https://peterssendreceiveapp.ngrok.io/shop?item\\u003d101\",\"webview_height_ratio\":\"tall\","
            + "\"title\":\"Shop Now\",\"type\":\"web_url\"}],\"default_action\":{\"type\":\"web_url\","
            + "\"url\":\"https://peterssendreceiveapp.ngrok.io/view?item\\u003d101\",\"webview_height_ratio\":\"tall\"}},"
            + "{\"title\":\"Classic Black T-Shirt\",\"subtitle\":\"100% Cotton, 200% Comfortable\","
            + "\"image_url\":\"https://peterssendreceiveapp.ngrok.io/img/black-t-shirt.png\","
            + "\"buttons\":[{\"url\":\"https://peterssendreceiveapp.ngrok.io/shop?item\\u003d102\",\"webview_height_ratio\":\"tall\","
            + "\"title\":\"Shop Now\",\"type\":\"web_url\"}],\"default_action\":{\"type\":\"web_url\","
            + "\"url\":\"https://peterssendreceiveapp.ngrok.io/view?item\\u003d102\",\"webview_height_ratio\":\"tall\"}}],"
            + "\"template_type\":\"list\"}}}}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendOpenGraphTemplateMessage() throws Exception {
    // tag::send-OpenGraphTemplate[]
    final String recipientId = "USER_ID";

    final UrlButton urlButton =
        UrlButton.create("View More", new URL("https://en.wikipedia.org/wiki/Rickrolling"));
    final OpenGraphObject openGraphObject =
        OpenGraphObject.create(
            new URL("https://open.spotify.com/track/7GhIk7Il098yCjg4BQjzvb"),
            of(singletonList(urlButton)));
    final OpenGraphTemplate openGraphTemplate =
        OpenGraphTemplate.create(singletonList(openGraphObject));

    messenger.send(
        MessagePayload.create(
            recipientId, MessagingType.RESPONSE, TemplateMessage.create(openGraphTemplate)));
    // end::send-OpenGraphTemplate[]

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\":{\n"
            + "    \"id\":\"USER_ID\"\n"
            + "  },\n"
            + "  \"messaging_type\":\"RESPONSE\","
            + "  \"message\":{\n"
            + "    \"attachment\":{\n"
            + "      \"type\":\"template\",\n"
            + "      \"payload\":{\n"
            + "        \"template_type\":\"open_graph\",\n"
            + "        \"elements\":[\n"
            + "           {\n"
            + "            \"url\":\"https://open.spotify.com/track/7GhIk7Il098yCjg4BQjzvb\",\n"
            + "            \"buttons\":[\n"
            + "              {\n"
            + "                \"type\":\"web_url\",\n"
            + "                \"url\":\"https://en.wikipedia.org/wiki/Rickrolling\",\n"
            + "                \"title\":\"View More\"\n"
            + "              }              \n"
            + "            ]      \n"
            + "          }\n"
            + "        ]\n"
            + "      }\n"
            + "    }\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldSendButtonTemplateMessageWithUrlButtonWithWebviewShareButtonState() throws Exception {
    final String recipientId = "USER_ID";

    final UrlButton buttonA =
        UrlButton.create("Show Website", new URL("https://petersapparel.parseapp.com"));
    final UrlButton buttonB =
        UrlButton.create(
            "Show Website",
            new URL("https://petersapparel.parseapp.com"),
            of(WebviewHeightRatio.FULL),
            of(true),
            of(new URL("https://petersfancyapparel.com/fallback")),
            of(WebviewShareButtonState.SHOW));
    final UrlButton buttonC =
        UrlButton.create(
            "Show Website",
            new URL("https://petersapparel.parseapp.com"),
            of(WebviewHeightRatio.FULL),
            of(true),
            of(new URL("https://petersfancyapparel.com/fallback")),
            of(WebviewShareButtonState.HIDE));

    final List<Button> buttons = Arrays.asList(buttonA, buttonB, buttonC);
    final ButtonTemplate buttonTemplate =
        ButtonTemplate.create("What do you want to do next?", buttons);

    final TemplateMessage templateMessage = TemplateMessage.create(buttonTemplate);
    final MessagePayload payload =
        MessagePayload.create(recipientId, MessagingType.RESPONSE, templateMessage);

    messenger.send(payload);

    final ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
    final String expectedJsonBody =
        "{\n"
            + "  \"recipient\": {\n"
            + "    \"id\": \"USER_ID\"\n"
            + "  },\n"
            + "  \"messaging_type\": \"RESPONSE\",\n"
            + "  \"message\": {\n"
            + "    \"attachment\": {\n"
            + "      \"type\": \"template\",\n"
            + "      \"payload\": {\n"
            + "        \"text\": \"What do you want to do next?\",\n"
            + "        \"buttons\": [{\n"
            + "          \"url\": \"https://petersapparel.parseapp.com\",\n"
            + "          \"title\": \"Show Website\",\n"
            + "          \"type\": \"web_url\"\n"
            + "        }, {\n"
            + "          \"url\": \"https://petersapparel.parseapp.com\",\n"
            + "          \"webview_height_ratio\": \"full\",\n"
            + "          \"messenger_extensions\": true,\n"
            + "          \"fallback_url\": \"https://petersfancyapparel.com/fallback\",\n"
            + "          \"title\": \"Show Website\",\n"
            + "          \"type\": \"web_url\"\n"
            + "        }, {\n"
            + "          \"url\": \"https://petersapparel.parseapp.com\",\n"
            + "          \"webview_share_button\": \"hide\",\n"
            + "          \"webview_height_ratio\": \"full\",\n"
            + "          \"messenger_extensions\": true,\n"
            + "          \"fallback_url\": \"https://petersfancyapparel.com/fallback\",\n"
            + "          \"title\": \"Show Website\",\n"
            + "          \"type\": \"web_url\"\n"
            + "        }],\n"
            + "        \"template_type\": \"button\"\n"
            + "      }\n"
            + "    }\n"
            + "  }\n"
            + "}";
    verify(mockHttpClient).execute(eq(POST), endsWith(PAGE_ACCESS_TOKEN), payloadCaptor.capture());
    JSONAssert.assertEquals(expectedJsonBody, payloadCaptor.getValue(), true);
  }

  @Test
  void shouldHandleSuccessResponse() throws Exception {
    final HttpResponse successfulResponse =
        new HttpResponse(
            200,
            "{\n"
                + "  \"recipient_id\": \"USER_ID\",\n"
                + "  \"message_id\": \"mid.1473372944816:94f72b88c597657974\",\n"
                + "  \"attachment_id\": \"1745504518999123\"\n"
                + "}");
    when(mockHttpClient.execute(any(HttpMethod.class), anyString(), anyString()))
        .thenReturn(successfulResponse);

    // tag::send-SuccessResponse[]
    final UrlRichMediaAsset richMediaAsset =
        UrlRichMediaAsset.create(IMAGE, new URL("http://image.url"), of(true));
    final MessagePayload payload =
        MessagePayload.create(
            "USER_ID", MessagingType.RESPONSE, RichMediaMessage.create(richMediaAsset));

    final MessageResponse messageResponse = messenger.send(payload);

    final Optional<String> recipientId = messageResponse.recipientId();
    final Optional<String> messageId = messageResponse.messageId();
    final Optional<String> attachmentId = messageResponse.attachmentId();
    log.debug(
        "RecipientId: {} | MessageId: {} | AttachmentId: {}", recipientId, messageId, attachmentId);
    // end::send-SuccessResponse[]

    assertThat(recipientId, is(equalTo(of("USER_ID"))));
    assertThat(messageId, is(equalTo(of("mid.1473372944816:94f72b88c597657974"))));
    assertThat(attachmentId, is(equalTo(of("1745504518999123"))));
  }

  @Test
  void shouldHandleErrorResponse() throws Exception {
    final HttpResponse errorResponse =
        new HttpResponse(
            401,
            "{\n"
                + "  \"error\": {\n"
                + "    \"message\": \"Invalid OAuth access token.\",\n"
                + "    \"type\": \"OAuthException\",\n"
                + "    \"code\": 190,\n"
                + "    \"fbtrace_id\": \"BLBz/WZt8dN\"\n"
                + "  }\n"
                + "}");
    when(mockHttpClient.execute(any(HttpMethod.class), anyString(), anyString()))
        .thenReturn(errorResponse);

    MessengerApiException messengerApiException = null;
    try {
      final MessagePayload payload =
          MessagePayload.create("test", MessagingType.RESPONSE, TextMessage.create("test"));
      messenger.send(payload);
    } catch (MessengerApiException e) {
      messengerApiException = e;
    }

    assertThat(messengerApiException, is(notNullValue()));
    assertThat(messengerApiException.message(), is(equalTo("Invalid OAuth access token.")));
    assertThat(messengerApiException.type(), is(equalTo(of("OAuthException"))));
    assertThat(messengerApiException.code(), is(equalTo(of(190))));
    assertThat(messengerApiException.fbTraceId(), is(equalTo(of("BLBz/WZt8dN"))));
  }
}


text:
<?xml version="1.0" encoding="UTF-8"?>

<!-- Generated by OpenXava: Wed Sep 11 11:56:50 CEST 2013 -->

<!DOCTYPE hibernate-mapping SYSTEM "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="org.openxava.test.model">

  <class
  	name="DeliveryType" schema="XAVATEST"
  	table="DeliveryType">
		<id name="number" column="number" access="field" type='int' length='4'>
			<generator class="assigned"/>
		</id>
		<property name="description" access="field" type='java.lang.String' length='50'>
			<column name="DESCRIPTION"  />
		</property>
		<set name="deliveries" order-by="INVOICE_NUMBER, INVOICE_YEAR, NUMBER, TYPE"  inverse='true'>
			<key column="TYPE"/>
			<one-to-many class="Delivery"/>
		</set>
  </class>

</hibernate-mapping>

redpajama_set_name: RedPajamaGithub


text:
package org.elasticsearch.xpack.core.security.authz.store;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.remote.RemoteInfoAction;
import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction;
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction;
import org.elasticsearch.action.admin.indices.rollover.RolloverAction;
import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction;
import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction;
import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.user.KibanaSystemUser;
import org.elasticsearch.xpack.core.security.user.UsernamesField;
import org.elasticsearch.xpack.core.transform.transforms.persistence.TransformInternalIndexConstants;
import org.elasticsearch.xpack.core.watcher.execution.TriggeredWatchStoreField;
import org.elasticsearch.xpack.core.watcher.history.HistoryStoreField;
import org.elasticsearch.xpack.core.watcher.watch.Watch;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

public class ReservedRolesStore implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
    public static final String ALERTS_LEGACY_INDEX = ".siem-signals*";
    public static final String ALERTS_BACKING_INDEX = ".internal.alerts*";
    public static final String ALERTS_INDEX_ALIAS = ".alerts*";
    public static final String PREVIEW_ALERTS_INDEX_ALIAS = ".preview.alerts*";
    public static final String PREVIEW_ALERTS_BACKING_INDEX_ALIAS = ".internal.preview.alerts*";

    public static final RoleDescriptor SUPERUSER_ROLE_DESCRIPTOR = new RoleDescriptor(
        "superuser",
        new String[] { "all" },
        new RoleDescriptor.IndicesPrivileges[] {
            RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(true).build() },
        new RoleDescriptor.ApplicationResourcePrivileges[] {
            RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() },
        null,
        new String[] { "*" },
        MetadataUtils.DEFAULT_RESERVED_METADATA,
        Collections.emptyMap()
    );
    private static final Map<String, RoleDescriptor> RESERVED_ROLES = initializeReservedRoles();

    private static Map<String, RoleDescriptor> initializeReservedRoles() {
        return MapBuilder.<String, RoleDescriptor>newMapBuilder()
            .put("superuser", SUPERUSER_ROLE_DESCRIPTOR)
            .put(
                "transport_client",
                new RoleDescriptor(
                    "transport_client",
                    new String[] { "transport_client" },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put("kibana_admin", kibanaAdminUser("kibana_admin", MetadataUtils.DEFAULT_RESERVED_METADATA))
            .put(
                "kibana_user",
                kibanaAdminUser("kibana_user", MetadataUtils.getDeprecatedReservedMetadata("Please use the [kibana_admin] role instead"))
            )
            .put(
                "monitoring_user",
                new RoleDescriptor(
                    "monitoring_user",
                    new String[] { "cluster:monitor/main", "cluster:monitor/xpack/info", RemoteInfoAction.NAME },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".monitoring-*")
                            .privileges("read", "read_cross_cluster")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("metricbeat-*")
                            .privileges("read", "read_cross_cluster")
                            .build() },
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_monitoring")
                            .build() },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "remote_monitoring_agent",
                new RoleDescriptor(
                    "remote_monitoring_agent",
                    new String[] {
                        "manage_index_templates",
                        "manage_ingest_pipelines",
                        "monitor",
                        GetLifecycleAction.NAME,
                        PutLifecycleAction.NAME,
                        "cluster:monitor/xpack/watcher/watch/get",
                        "cluster:admin/xpack/watcher/watch/put",
                        "cluster:admin/xpack/watcher/watch/delete" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder().indices(".monitoring-*").privileges("all").build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("metricbeat-*")
                            .privileges("index", "create_index", "view_index_metadata", IndicesAliasesAction.NAME)
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "remote_monitoring_collector",
                new RoleDescriptor(
                    "remote_monitoring_collector",
                    new String[] { "monitor" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("monitor").allowRestrictedIndices(true).build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".kibana*")
                            .privileges("read")
                            .allowRestrictedIndices(true)
                            .build() },
                    null,
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "ingest_admin",
                new RoleDescriptor(
                    "ingest_admin",
                    new String[] { "manage_index_templates", "manage_pipeline" },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            // reporting_user doesn't have any privileges in Elasticsearch, and Kibana authorizes privileges based on this role
            .put(
                "reporting_user",
                new RoleDescriptor(
                    "reporting_user",
                    null,
                    null,
                    null,
                    null,
                    null,
                    MetadataUtils.getDeprecatedReservedMetadata("Please use Kibana feature privileges instead"),
                    null
                )
            )
            .put(KibanaSystemUser.ROLE_NAME, kibanaSystemRoleDescriptor(KibanaSystemUser.ROLE_NAME))
            .put(
                "logstash_system",
                new RoleDescriptor(
                    "logstash_system",
                    new String[] { "monitor", MonitoringBulkAction.NAME },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "beats_admin",
                new RoleDescriptor(
                    "beats_admin",
                    null,
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder().indices(".management-beats").privileges("all").build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                UsernamesField.BEATS_ROLE,
                new RoleDescriptor(
                    UsernamesField.BEATS_ROLE,
                    new String[] { "monitor", MonitoringBulkAction.NAME },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".monitoring-beats-*")
                            .privileges("create_index", "create")
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                UsernamesField.APM_ROLE,
                new RoleDescriptor(
                    UsernamesField.APM_ROLE,
                    new String[] { "monitor", MonitoringBulkAction.NAME },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".monitoring-beats-*")
                            .privileges("create_index", "create_doc")
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "apm_user",
                new RoleDescriptor(
                    "apm_user",
                    null,
                    new RoleDescriptor.IndicesPrivileges[] {
                        // Self managed APM Server
                        // Can be removed in 8.0
                        RoleDescriptor.IndicesPrivileges.builder().indices("apm-*").privileges("read", "view_index_metadata").build(),

                        // APM Server under fleet (data streams)
                        RoleDescriptor.IndicesPrivileges.builder().indices("logs-apm.*").privileges("read", "view_index_metadata").build(),
                        RoleDescriptor.IndicesPrivileges.builder().indices("logs-apm-*").privileges("read", "view_index_metadata").build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("metrics-apm.*")
                            .privileges("read", "view_index_metadata")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("metrics-apm-*")
                            .privileges("read", "view_index_metadata")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("traces-apm.*")
                            .privileges("read", "view_index_metadata")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("traces-apm-*")
                            .privileges("read", "view_index_metadata")
                            .build(),

                        // Machine Learning indices. Only needed for legacy reasons
                        // Can be removed in 8.0
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".ml-anomalies*")
                            .privileges("read", "view_index_metadata")
                            .build(),

                        // Annotations
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("observability-annotations")
                            .privileges("read", "view_index_metadata")
                            .build() },
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_ml_apm_user")
                            .build() },
                    null,
                    null,
                    MetadataUtils.getDeprecatedReservedMetadata("This role will be removed in 8.0"),
                    null
                )
            )
            .put(
                "machine_learning_user",
                new RoleDescriptor(
                    "machine_learning_user",
                    new String[] { "monitor_ml" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".ml-anomalies*", ".ml-notifications*")
                            .privileges("view_index_metadata", "read")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".ml-annotations*")
                            .privileges("view_index_metadata", "read", "write")
                            .build() },
                    // This role also grants Kibana privileges related to ML.
                    // This makes it completely clear to UI administrators that
                    // if they grant the Elasticsearch backend role to a user then
                    // they cannot expect Kibana privileges to stop that user from
                    // accessing ML functionality - the user could switch to curl
                    // or even Kibana dev console and call the ES endpoints directly
                    // bypassing the Kibana privileges layer entirely.
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_ml_user")
                            .build() },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "machine_learning_admin",
                new RoleDescriptor(
                    "machine_learning_admin",
                    new String[] { "manage_ml" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".ml-anomalies*", ".ml-notifications*", ".ml-state*", ".ml-meta*", ".ml-stats-*")
                            .allowRestrictedIndices(true) // .ml-meta is a restricted index
                            .privileges("view_index_metadata", "read")
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".ml-annotations*")
                            .privileges("view_index_metadata", "read", "write")
                            .build() },
                    // This role also grants Kibana privileges related to ML.
                    // This makes it completely clear to UI administrators that
                    // if they grant the Elasticsearch backend role to a user then
                    // they cannot expect Kibana privileges to stop that user from
                    // accessing ML functionality - the user could switch to curl
                    // or even Kibana dev console and call the ES endpoints directly
                    // bypassing the Kibana privileges layer entirely.
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_ml_admin")
                            .build() },
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            // DEPRECATED: to be removed in 9.0.0
            .put(
                "data_frame_transforms_admin",
                new RoleDescriptor(
                    "data_frame_transforms_admin",
                    new String[] { "manage_data_frame_transforms" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN,
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN_DEPRECATED,
                                TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS
                            )
                            .privileges("view_index_metadata", "read")
                            .build() },
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_ml_user")
                            .build() },
                    null,
                    null,
                    MetadataUtils.getDeprecatedReservedMetadata("Please use the [transform_admin] role instead"),
                    null
                )
            )
            // DEPRECATED: to be removed in 9.0.0
            .put(
                "data_frame_transforms_user",
                new RoleDescriptor(
                    "data_frame_transforms_user",
                    new String[] { "monitor_data_frame_transforms" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN,
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN_DEPRECATED,
                                TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS
                            )
                            .privileges("view_index_metadata", "read")
                            .build() },
                    new RoleDescriptor.ApplicationResourcePrivileges[] {
                        RoleDescriptor.ApplicationResourcePrivileges.builder()
                            .application("kibana-*")
                            .resources("*")
                            .privileges("reserved_ml_user")
                            .build() },
                    null,
                    null,
                    MetadataUtils.getDeprecatedReservedMetadata("Please use the [transform_user] role instead"),
                    null
                )
            )
            .put(
                "transform_admin",
                new RoleDescriptor(
                    "transform_admin",
                    new String[] { "manage_transform" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN,
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN_DEPRECATED,
                                TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS
                            )
                            .privileges("view_index_metadata", "read")
                            .build() },
                    null,
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "transform_user",
                new RoleDescriptor(
                    "transform_user",
                    new String[] { "monitor_transform" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN,
                                TransformInternalIndexConstants.AUDIT_INDEX_PATTERN_DEPRECATED,
                                TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS
                            )
                            .privileges("view_index_metadata", "read")
                            .build() },
                    null,
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "watcher_admin",
                new RoleDescriptor(
                    "watcher_admin",
                    new String[] { "manage_watcher" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(Watch.INDEX, TriggeredWatchStoreField.INDEX_NAME, HistoryStoreField.INDEX_PREFIX + "*")
                            .privileges("read")
                            .allowRestrictedIndices(true)
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "watcher_user",
                new RoleDescriptor(
                    "watcher_user",
                    new String[] { "monitor_watcher" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(Watch.INDEX)
                            .privileges("read")
                            .allowRestrictedIndices(true)
                            .build(),
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(HistoryStoreField.INDEX_PREFIX + "*")
                            .privileges("read")
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "logstash_admin",
                new RoleDescriptor(
                    "logstash_admin",
                    new String[] { "manage_logstash_pipelines" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices(".logstash*")
                            .privileges("create", "delete", "index", "manage", "read")
                            .allowRestrictedIndices(true)
                            .build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put(
                "rollup_user",
                new RoleDescriptor("rollup_user", new String[] { "monitor_rollup" }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)
            )
            .put(
                "rollup_admin",
                new RoleDescriptor("rollup_admin", new String[] { "manage_rollup" }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)
            )
            .put(
                "snapshot_user",
                new RoleDescriptor(
                    "snapshot_user",
                    new String[] { "create_snapshot", GetRepositoriesAction.NAME },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder()
                            .indices("*")
                            .privileges("view_index_metadata")
                            .allowRestrictedIndices(true)
                            .build() },
                    null,
                    null,
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA,
                    null
                )
            )
            .put(
                "enrich_user",
                new RoleDescriptor(
                    "enrich_user",
                    new String[] { "manage_enrich", "manage_ingest_pipelines", "monitor" },
                    new RoleDescriptor.IndicesPrivileges[] {
                        RoleDescriptor.IndicesPrivileges.builder().indices(".enrich-*").privileges("manage", "read", "write").build() },
                    null,
                    MetadataUtils.DEFAULT_RESERVED_METADATA
                )
            )
            .put("viewer", buildViewerRoleDescriptor())
            .put("editor", buildEditorRoleDescriptor())
            .immutableMap();
    }

    private static RoleDescriptor buildViewerRoleDescriptor() {
        return new RoleDescriptor(
            "viewer",
            new String[] {},
            new RoleDescriptor.IndicesPrivileges[] {
                // Stack
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices("/~(([.]|ilm-history-).*)/")
                    .privileges("read", "view_index_metadata")
                    .build(),
                // Security
                RoleDescriptor.IndicesPrivileges.builder().indices(".siem-signals-*").privileges("read", "view_index_metadata").build(),
                // Alerts-as-data
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(ReservedRolesStore.ALERTS_INDEX_ALIAS, ReservedRolesStore.PREVIEW_ALERTS_INDEX_ALIAS)
                    .privileges("read", "view_index_metadata")
                    .build() },
            new RoleDescriptor.ApplicationResourcePrivileges[] {
                RoleDescriptor.ApplicationResourcePrivileges.builder()
                    .application("kibana-.kibana")
                    .resources("*")
                    .privileges("read")
                    .build() },
            null,
            null,
            MetadataUtils.DEFAULT_RESERVED_METADATA,
            null
        );
    }

    private static RoleDescriptor buildEditorRoleDescriptor() {
        return new RoleDescriptor(
            "editor",
            new String[] {},
            new RoleDescriptor.IndicesPrivileges[] {
                // Stack
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices("/~(([.]|ilm-history-).*)/")
                    .privileges("read", "view_index_metadata")
                    .build(),
                // Observability
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices("observability-annotations")
                    .privileges("read", "view_index_metadata", "write")
                    .build(),
                // Security
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".siem-signals-*", ".lists-*", ".items-*")
                    .privileges("read", "view_index_metadata", "write", "maintenance")
                    .build(),
                // Alerts-as-data
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(
                        ReservedRolesStore.ALERTS_BACKING_INDEX,
                        ReservedRolesStore.ALERTS_INDEX_ALIAS,
                        ReservedRolesStore.PREVIEW_ALERTS_BACKING_INDEX_ALIAS,
                        ReservedRolesStore.PREVIEW_ALERTS_INDEX_ALIAS
                    )
                    .privileges("read", "view_index_metadata", "write")
                    .build() },
            new RoleDescriptor.ApplicationResourcePrivileges[] {
                RoleDescriptor.ApplicationResourcePrivileges.builder()
                    .application("kibana-.kibana")
                    .resources("*")
                    .privileges("all")
                    .build() },
            null,
            null,
            MetadataUtils.DEFAULT_RESERVED_METADATA,
            null
        );
    }

    private static RoleDescriptor kibanaAdminUser(String name, Map<String, Object> metadata) {
        return new RoleDescriptor(
            name,
            null,
            null,
            new RoleDescriptor.ApplicationResourcePrivileges[] {
                RoleDescriptor.ApplicationResourcePrivileges.builder()
                    .application("kibana-.kibana")
                    .resources("*")
                    .privileges("all")
                    .build() },
            null,
            null,
            metadata,
            null
        );
    }

    public static RoleDescriptor kibanaSystemRoleDescriptor(String name) {
        return new RoleDescriptor(
            name,
            new String[] {
                "monitor",
                "manage_index_templates",
                MonitoringBulkAction.NAME,
                "manage_saml",
                "manage_token",
                "manage_oidc",
                // For Fleet package upgrade
                "manage_pipeline",
                "manage_ilm",
                // For the endpoint package that ships a transform
                "manage_transform",
                InvalidateApiKeyAction.NAME,
                "grant_api_key",
                "manage_own_api_key",
                GetBuiltinPrivilegesAction.NAME,
                "delegate_pki",
                // To facilitate ML UI functionality being controlled using Kibana security privileges
                "manage_ml",
                // The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core
                "cluster:admin/analyze",
                // To facilitate using the file uploader functionality
                "monitor_text_structure",
                // To cancel tasks and delete async searches
                "cancel_task" },
            new RoleDescriptor.IndicesPrivileges[] {
                // System indices defined in KibanaPlugin
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".kibana*", ".reporting-*")
                    .privileges("all")
                    .allowRestrictedIndices(true)
                    .build(),
                RoleDescriptor.IndicesPrivileges.builder().indices(".monitoring-*").privileges("read", "read_cross_cluster").build(),
                RoleDescriptor.IndicesPrivileges.builder().indices(".management-beats").privileges("create_index", "read", "write").build(),
                // To facilitate ML UI functionality being controlled using Kibana security privileges
                RoleDescriptor.IndicesPrivileges.builder().indices(".ml-anomalies*", ".ml-stats-*").privileges("read").build(),
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".ml-annotations*", ".ml-notifications*")
                    .privileges("read", "write")
                    .build(),
                // APM agent configuration - system index defined in KibanaPlugin
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".apm-agent-configuration")
                    .privileges("all")
                    .allowRestrictedIndices(true)
                    .build(),
                // APM custom link index creation - system index defined in KibanaPlugin
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".apm-custom-link")
                    .privileges("all")
                    .allowRestrictedIndices(true)
                    .build(),
                // APM telemetry queries APM indices in kibana task runner
                RoleDescriptor.IndicesPrivileges.builder().indices("apm-*").privileges("read", "read_cross_cluster").build(),
                // Data telemetry reads mappings, metadata and stats of indices
                RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("view_index_metadata", "monitor").build(),
                // Endpoint diagnostic information. Kibana reads from these indices to send telemetry
                RoleDescriptor.IndicesPrivileges.builder().indices(".logs-endpoint.diagnostic.collection-*").privileges("read").build(),
                // Fleet Server indices. Kibana create this indice before Fleet Server use them.
                // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents
                RoleDescriptor.IndicesPrivileges.builder().indices(".fleet*").allowRestrictedIndices(true).privileges("all").build(),
                // Legacy "Alerts as data" used in Security Solution.
                // Kibana user creates these indices; reads / writes to them.
                RoleDescriptor.IndicesPrivileges.builder().indices(ReservedRolesStore.ALERTS_LEGACY_INDEX).privileges("all").build(),
                // "Alerts as data" internal backing indices used in Security Solution, Observability, etc.
                // Kibana system user creates these indices; reads / writes to them via the aliases (see below).
                RoleDescriptor.IndicesPrivileges.builder().indices(ReservedRolesStore.ALERTS_BACKING_INDEX).privileges("all").build(),
                // "Alerts as data" public index aliases used in Security Solution, Observability, etc.
                // Kibana system user uses them to read / write alerts.
                RoleDescriptor.IndicesPrivileges.builder().indices(ReservedRolesStore.ALERTS_INDEX_ALIAS).privileges("all").build(),
                // "Alerts as data" public index alias used in Security Solution
                // Kibana system user uses them to read / write alerts.
                RoleDescriptor.IndicesPrivileges.builder().indices(ReservedRolesStore.PREVIEW_ALERTS_INDEX_ALIAS).privileges("all").build(),
                // "Alerts as data" internal backing indices used in Security Solution
                // Kibana system user creates these indices; reads / writes to them via the aliases (see below).
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(ReservedRolesStore.PREVIEW_ALERTS_BACKING_INDEX_ALIAS)
                    .privileges("all")
                    .build(),
                // Endpoint / Fleet policy responses. Kibana requires read access to send telemetry
                RoleDescriptor.IndicesPrivileges.builder().indices("metrics-endpoint.policy-*").privileges("read").build(),
                // Endpoint metrics. Kibana requires read access to send telemetry
                RoleDescriptor.IndicesPrivileges.builder().indices("metrics-endpoint.metrics-*").privileges("read").build(),
                // Fleet package install and upgrade
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(
                        "logs-*",
                        "synthetics-*",
                        "traces-*",
                        "/metrics-.*&~(metrics-endpoint\\.metadata_current_default)/",
                        ".logs-endpoint.action.responses-*",
                        ".logs-endpoint.diagnostic.collection-*",
                        ".logs-endpoint.actions-*"
                    )
                    .privileges(UpdateSettingsAction.NAME, PutMappingAction.NAME, RolloverAction.NAME)
                    .build(),
                // For ILM policy for APM & Endpoint packages that have delete action
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(".logs-endpoint.diagnostic.collection-*", "traces-apm.sampled-*")
                    .privileges(DeleteIndexAction.NAME)
                    .build(),
                // For src/dest indices of the Endpoint package that ships a transform
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices("metrics-endpoint.metadata*")
                    .privileges("read", "view_index_metadata")
                    .build(),
                RoleDescriptor.IndicesPrivileges.builder()
                    .indices(
                        "metrics-endpoint.metadata_current_default",
                        ".metrics-endpoint.metadata_current_default",
                        ".metrics-endpoint.metadata_united_default"
                    )
                    .privileges("create_index", "delete_index", "read", "index")
                    .build(), },
            null,
            new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) },
            null,
            MetadataUtils.DEFAULT_RESERVED_METADATA,
            null
        );
    }

    public static boolean isReserved(String role) {
        return RESERVED_ROLES.containsKey(role)
            || UsernamesField.SYSTEM_ROLE.equals(role)
            || UsernamesField.XPACK_ROLE.equals(role)
            || UsernamesField.ASYNC_SEARCH_ROLE.equals(role);
    }

    public Map<String, Object> usageStats() {
        return Collections.emptyMap();
    }

    public RoleDescriptor roleDescriptor(String role) {
        return RESERVED_ROLES.get(role);
    }

    public Collection<RoleDescriptor> roleDescriptors() {
        return RESERVED_ROLES.values();
    }

    public static Set<String> names() {
        return RESERVED_ROLES.keySet();
    }

    @Override
    public void accept(Set<String> roleNames, ActionListener<RoleRetrievalResult> listener) {
        final Set<RoleDescriptor> descriptors = roleNames.stream()
            .map(RESERVED_ROLES::get)
            .filter(Objects::nonNull)
            .collect(Collectors.toSet());
        listener.onResponse(RoleRetrievalResult.success(descriptors));
    }

    @Override
    public String toString() {
        return "reserved roles store";
    }
}


text:
import logging

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import FormView

from ..forms import GraduadoForm, SelectCarreraForm
from ..models import Alumno

# Get an instance of a logger
logger = logging.getLogger('fiubar')

context = {'slug': 'facultad'}

class HomePageView(LoginRequiredMixin, TemplateView):

    template_name = "carreras/carreras_home.html"

class AddView(LoginRequiredMixin, FormView):

    template_name = "carreras/carrera_add_form.html"
    form_class = SelectCarreraForm

    def form_valid(self, form):
        request = self.request
        plancarrera = form.cleaned_data['plancarrera']
        begin_date = form.cleaned_data['begin_date']
        alumno = Alumno.objects.create(user=request.user,
                                       carrera=plancarrera.carrera,
                                       plancarrera=plancarrera,
                                       begin_date=begin_date)
        if alumno:
            messages.add_message(request, messages.SUCCESS,
                                 _('Carrera agregada.'))
            logger.info("%s - carreras-add: user '%s', plancarrera '%s'" %
                        (request.META.get('REMOTE_ADDR'), request.user,
                         plancarrera.name))
        else:
            messages.add_message(request, messages.ERROR,
                                 _('Ya cursás esa carrera.'))
            logger.error("%s - carreras-add: user '%s', plancarrera '%s', "
                         '"Ya cursás esa carrera."' %
                         (request.META.get('REMOTE_ADDR'),
                          request.user, plancarrera.name))
        return HttpResponseRedirect(reverse('facultad:carreras-home'))

    def form_invalid(self, form):

        logger.error("%s - carreras-add: user '%s', plancarrera '%s', "
                     '"Form not valid."' %
                     (self.request.META.get('REMOTE_ADDR'), self.request.user,
                      form.cleaned_data.get('plancarrera')))

        return super(AddView, self).form_invalid(form)

class DeleteView(LoginRequiredMixin, TemplateView):

    template_name = "carreras/carrera_delete.html"

    def get(self, request, *args, **kwargs):
        plancarrera = kwargs.get('plancarrera')

        if plancarrera:
            alumno = get_object_or_404(Alumno, user=request.user,
                                       plancarrera__short_name=plancarrera)
            alumno.delete()
            messages.add_message(request, messages.SUCCESS,
                                 _('Carrera borrada.'))

            logger.info("%s - carreras-delete: user '%s', plancarrera '%s'" %
                        (request.META.get('REMOTE_ADDR'),
                         request.user, plancarrera))

            return HttpResponseRedirect(reverse('facultad:carreras-home'))

        return super().get(request, *args, **kwargs)

class GraduadoView(LoginRequiredMixin, FormView):

    template_name = "carreras/carrera_graduado_form.html"
    form_class = GraduadoForm
    success_url = reverse_lazy('facultad:carreras-home')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['alumno'] = self.alumno
        return context

    def get_initial(self):
        """
        Returns the initial data to use for forms on this view.
        """
        initial = super(GraduadoView, self).get_initial()

        self.plan_carrera = self.kwargs['plancarrera']
        initial['plancarrera'] = self.plan_carrera

        alumno = get_object_or_404(Alumno, user=self.request.user,
                                   plancarrera__short_name=self.plan_carrera)
        self.alumno = alumno

        if alumno.graduado_date:
            initial['month'] = alumno.graduado_date.month
            initial['year'] = alumno.graduado_date.year

        return initial

    def form_valid(self, form):
        alumno = get_object_or_404(Alumno, user=self.request.user,
                                   plancarrera__short_name=self.plan_carrera)
        alumno.graduado_date = form.cleaned_data['graduado_date']
        alumno.save()

        messages.add_message(self.request, messages.SUCCESS,
                             _('¡Felicitaciones!'))

        logger.info("%s - carreras-graduado: user '%s', plancarrera '%s'" %
                    (self.request.META.get('REMOTE_ADDR'),
                     self.request.user, alumno.plancarrera))

        return super(GraduadoView, self).form_valid(form)

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        try:
            return super(GraduadoView, self).post(request, *args, **kwargs)
        except ValidationError as e:
            form = self.get_form()
            messages.add_message(self.request, messages.ERROR, e.messages[0])
            return self.form_invalid(form)

class GraduadoDeleteView(LoginRequiredMixin, RedirectView):

    url = reverse_lazy('facultad:carreras-home')

    def get(self, request, *args, **kwargs):
        plancarrera = kwargs.get('plancarrera')

        alumno = get_object_or_404(Alumno, user=request.user,
                                   plancarrera__short_name=plancarrera)
        alumno.del_graduado()

        messages.add_message(request, messages.INFO,
                             _('A seguir estudiando...'))

        return super(GraduadoDeleteView, self).get(request, args, kwargs)