import { html, nothing, TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { nanoid } from 'nanoid'
import { Headers } from '@api-client/core/lib/headers/Headers.js'
import type {
  IApiEndPoint,
  IApiOperation,
  IApiParameter,
  IApiPayload,
  IApiServer,
} from '@api-client/core/amf/definitions/Api.js'
import { IRequestProxyInit } from '@api-client/core/proxy/RequestProxy.js'
import { IProxyResult } from '@api-client/core/proxy/Proxy.js'
import { IRequestLog } from '@api-client/core/models/RequestLog.js'
import { Request, type IRequest } from '@api-client/core/models/Request.js'
import { IApiScalarNode } from '@api-client/core/amf/definitions/Shapes.js'
import { Kind as HttpRequestKind } from '@api-client/core/models/HttpRequest.js'
import { cancelEvent } from '@api-client/core/lib/events/Utils.js'
import { Events as CoreEvents } from '@api-client/core/events/Events.js'
import { ensureContentType, generateHeaders } from '../../amf/lib/Utils.js'
import { cachePayloadValue, getPayloadValue, PayloadInfo, readCachePayloadValue } from '../../amf/lib/PayloadUtils.js'
import {
  applyUrlParameters,
  applyUrlVariables,
  computeEndpointUri,
  applyQueryParamStringToObject,
} from '../../amf/lib/UrlUtils.js'
import { AmfInputParser } from '../../amf/lib/AmfInputParser.js'
import * as InputCache from '../../amf/lib/InputCache.js'
import { Events } from '../../events/amf/Events.js'
import { EventTypes } from '../../events/amf/EventTypes.js'
import { EventTypes as UiEventTypes } from '../../events/EventTypes.js'
import { OperationParameter, PopulationInfo, SecuritySelectorListItem, ServerType } from '../../amf/types.js'
import { Oauth2Credentials } from '../authorization/types.js'
import BodyTextEditor from '../http/BodyTextEditor.js'
import BodyUrlEncodedEditor from '../http/BodyUrlEncodedEditor.js'
import { ifProperty } from '../../directives/if-property.js'
import ApiElement from '../ApiElement.js'
import type Input from '../../md/input/Input.js'
import type RadioElement from '../../md/radio/internals/RadioElement.js'
import { AmfParameters } from '../../amf/lib/AmfParameters.js'
import { UiDropdownListSelection } from '../../md/dropdown-list/internals/UiDropdownList.js'
import { AmfSecurity } from '../../amf/lib/AmfSecurity.js'
import { SnackNotifications } from '../../md/notification/SnackNotifications.js'
import '../../define/amf/api-authorization-editor.js'
import '../../define/amf/api-server-picker.js'
import '../../define/http/http-body-urlencoded-editor.js'
import '../../define/http/http-body-multipart-editor.js'
import '../../define/http/http-body-text-editor.js'
import '../../md/icons/ui-icon.js'
import '../../md/text-field/ui-text-field.js'
import '../../md/switch/ui-switch.js'
import '../../md/button/ui-filled-tonal-button.js'
import '../../md/button/ui-outlined-button.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/progress/ui-progress.js'

export const EventCategory = 'API Request editor'

/**
 * @fires amfstoreopget - Dispatched when querying for an operation data
 * @fires amfstoreopgetparent - Dispatched when querying for an operation data for an endpoint
 * @fires amfstoreendpointget - Dispatched when querying for an endpoint data
 * @fires apistoreserverquery - Dispatched when querying for API servers
 * @fires amfstoreapiprotocols - Dispatched when querying for API protocols
 * @fires amfstoreapiversion - Dispatched when querying for API version
 * @fires response - Dispatched when the response was received by the editor.
 */
export default class ApiRequestEditor extends ApiElement {
  /**
   * The currently selected media type for the payloads.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor mimeType: string | undefined

  /**
   * When set it renders a label with the computed URL.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor urlLabel: boolean | undefined

  /**
   * When set it renders the URL input above the URL parameters.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor urlEditor: boolean | undefined

  /**
   * If set it computes `hasOptional` property and shows checkbox in the
   * form to show / hide optional properties.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor allowHideOptional: boolean | undefined

  /**
   * When set, renders "add custom" item button.
   * If the element is to be used without AMF model this should always
   * be enabled. Otherwise users won't be able to add a parameter.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor allowCustom: boolean | undefined

  /**
   * OAuth2 redirect URI.
   * This value **must** be set in order for OAuth 1/2 to work properly.
   * @attribute
   */
  @property({ type: String }) accessor redirectUri: string | undefined

  /**
   * Final request URL including settings like `baseUri`, AMF
   * model settings and user provided parameters.
   * @attribute
   */
  @property({ type: String }) accessor url: string | undefined

  /**
   * Holds the value of the currently selected server
   * Data type: URI
   * @attribute
   */
  @property({ type: String }) accessor serverValue: string | undefined

  /**
   * Holds the type of the currently selected server
   * Values: `server` | `uri` | `custom`
   * @attribute
   */
  @property({ type: String }) accessor serverType: string | undefined

  /**
   * Optional property to set
   * If true, the server selector is not rendered
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor noServerSelector: boolean | undefined

  /**
   * Optional property to set
   * If true, the server selector custom base URI option is rendered
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor allowCustomBaseUri: boolean | undefined

  /**
   * List of credentials source
   */
  @property({ type: Array }) accessor credentialsSource: Oauth2Credentials[] | undefined

  /**
   * The domain id key of the selected security
   * @attribute
   */
  @property({ type: String }) accessor selectedSecurity: string | undefined

  /**
   * Optional. The parent endpoint id. When set it uses this value to query for the endpoint
   * instead of querying for a parent through the operation id.
   * Also, when `endpoint` is set and the `endpointId` match then it ignores querying for
   * the endpoint.
   * @attribute
   */
  @property({ type: String }) accessor endpointId: string | undefined

  protected domainIdValue: string | undefined

  /**
   * The domain id (AMF id) of the rendered operation.
   * @attribute
   */
  get domainId(): string | undefined {
    return this.domainIdValue
  }

  @property({ type: String, reflect: true })
  set domainId(value: string | undefined) {
    const old = this.domainIdValue
    /* istanbul ignore if */
    if (old === value) {
      return
    }
    this.domainIdValue = value
    this.requestUpdate('domainId', old)
    this.processDebounce()
  }

  protected endpointValue: IApiEndPoint | undefined

  get endpoint(): IApiEndPoint | undefined {
    return this.endpointValue
  }

  set endpoint(value: IApiEndPoint | undefined) {
    const old = this.endpointValue
    if (old === value) {
      return
    }
    this.endpointValue = value
    this.requestUpdate()
    this.readUrlData()
    this.notifyChange()
  }

  /**
   * The HTTP method name.
   */
  get httpMethod(): string | undefined {
    const op = this.operationValue
    if (!op) {
      return undefined
    }
    return op.method
  }

  /**
   * A flag set when the request is being made.
   */
  @state() accessor loadingRequest: boolean | undefined

  protected baseUriValue: string | undefined

  /**
   * A base URI for the API. To be set if RAML spec is missing `baseUri`
   * declaration and this produces invalid URL input. This information
   * is passed to the URL editor that prefixes the URL with `baseUri` value
   * if passed URL is a relative URL.
   * @attribute
   */
  get baseUri(): string | undefined {
    return this.baseUriValue
  }

  @property({ type: String })
  set baseUri(value: string | undefined) {
    const old = this.baseUriValue
    if (old === value) {
      return
    }
    this.baseUriValue = value
    this.readUrlData()
    this.requestUpdate('baseUri', old)
  }

  protected serversValue: IApiServer[] | undefined

  /**
   * The computed list of servers.
   */
  get servers(): IApiServer[] | undefined {
    return this.serversValue
  }

  protected serverLocalValue: IApiServer | undefined

  get server(): IApiServer | undefined {
    return this.serverLocalValue
  }

  /**
   * This is the final computed value for the baseUri to propagate downwards
   * If baseUri is defined, return baseUri
   * Else, return the selectedServerValue if serverType is not `server`
   */
  get effectiveBaseUri(): string {
    if (this.baseUri) {
      return this.baseUri
    }
    if (this.serverType !== 'server') {
      return this.serverValue as string
    }
    return ''
  }

  /**
   * @return True when there is not enough servers to render the selector
   */
  get serverSelectorHidden(): boolean {
    const { serversCount = 0, noServerSelector = false, allowCustomBaseUri } = this
    return noServerSelector || (!allowCustomBaseUri && serversCount < 2)
  }

  /**
   * The list of security list items to render.
   * An operation may have multiple security definition in an or/and fashion.
   * This allows to render the selector to pick the current security.
   */
  protected securityList: SecuritySelectorListItem[] | undefined

  /**
   * The security requirement for the operation or undefined.
   */
  get security(): SecuritySelectorListItem[] | undefined {
    const items = this.securityList
    if (Array.isArray(items) && items.length) {
      return items
    }
    return undefined
  }

  /**
   * The currently rendered payload, if any.
   */
  get payload(): IApiPayload | undefined {
    const { payloads } = this
    if (!payloads) {
      return undefined
    }
    const { mimeType } = this
    let payload: IApiPayload | undefined
    if (mimeType) {
      payload = payloads.find((i) => i.mediaType === mimeType)
    }
    if (!payload) {
      ;[payload] = payloads
    }
    return payload
  }

  /**
   * The list of all possible payloads for this operation.
   */
  get payloads(): IApiPayload[] | undefined {
    const operation = this.operationValue
    if (!operation) {
      return undefined
    }
    const { request } = operation
    if (!request) {
      return undefined
    }
    const { payloads } = request
    if (!Array.isArray(payloads) || !payloads.length) {
      return undefined
    }
    return payloads
  }

  /**
   * @returns API defined base URI (current server + the endpoint)
   */
  get apiBaseUri(): string | undefined {
    const endpoint = this.endpointValue
    const server = this.serverLocalValue
    if (!server || !endpoint) {
      return undefined
    }
    const { path = '' } = endpoint
    let { url = '' } = server
    if (url.endsWith('/')) {
      url = url.substr(0, url.length - 1)
    }
    return `${url}${path}`
  }

  protected urlInvalidValue?: boolean

  /**
   * True when the URL input is invalid.
   */
  get urlInvalid(): boolean | undefined {
    return this.urlInvalidValue
  }

  protected protocolsValue: string[] | undefined

  /**
   * The API's protocols.
   */
  get protocols(): string[] | undefined {
    return this.protocolsValue
  }

  protected versionValue: string | undefined

  /**
   * The API's version.
   */
  get version(): string | undefined {
    return this.versionValue
  }

  queryDebouncerTimeout: number

  /**
   * Set when the selection change, this is a JS object created form the
   * supportedOperation definition of the AMF graph.
   */
  protected operationValue?: IApiOperation

  /**
   * The list of parameter groups that are opened when `allowHideOptional` is set.
   */
  openedOptional: string[]

  protected debounceValue: unknown

  protected urlSearchRegexpValue: RegExp | undefined

  serversCount?: number

  amfParameters: AmfParameters

  /**
   * This is set while the HTTP request is being executed.
   * It allows to abort the request.
   */
  requestController?: AbortController

  /**
   * @constructor
   */
  constructor() {
    super()
    /**
     * The timeout after which the `queryGraph()` function is called
     * in the debouncer.
     */
    this.queryDebouncerTimeout = 1
    this.authRedirectChangedHandler = this.authRedirectChangedHandler.bind(this)
    this.populateAnnotatedFieldsHandler = this.populateAnnotatedFieldsHandler.bind(this)

    this.openedOptional = []
    this.amfParameters = new AmfParameters(this)
    this.amfParameters.addEventListener('render', () => this.requestUpdate())
    this.amfParameters.addEventListener('change', () => this.notifyChange())
    this.amfParameters.addEventListener('paramchange', this.paramChangeHandler.bind(this) as EventListener)
    InputCache.registerLocal(this)
    this.graphChangeHandler = this.graphChangeHandler.bind(this)
    this.addEventListener(UiEventTypes.Http.Request.send, this.internalSendHandler.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.processDebounce()
    this.amfParameters.globalCache = true
    const node = this.eventsTarget
    node.addEventListener(EventTypes.Store.graphChange, this.graphChangeHandler)
    node.addEventListener(EventTypes.Request.redirectUriChange, this.authRedirectChangedHandler as EventListener)
    node.addEventListener(
      EventTypes.Request.populateAnnotatedFields,
      this.populateAnnotatedFieldsHandler as EventListener
    )
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    if (this.debounceValue) {
      clearTimeout(this.debounceValue as number)
      this.debounceValue = undefined
    }
    const node = this.eventsTarget
    node.removeEventListener(EventTypes.Store.graphChange, this.graphChangeHandler)
    node.removeEventListener(EventTypes.Request.redirectUriChange, this.authRedirectChangedHandler as EventListener)
    node.removeEventListener(
      EventTypes.Request.populateAnnotatedFields,
      this.populateAnnotatedFieldsHandler as EventListener
    )
  }

  /**
   * Handler for the event dispatched by the store when the graph model change.
   */
  protected graphChangeHandler(): void {
    this.processDebounce()
  }

  /**
   * Calls the `queryGraph()` function in a debouncer.
   */
  protected processDebounce(): void {
    if (this.debounceValue) {
      clearTimeout(this.debounceValue as number)
    }
    this.debounceValue = setTimeout(() => {
      this.debounceValue = undefined
      this.processGraph()
    }, this.queryDebouncerTimeout)
  }

  /**
   * Reads the URL data from the ApiUrlDataModel library and sets local variables.
   */
  readUrlData(): void {
    this.updateServerParameters()
    this.orderPathParameters()
    this.computeUrlValue()
  }

  /**
   * It makes sure that the path parameters are rendered in order (server, endpoint)
   * and in order of occurrence in the URL.
   */
  protected orderPathParameters(): void {
    const params = this.getOrderedPathParams()
    if (!params || !params.length) {
      return
    }
    const { amfParameters } = this
    amfParameters.parametersValue = amfParameters.parametersValue.filter((item) => item.binding !== 'path')
    amfParameters.parametersValue = amfParameters.parametersValue.concat(params)
  }

  protected paramChangeHandler(e: CustomEvent<string>): void {
    this.paramChanged(e.detail)
  }

  /**
   * A function to be overwritten by child classes to execute an action when a parameter has changed.
   */
  protected paramChanged(key: string): void {
    this.computeUrlValue()
    this.validateUrl()
    const { amfParameters } = this
    const param = amfParameters.parametersValue.find((p) => p.paramId === key)
    if (param && param.binding === 'header' && (param.parameter.name || '').toLocaleLowerCase() === 'content-type') {
      const value = InputCache.get(this, param.paramId, true) as string
      this.mimeType = value
    }
  }

  /**
   * Computes the URL value for the current serves, selected server, and endpoint's path.
   */
  protected computeUrlValue(): void {
    const { effectiveBaseUri, server, protocols, version } = this
    const endpoint = this.endpointValue
    const result = computeEndpointUri({
      baseUri: effectiveBaseUri,
      server,
      endpoint,
      protocols,
      version,
      forceHttpProtocol: true,
    })
    const { amfParameters } = this
    const params = amfParameters.parametersValue.map((p) => p.parameter)
    const report = AmfInputParser.reportRequestInputs(params, InputCache.getStore(this, true), amfParameters.nilValues)
    let url = applyUrlVariables(result, report.path, true)
    url = applyUrlParameters(url, report.query, true)
    this.url = url
  }

  /**
   * Checks if the current server has variables and update the parameters array
   */
  protected updateServerParameters(): void {
    const { server } = this
    const source = 'server'
    const { amfParameters } = this
    // clears previously set request parameters related to server configuration.
    amfParameters.parametersValue = amfParameters.parametersValue.filter((item) => item.source !== source)
    if (!server || ['custom', 'uri'].includes(this.serverType || '')) {
      // we don't need to compute server variables for a custom URLs.
      return
    }
    if (Array.isArray(server.variables) && server.variables.length) {
      server.variables.forEach((param) => {
        const item: OperationParameter = {
          binding: param.binding || '',
          paramId: param.id,
          parameter: param,
          source,
        }
        if (param.schema) {
          item.schema = param.schema
          item.schemaId = param.schema.id
        }
        amfParameters.parametersValue.push(item)
      })
    }
  }

  /**
   * Checks if the current endpoint has variables and requests them when needed.
   */
  protected updateEndpointParameters(): void {
    const source = 'endpoint'
    const { amfParameters } = this
    // clears previously set request parameters related to server configuration.
    amfParameters.parametersValue = amfParameters.parametersValue.filter((item) => item.source !== source)
    const endpoint = this.endpointValue
    if (!endpoint) {
      // we don't need to compute endpoint variables for a custom URLs.
      return
    }
    if (Array.isArray(endpoint.parameters) && endpoint.parameters.length) {
      endpoint.parameters.forEach((param) => {
        const item: OperationParameter = {
          binding: param.binding || '',
          paramId: param.id,
          parameter: param,
          source,
        }
        if (param.schema) {
          item.schema = param.schema
          item.schemaId = param.schema.id
        }
        amfParameters.parametersValue.push(item)
      })
    }
  }

  reset(): void {
    this.securityList = undefined
    this.mimeType = undefined
    this.amfParameters.parametersValue = []
  }

  /**
   * Processes the selection of the domain id for an operation.
   */
  async processGraph(): Promise<void> {
    await this.queryEndpoint()
    await this.queryOperation()
    await this.queryServers()
    await this.queryProtocols()
    this.processEndpoint()
    this.processOperation()
    this.processSecurity()
    this.processPayload()
    this.computeUrlValue()
    this.readUrlData()
    this.notifyChange()
  }

  /**
   * Queries the store for the operation data, when needed.
   */
  async queryOperation(): Promise<void> {
    const { domainId } = this
    if (!domainId || domainId === 'undefined') {
      // this.operationValue = undefined;
      return
    }
    if (this.operationValue && this.operationValue.id === domainId) {
      // in case the operation model was provided via the property setter.
      return
    }
    try {
      const endpointId = this.endpointValue && this.endpointValue.id
      const info = await Events.Operation.get(this, domainId, endpointId)
      this.operationValue = info
    } catch (e) {
      const ex = e as Error
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for API operation data: ${ex.message}`, this.localName)
    }
  }

  /**
   * Queries the store for the endpoint data.
   * @returns {Promise<void>}
   */
  async queryEndpoint(): Promise<void> {
    const { domainId, endpointId } = this
    if (!domainId || domainId === 'undefined') {
      // this.endpointValue = undefined;
      return
    }
    if (this.endpointValue && this.endpointValue.id === endpointId) {
      // in case the endpoint model was provided via the property setter.
      return
    }
    this.endpointValue = undefined
    try {
      const info = await (endpointId
        ? Events.Endpoint.get(this, endpointId)
        : Events.Operation.getParent(this, domainId))
      this.endpointValue = info
    } catch (e) {
      const ex = e as Error
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for API endpoint data: ${ex.message}`, this.localName)
    }
  }

  /**
   * Queries for the current servers value.
   */
  async queryServers(): Promise<void> {
    const { domainId } = this
    const endpointId = this.endpointValue && this.endpointValue.id
    try {
      const info = await Events.Server.query(this, {
        endpointId,
        methodId: domainId,
      })
      this.serversValue = info || undefined
    } catch (e) {
      const ex = e as Error
      this.serversValue = undefined
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for API servers: ${ex.message}`, this.localName)
    }
  }

  /**
   * Queries the API store for the API protocols list.
   */
  async queryProtocols(): Promise<void> {
    this.protocolsValue = undefined
    try {
      const info = await Events.Api.protocols(this)
      this.protocolsValue = info
    } catch (e) {
      const ex = e as Error
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for API protocols list: ${ex.message}`, this.localName)
    }
  }

  /**
   * Searches for the current operation endpoint and sets variables from the endpoint definition.
   */
  protected processEndpoint(): void {
    this.updateEndpointParameters()
    this.computeUrlRegexp()
  }

  /**
   * Collects operations input parameters into a single object.
   */
  protected processOperation(): void {
    const source = 'request'
    const operation = this.operationValue
    const { amfParameters } = this
    // clears previously set request parameters (query, path, headers)
    amfParameters.parametersValue = amfParameters.parametersValue.filter((item) => item.source !== source)
    if (!operation) {
      return
    }
    const { request } = operation
    if (!request) {
      return
    }
    const uri: IApiParameter[] = []
    if (Array.isArray(request.uriParameters)) {
      // OAS has this weird thing where you can define URI parameters on the endpoint and the operation.
      // this eliminates the duplicates and merges the schemas
      request.uriParameters.forEach((param) => {
        if (!param.paramName) {
          uri.push(param)
          return
        }
        const index = amfParameters.parametersValue.findIndex(
          (p) => p.binding === 'path' && p.parameter.paramName === param.paramName
        )
        if (index !== -1) {
          // remove the parameter that is being replaced by the local definition
          amfParameters.parametersValue.splice(index, 1)
        }
        uri.push(param)
      })
    }
    this.appendToParams(uri, source)
    this.appendToParams(request.queryParameters, source)
    this.appendToParams(request.headers, source)
    this.appendToParams(request.cookieParameters, source)
  }

  /**
   * Processes security information for the UI.
   */
  protected processSecurity(): void {
    const operation = this.operationValue
    if (!operation) {
      return
    }
    const { security } = operation
    this.securityList = AmfSecurity.readSecurityList(security)
    const [first] = this.securityList
    this.selectedSecurity = first ? first.security.id : undefined
  }

  /**
   * Makes sure the correct mime type is selected for the current selection.
   */
  protected processPayload(): void {
    const operation = this.operationValue
    if (!operation) {
      return
    }
    const { request } = operation
    if (!request) {
      return
    }
    const { payloads = [] } = request
    if (!payloads.length) {
      return
    }
    let mime = this.mimeType

    if (mime) {
      const has = payloads.find((p) => p.mediaType === mime)
      if (!has) {
        mime = undefined
      }
    }
    if (!mime) {
      const first = payloads.find((p) => p.mediaType)
      if (first) {
        mime = first.mediaType
      }
    }
    this.mimeType = mime
  }

  /**
   * Appends a list of parameters to the list of rendered parameters
   */
  protected appendToParams(list: IApiParameter[], source: string): void {
    const params = this.amfParameters.parametersValue
    if (Array.isArray(list)) {
      list.forEach((param) => {
        params.push({
          paramId: param.id,
          parameter: param,
          binding: param.binding || '',
          source,
          schema: param.schema,
          schemaId: param.schema && param.schema.id ? param.schema.id : undefined,
        })
      })
    }
  }

  /**
   * A handler for the change event dispatched by the `raw` editor.
   */
  protected rawBodyChangeHandler(e: Event): void {
    const editor = e.target as BodyTextEditor
    const { value, dataset } = editor
    const { payloadId } = dataset
    if (!payloadId) {
      return
    }
    cachePayloadValue(payloadId, value as string)
  }

  /**
   * A handler for the change event dispatched by the
   * `urlEncode` editor.
   * Updated the local value, model, and notifies the change.
   */
  protected modelBodyEditorChangeHandler(e: Event): void {
    const editor = e.target as BodyUrlEncodedEditor
    const { value, model, dataset } = editor
    const { payloadId } = dataset
    if (!payloadId) {
      return
    }
    cachePayloadValue(payloadId, value as string, model)
  }

  protected handleAuthSelect(e: CustomEvent<UiDropdownListSelection>): void {
    const { item } = e.detail
    const { id } = item.dataset
    if (!id) {
      return
    }
    this.selectedSecurity = id
    this.requestUpdate()
  }

  /**
   * Handles send button click.
   * Depending on authorization validity it either sends the
   * request or forces authorization and sends the request.
   */
  protected sendHandler(): void {
    this.execute()
  }

  /**
   * To be called when the user want to execute the request but
   * authorization is invalid (missing values).
   * This function brings the auth panel to front and displays error toast
   */
  async authAndExecute(): Promise<void> {
    const { shadowRoot } = this
    if (!shadowRoot) {
      throw new Error(`Invalid state. The element is not initialized.`)
    }

    const panel = shadowRoot.querySelector('api-authorization-editor')
    if (!panel) {
      throw new Error(`Invalid state. The authorization editor element was removed from the DOM.`)
    }
    await panel.authorize()
    const valid = panel.validate()
    if (valid) {
      this.execute()
    }
  }

  protected async proxyRequest(init: IRequestProxyInit): Promise<IProxyResult<IRequestLog> | undefined> {
    let result: IProxyResult<IRequestLog> | undefined
    try {
      result = await CoreEvents.Transport.Core.request(init, this)
    } catch {
      // ...
    }
    return result
  }

  /**
   * Proxies the request to the endpoint using the core libraries.
   */
  async execute(): Promise<void> {
    if (this.loadingRequest) {
      this.abort()
    }
    Events.Telemetry.event(this, {
      category: EventCategory,
      action: 'request-execute',
      label: 'true',
    })
    const request = await this.serialize()
    const ctrl = new AbortController()
    this.requestController = ctrl
    const init: IRequestProxyInit = {
      kind: HttpRequestKind,
      request: request.expects,
      authorization: request.authorization,
      signal: ctrl.signal,
    }
    this.loadingRequest = true
    const result = await this.proxyRequest(init)
    this.loadingRequest = false
    if (!result) {
      if (ctrl.signal.aborted) {
        return
      }
      const message = `The request was not sent. Proxy not available.`
      SnackNotifications.notify(message, { timeout: 2000, close: true })
      throw new Error(message)
    }
    this.dispatchEvent(new CustomEvent<IRequestLog>('response', { detail: result.result }))
  }

  /**
   * Uses the `requestController` to abort the ongoing HTTP request.
   */
  abort(): void {
    Events.Telemetry.event(this, {
      category: EventCategory,
      action: 'request-abort',
      label: 'true',
    })
    const { requestController } = this
    if (!requestController) {
      return
    }
    requestController.abort()
    this.requestController = undefined
    this.loadingRequest = false
  }

  /**
   * Event handler for abort click.
   */
  protected abortHandler(): void {
    this.abort()
  }

  /**
   * Serializes the state of the request editor.
   */
  async serialize(): Promise<IRequest> {
    const op = this.operationValue
    if (!op) {
      throw new Error(`No API operation defined on the editor`)
    }
    const method = (op.method || 'get').toUpperCase()
    const params: IApiParameter[] = []
    const { amfParameters } = this
    amfParameters.parametersValue.forEach((item) => {
      const { parameter, enabled } = item
      if (enabled === false) {
        return
      }
      params.push(parameter)
    })
    const report = AmfInputParser.reportRequestInputs(params, InputCache.getStore(this, true), amfParameters.nilValues)
    const serverUrl = computeEndpointUri({
      baseUri: this.effectiveBaseUri,
      server: this.server,
      endpoint: this.endpointValue,
      protocols: this.protocols,
      forceHttpProtocol: true,
    })
    let url = applyUrlVariables(serverUrl, report.path, true)
    url = applyUrlParameters(url, report.query, true)
    const headers = generateHeaders(report.header)

    const instance = new Request()
    const expects = instance.getExpects()
    expects.method = method
    expects.url = url
    expects.headers = headers

    if (!['GET', 'HEAD'].includes(method)) {
      let body: string | FormData | Blob | File | ArrayBuffer | undefined
      const { payload } = this
      if (payload) {
        const info = readCachePayloadValue(payload.id)
        if (info && info.value) {
          body = info.value
        }
      }
      if (body instanceof FormData) {
        const parser = new Headers(expects.headers)
        parser.delete('content-type')
        expects.headers = parser.toString()
      } else if (payload) {
        expects.headers = ensureContentType(expects.headers || '', payload.mediaType)
      }
      if (typeof body !== 'undefined') {
        await expects.writePayload(body)
      }
    }

    const result = instance.toJSON()

    const authElement = this.shadowRoot?.querySelector('api-authorization-editor')
    if (authElement) {
      const auth = authElement.serialize()
      result.authorization = auth
    }
    return result
  }

  /**
   * Handler for the `oauth2-redirect-uri-changed` custom event. Changes
   * the `redirectUri` property.
   */
  protected authRedirectChangedHandler(e: CustomEvent): void {
    this.redirectUri = e.detail.value
  }

  /**
   * Handle event for populating annotated fields in the editor.
   */
  protected populateAnnotatedFieldsHandler(e: CustomEvent): void {
    const populationInfoArray: PopulationInfo[] = e.detail.values
    const { parametersValue = [] } = this.amfParameters
    const allAnnotated = parametersValue.filter(
      (param) =>
        Array.isArray(param.parameter.customDomainProperties) && !!param.parameter.customDomainProperties.length
    )
    let update = false
    populationInfoArray.forEach(({ annotationName, annotationValue, fieldValue }) => {
      allAnnotated.forEach((item) => {
        const { parameter, paramId } = item
        const hasAnnotation = (parameter.customDomainProperties || []).some(
          (prop) => prop.name === annotationName && (prop.extension as IApiScalarNode).value === annotationValue
        )
        if (!hasAnnotation) {
          return
        }
        InputCache.set(this, paramId, fieldValue, true)
        update = true
      })
    })
    if (update) {
      this.requestUpdate()
    }
    /* @TODO populate values for the security schema. */
  }

  /**
   * Computes a current server value for selection made in the server selector.
   */
  protected updateServer(): void {
    const { serverValue, serverType, servers = [] } = this
    if (serverType !== 'server') {
      this.serverLocalValue = undefined
    } else {
      this.serverLocalValue = servers.find((server) => server.url === serverValue)
    }
    this.readUrlData()
    this.computeUrlRegexp()
  }

  /**
   * Handler for the change dispatched from the server selector.
   */
  protected serverCountHandler(e: CustomEvent): void {
    const { value = 0 } = e.detail
    this.serversCount = value
    this.updateServer()
    this.requestUpdate()
  }

  /**
   * Handler for the change dispatched from the server selector.
   */
  protected serverHandler(e: CustomEvent): void {
    const { value, type } = e.detail
    this.serverType = type
    this.serverValue = value
    this.updateServer()
    this.computeUrlRegexp()
  }

  /**
   * Computes a regexp for the base URI defined in the server to process URL input change
   * and set the `.urlSearchRegexpValue` value.
   * This should be computed only when a server and en endpoint change.
   */
  protected computeUrlRegexp(): void {
    const { effectiveBaseUri } = this
    let value
    if (effectiveBaseUri) {
      value = computeEndpointUri({
        baseUri: effectiveBaseUri,
        endpoint: this.endpointValue,
        forceHttpProtocol: true,
      })
    } else {
      value = this.apiBaseUri
    }
    if (!value) {
      this.urlSearchRegexpValue = undefined
    } else {
      value = value.replace('?', '\\?')
      value = value.replace(/(\.|\/)/g, '\\$1')
      value = value.replace(/{[\w\\+]+}/g, "([a-zA-Z0-9\\$\\-_\\.~\\+!'\\(\\)\\*\\{\\}]+)")
      value += '.*'
      this.urlSearchRegexpValue = new RegExp(value)
    }
  }

  protected handleMediaTypeSelect(e: Event): void {
    const node = e.target as RadioElement
    const { value } = node
    this.mimeType = value
    const ctParam = this.amfParameters.parametersValue.find(
      (p) => p.binding === 'header' && (p.parameter.name || '').toLocaleLowerCase() === 'content-type'
    )
    if (ctParam) {
      InputCache.set(this, ctParam.paramId, value, true)
    }
  }

  protected async notifyResizeAfterUpdate(): Promise<void> {
    await this.updateComplete
    this.dispatchEvent(new Event('resize', { bubbles: true, composed: true }))
  }

  /**
   * Toggles optional parameter groups.
   */
  protected handleOptionalToggle(e: Event): void {
    const node = e.target as HTMLElement
    const { target } = node.dataset
    if (!target) {
      return
    }
    if (!Array.isArray(this.openedOptional)) {
      this.openedOptional = []
    }
    if (this.openedOptional.includes(target)) {
      const index = this.openedOptional.indexOf(target)
      this.openedOptional.splice(index, 1)
    } else {
      this.openedOptional.push(target)
    }
    this.requestUpdate()
    this.notifyResizeAfterUpdate()
  }

  /**
   * When enabled it adds a new custom parameter to the request section defined in the source button.
   */
  protected addCustomHandler(e: Event): void {
    const button = e.currentTarget as HTMLElement
    const { type } = button.dataset
    if (!['query', 'header'].includes(type as string)) {
      return
    }
    const id = nanoid()
    const param: OperationParameter = {
      binding: type || '',
      source: 'custom',
      paramId: id,
      parameter: {
        id,
        required: true,
        binding: type,
        name: '',
        examples: [],
        payloads: [],
        customDomainProperties: [],
        types: [],
      },
    }
    this.amfParameters.parametersValue.push(param)
    this.requestUpdate()
  }

  /**
   * Updates path/query model from user input.
   *
   * @param e The change event
   */
  protected handleUrlEditorChange(e: Event): void {
    const { value } = e.target as Input
    let matches
    const uriRegexp = this.urlSearchRegexpValue
    // parameters must be in order from server to the endpoint
    const pathParams = this.getOrderedPathParams()
    let changed = false
    if (pathParams.length && uriRegexp) {
      matches = value.match(uriRegexp)
      if (matches) {
        matches.shift()
        changed = this.applyUriValues(matches, pathParams)
      }
    }
    const matchesNew = value.match(/[^&?]*?=[^&?]*/g)
    if (matchesNew) {
      const params: Record<string, string | string[]> = {}
      matchesNew.forEach((item) => applyQueryParamStringToObject(item, params))
      const qpChanged = this.applyQueryParamsValues(params)
      if (!changed) {
        changed = qpChanged
      }
    }
    this.url = value
    this.validateUrl()
    this.notifyChange()
    this.requestUpdate()
  }

  /**
   * Sets the value of `.urlInvalidValue` and therefore `urlInvalid` properties.
   */
  validateUrl(): void {
    this.urlInvalidValue = !this.readUrlValidity()
    this.requestUpdate()
  }

  /**
   * Validates the current URL value.
   * @returns True when the current URL is a valid URL.
   */
  protected readUrlValidity(): boolean {
    const { url } = this
    if (!url) {
      return false
    }
    if (typeof url !== 'string') {
      return false
    }
    if (url.indexOf('{') !== -1 && url.indexOf('}') !== -1) {
      return false
    }
    const { shadowRoot } = this
    if (!shadowRoot) {
      return true
    }
    const inputElement = shadowRoot.querySelector('.url-input') as Input
    if (inputElement) {
      return inputElement.reportValidity()
    }
    return true
  }

  /**
   * Reads the ordered list of path parameters from the server and the endpoint.
   */
  protected getOrderedPathParams(): OperationParameter[] {
    const result: OperationParameter[] = []
    const { effectiveBaseUri } = this
    let url
    if (effectiveBaseUri) {
      url = computeEndpointUri({
        baseUri: effectiveBaseUri,
        endpoint: this.endpointValue,
        forceHttpProtocol: true,
      })
    } else {
      url = this.apiBaseUri
    }
    if (!url) {
      return result
    }
    const matches = url.match(/{[\w\\+]+}/g)
    if (!matches) {
      return result
    }
    const all = this.amfParameters.parametersValue
    matches.forEach((tpl) => {
      const name = tpl.substr(1, tpl.length - 2)
      const param = all.find((p) => p.binding === 'path' && p.parameter.name === name)
      if (param) {
        result.push(param)
      }
    })
    return result
  }

  /**
   * Applies values from the `values` array to the uri parameters which names are in the `names` array.
   * Both lists are ordered list of parameters.
   *
   * @param values Values for the parameters
   * @param params List of path parameters.
   * @returns True when any parameter was changed.
   */
  protected applyUriValues(values: string[], params: OperationParameter[]): boolean {
    let changed = false
    for (let i = 0, len = params.length; i < len; i++) {
      const value = values[i]
      if (value && value[0] === '{') {
        // This is still a variable
        continue
      }
      const param = params[i]
      if (InputCache.get(this, param.paramId, true) !== value) {
        InputCache.set(this, param.paramId, value, true)
        changed = true
      }
    }
    return changed
  }

  /**
   * Applies query parameters values to the render list.
   *
   * @returns True when any parameter was changed.
   */
  protected applyQueryParamsValues(map: Record<string, string | string[]>): boolean {
    if (!map) {
      return false
    }
    const keys = Object.keys(map)
    let changed = false
    keys.forEach((key) => {
      const value = map[key]
      if (value && value[0] === '{') {
        // This is still a variable
        return
      }
      const param = this.amfParameters.parametersValue.find((p) => p.binding === 'query' && p.parameter.name === key)
      if (param) {
        if (InputCache.get(this, param.paramId, true) !== value) {
          InputCache.set(this, param.paramId, value, true)
          changed = true
        }
      }
    })
    return changed
  }

  protected internalSendHandler(e: Event): void {
    e.stopPropagation()
    this.execute()
  }

  /**
   * A handler for the change event on the authorization panel.
   */
  protected authorizationChangeHandler(): void {
    this.notifyChange()
  }

  override render(): TemplateResult {
    return html` <div class="content">
      ${this.serverSelectorTemplate()} ${this.urlLabelTemplate()} ${this.renderUrlEditor()} ${this.parametersTemplate()}
      ${this.headersTemplate()} ${this.mediaTypeSelectorTemplate()} ${this.bodyTemplate()}
      ${this.authorizationTemplate()} ${this.formActionsTemplate()}
    </div>`
  }

  /**
   * @return Template for the request URL label.
   */
  protected urlLabelTemplate(): TemplateResult | typeof nothing {
    const { urlLabel, url } = this
    if (!urlLabel) {
      return nothing
    }
    return html`<div class="url-label text-selectable" title="Current request URL">${url}</div>`
  }

  protected authorizationTemplate(): TemplateResult | typeof nothing {
    const { security } = this
    if (!security) {
      return nothing
    }
    const { selectedSecurity, redirectUri, credentialsSource } = this
    const rendered = security.find((i) => i.security.id === selectedSecurity)
    if (!rendered) {
      return nothing
    }
    return html`
      <section class="authorization params-section">
        <div class="section-title"><span class="label">Credentials</span></div>
        ${security.length > 1 ? this.authorizationSelectorTemplate(security, rendered) : ''}
        <api-authorization-editor
          .security="${rendered.security}"
          .oauth2RedirectUri="${redirectUri}"
          .credentialsSource="${credentialsSource}"
          @change="${this.authorizationChangeHandler}"
        ></api-authorization-editor>
      </section>
    `
  }

  /**
   * @returns The template for the security drop down selector.
   */
  protected authorizationSelectorTemplate(
    security: SecuritySelectorListItem[],
    selected: SecuritySelectorListItem
  ): TemplateResult {
    const finalValue = selected.labels.join(', ')
    return html`
      <ui-dropdown-list
        class="authorization-selector"
        closeOnOutsideClick
        matchTriggerWidth
        @select="${this.handleAuthSelect}"
      >
        <ui-text-field
          class="dropdown-trigger"
          label="Authorization method"
          readOnly
          name="authorization"
          .value="${finalValue || ''}"
          @select="${cancelEvent}"
        >
          <ui-icon icon="arrowDropDown" role="presentation" slot="suffix"></ui-icon>
        </ui-text-field>
        <ui-list slot="dropdown" role="menu" selector="[data-value]" selectActive>
          ${security.map((item) => this.authorizationSelectorItemTemplate(item))}
        </ui-list>
      </ui-dropdown-list>
    `
  }

  /**
   * @returns The template for the security drop down selector list item.
   */
  protected authorizationSelectorItemTemplate(info: SecuritySelectorListItem): TemplateResult {
    const { labels, types, security } = info

    const label = labels.join(', ')
    const type = types.join(', ')
    const single = !type
    return html`
      <ui-list-item role="menuitem" lines="${single ? 'one' : 'two'}" data-id="${security.id}">
        ${label} ${!single ? html`<div slot="supporting-text">${type}</div>` : ''}
      </ui-list-item>
    `
  }

  protected formActionsTemplate(): TemplateResult {
    const loading = this.loadingRequest
    return html` <div class="action-bar">
      ${loading ? this.abortButtonTemplate() : this.sendButtonTemplate()}
      <ui-progress ?hidden="${!loading}" indeterminate class="request-progress"></ui-progress>
    </div>`
  }

  /**
   * Creates a template for the "abort" button.
   */
  protected abortButtonTemplate(): TemplateResult {
    return html` <ui-filled-tonal-button class="send-button abort" @click="${this.abortHandler}"
      >Abort</ui-filled-tonal-button
    >`
  }

  /**
   * Creates a template for the "send" or "auth and send" button.
   */
  protected sendButtonTemplate(): TemplateResult {
    return html` <ui-filled-tonal-button class="send-button" @click="${this.sendHandler}">Send</ui-filled-tonal-button>`
  }

  /**
   * @return A template for the server selector
   */
  protected serverSelectorTemplate(): TemplateResult {
    const { serverType, serverValue, allowCustomBaseUri, serverSelectorHidden, domainId } = this
    return html` <api-server-picker
      ?hidden="${serverSelectorHidden}"
      ?custom="${allowCustomBaseUri}"
      .value="${serverValue}"
      .type="${serverType as ServerType}"
      .domainId="${domainId}"
      domainType="operation"
      auto
      @serverscountchanged="${this.serverCountHandler}"
      @apiserverchanged="${this.serverHandler}"
    >
      <slot name="custom-base-uri" slot="custom-base-uri"></slot>
    </api-server-picker>`
  }

  /**
   * @return
   */
  protected parametersTemplate(): TemplateResult | typeof nothing {
    const qp: OperationParameter[] = []
    const path: OperationParameter[] = []
    // NOTE, the "* Required field" has been added after the a11y audit of API Console.
    let hasRequired = false
    const { amfParameters } = this
    amfParameters.parametersValue.forEach((item) => {
      if (!hasRequired && item.parameter && item.parameter.required) {
        hasRequired = true
      }
      if (item.binding === 'query') {
        qp.push(item)
      } else if (item.binding === 'path') {
        path.push(item)
      }
    })
    const { allowCustom, openedOptional = [] } = this
    if (!allowCustom && !qp.length && !path.length) {
      return nothing
    }
    const pathOptions = Object.freeze({ required: true })
    const queryClasses = {
      'query-params': true,
      'hide-optional': !!this.allowHideOptional && !openedOptional.includes('query'),
    }
    return html`
      <section class="params-section parameter">
        <div class="section-title"><span class="label">Parameters</span></div>
        ${hasRequired ? html`<p class="required-field">* Required field</p>` : ''}
        <div class="path-params">${path.map((param) => amfParameters.parameterTemplate(param, pathOptions))}</div>
        <div class="${classMap(queryClasses)}">
          ${this.renderToggleOptional('query', qp)} ${qp.map((param) => amfParameters.parameterTemplate(param))}
          ${allowCustom ? this.addCustomButtonTemplate('query') : ''}
        </div>
      </section>
    `
  }

  protected headersTemplate(): TemplateResult | typeof nothing {
    const headers: OperationParameter[] = []
    // NOTE, the "* Required field" has been added after the a11y audit of API Console.
    let hasRequired = false
    const { amfParameters } = this
    amfParameters.parametersValue.forEach((item) => {
      if (item.binding === 'header') {
        headers.push(item)
        if (!hasRequired && item.parameter && item.parameter.required) {
          hasRequired = true
        }
      }
    })
    const { allowCustom, openedOptional = [] } = this
    if (!allowCustom && !headers.length) {
      return nothing
    }
    const classes = {
      'header-params': true,
      'hide-optional': !!this.allowHideOptional && !openedOptional.includes('header'),
    }
    return html`
      <section class="params-section header" aria-label="HTTP headers">
        <div class="section-title"><span class="label">Headers</span></div>
        ${hasRequired ? html`<p class="required-field">* Required field</p>` : ''}
        ${this.renderToggleOptional('header', headers)}
        <div class="${classMap(classes)}">
          ${headers.map((param) => amfParameters.parameterTemplate(param))}
          ${allowCustom ? this.addCustomButtonTemplate('header') : ''}
        </div>
      </section>
    `
  }

  /**
   * @returns The template for the add custom parameter button
   */
  protected addCustomButtonTemplate(type: string): TemplateResult {
    return html`
      <div class="add-custom-button">
        <ui-outlined-button
          data-type="${type}"
          title="Adds a new custom parameter to the request"
          @click="${this.addCustomHandler}"
        >
          <ui-icon icon="add" role="presentation"></ui-icon>
          Add custom
        </ui-outlined-button>
      </div>
    `
  }

  /**
   * @return The template for the payload's mime type selector.
   */
  protected mediaTypeSelectorTemplate(): TemplateResult | typeof nothing {
    const { payloads, mimeType } = this
    if (!payloads || payloads.length === 1) {
      return nothing
    }
    const mimes = payloads.map((p) => p.mediaType)
    let index = mimes.indexOf(mimeType)
    if (index === -1) {
      index = 0
    }

    return html`
      <div class="payload-mime-selector">
        <span id="body-mime" class="label-large">Body media type</span>
        ${mimes.map(
          (item) => html`
            <label class="label-large">
              <ui-radio
                name="mediaTypeValue"
                value="${item || ''}"
                .checked="${item === mimeType}"
                @change="${this.handleMediaTypeSelect}"
              ></ui-radio>
              ${item}
            </label>
          `
        )}
      </div>
    `
  }

  /**
   * @returns The template for the body editor.
   */
  protected bodyTemplate(): TemplateResult | typeof nothing {
    const { payload } = this
    if (!payload) {
      return nothing
    }
    const info = getPayloadValue(payload)
    const mimeType = payload.mediaType
    let content: TemplateResult
    if (mimeType === 'application/x-www-form-urlencoded') {
      content = this.formDataEditorTemplate(info, payload.id)
    } else if (mimeType === 'multipart/form-data') {
      content = this.multipartEditorTemplate(info, payload.id)
    } else {
      content = this.rawEditorTemplate(info, payload.id, mimeType)
    }
    return html`
      <section class="params-section body" aria-label="HTTP body">
        <div class="section-title"><span class="label">Body</span></div>
        ${content}
      </section>
    `
  }

  /**
   * @returns The template for the editor that specializes in the URL encoded form data
   */
  protected formDataEditorTemplate(info: PayloadInfo, id: string): TemplateResult {
    const editorModel = info.model
    const effectiveValue = Array.isArray(editorModel) && editorModel.length ? undefined : info.value
    return html`
      <http-body-urlencoded-editor
        class="body-editor"
        autoEncode
        .value="${ifProperty(effectiveValue)}"
        .model="${ifProperty(editorModel)}"
        data-payload-id="${id}"
        @change="${this.modelBodyEditorChangeHandler}"
      ></http-body-urlencoded-editor>
    `
  }

  /**
   * @returns The template for the editor that specializes in the multipart form data
   */
  protected multipartEditorTemplate(info: PayloadInfo, id: string): TemplateResult {
    const editorModel = info.model
    const effectiveValue = Array.isArray(editorModel) && editorModel.length ? undefined : info.value
    return html`
      <http-body-multipart-editor
        class="body-editor"
        .value="${ifProperty(effectiveValue)}"
        .model="${ifProperty(editorModel)}"
        data-payload-id="${id}"
        @change="${this.modelBodyEditorChangeHandler}"
      ></http-body-multipart-editor>
    `
  }

  /**
   * @returns The template for the editor that specializes in any text data
   */
  protected rawEditorTemplate(info: PayloadInfo, id: string, mimeType?: string): TemplateResult {
    let schemas
    if (Array.isArray(info.schemas) && info.schemas.length) {
      schemas = info.schemas
    }
    return html`
      <http-body-text-editor
        class="body-editor"
        .value="${info.value as string}"
        .contentType="${mimeType}"
        .schemas="${ifProperty(schemas)}"
        data-payload-id="${id}"
        @change="${this.rawBodyChangeHandler}"
      ></http-body-text-editor>
    `
  }

  /**
   * @param target The name of the target parameter group.
   * @param params The list of parameters. When all are required or empty it won't render then button.
   * @returns Template for the switch button to toggle visibility of the optional items.
   */
  protected renderToggleOptional(target: string, params: OperationParameter[]): TemplateResult | typeof nothing {
    const { openedOptional = [], allowHideOptional } = this
    if (!allowHideOptional || !params || !params.length) {
      return nothing
    }
    const optional = params.some((p) => !!p.parameter && !p.parameter.required)
    if (!optional) {
      return nothing
    }
    const checked = openedOptional.includes(target)
    return html`
      <div class="optional-checkbox">
        <label class="label-large">
          <ui-switch
            class="toggle-optional-switch"
            .checked="${checked}"
            @change="${this.handleOptionalToggle}"
            title="Toggles optional parameters"
            data-target="${target}"
            name="toggleOptional"
            aria-label="Toggle optional parameters"
          ></ui-switch>
          Show optional parameters
        </label>
      </div>
    `
  }

  /**
   * @returns A template for the URL editor.
   */
  protected renderUrlEditor(): TemplateResult | typeof nothing {
    const { urlEditor, url, urlInvalid } = this
    if (!urlEditor) {
      return nothing
    }
    return html`
      <div class="url-input-wrapper">
        <ui-text-field
          name="url"
          type="url"
          class="url-input"
          ?invalid="${urlInvalid}"
          required
          invalidText="The URL is invalid"
          .value="${url || ''}"
          @change="${this.handleUrlEditorChange}"
          @blur="${this.validateUrl}"
          label="Request URL"
        ></ui-text-field>
      </div>
    `
  }
}
