import { html, TemplateResult, PropertyValues, nothing } from 'lit'
import { property, state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { HttpRequest, type IHttpRequest } from '@api-client/core/models/HttpRequest.js'
import { type IRequestAuthorization, RequestAuthorization } from '@api-client/core/models/RequestAuthorization.js'
import { type IRequestConfig, RequestConfig } from '@api-client/core/models/RequestConfig.js'
import type { IHttpAssertion } from '@api-client/core/models/http-flows/HttpAssertion.js'
import { type DeserializedPayload, PayloadSerializer } from '@api-client/core/lib/transformers/PayloadSerializer.js'
import type { IHttpFlow } from '@api-client/core/models/http-flows/HttpFlows.js'
import type { IRequestUiMeta } from '@api-client/core/models/RequestUiMeta.js'
import type { AuthorizationType, IOAuth2Authorization } from '@api-client/core/models/Authorization.js'
import type { IProperty } from '@api-client/core/models/Property.js'
import { SecurityProcessor } from '@api-client/core/authorization/lib/SecurityProcessor.js'
import { Events as CoreEvents } from '@api-client/core/events/Events.js'
import { EventTypes } from '../../events/EventTypes.js'
import { Events } from '../../events/Events.js'
import ApiElement from '../ApiElement.js'
import { authTypeToLabel } from '../authorization/AuthLabels.js'
import type UrlInputEditorElement from './UrlInput.js'
import type BodyEditor from './BodyEditor.js'
import type { TabSelectionDetail } from '../../md/tabs/internals/Tabs.js'
import type UrlParamsForm from './UrlParamsForm.js'
import type HeadersForm from './HeadersForm.js'
import type { Authorization } from '../authorization/ui/Authorization.js'
import type HttpAssertions from './HttpAssertions.element.js'
import type RequestConfigElement from './RequestConfigElement.js'
import type { UiDialogClosingReason } from '../../md/dialog/internals/Dialog.js'
import '../../md/icons/ui-icon.js'
import '../../define/http/http-url-input.js'
import '../../define/http/http-url-params-form.js'
import '../../define/http/http-headers-form.js'
import '../../define/http/http-snippets.js'
import '../../define/http/http-body-editor.js'
import '../../define/http/http-assertions.js'
import '../../define/http/http-flows.js'
import '../../define/http/http-request-config.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/button/ui-button.js'
import '../../md/tabs/ui-tabs.js'
import '../../md/tabs/ui-tab.js'
import '../../md/divider/ui-divider.js'
import '../../md/dialog/ui-dialog.js'
import '../../define/authorization/basic-authorization.js'
import '../../define/authorization/bearer-authorization.js'
import '../../define/authorization/ntlm-authorization.js'
import '../../define/authorization/apikey-authorization.js'
import '../../define/authorization/oauth2-authorization.js'
import '../../define/authorization/oidc-authorization.js'
import '../../define/authorization/cc-authorization.js'
import HttpFlows from './HttpFlows.element.js'

export const HttpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'CONNECT', 'OPTIONS', 'TRACE']
export const NonPayloadMethods = ['GET', 'HEAD']

export type SelectedTab =
  | 'parameters'
  | 'headers'
  | 'payload'
  | 'authorization'
  | 'code-snippets'
  | 'assertions'
  | 'config'
  | 'flows'

export interface SendDetails {
  /**
   * The request object to send.
   */
  request: IHttpRequest
  /**
   * Optional authorization configuration.
   */
  authorization: IRequestAuthorization[] | undefined
  /**
   * The request options.
   */
  config: IRequestConfig | undefined
  /**
   * HTTP tests to perform.
   */
  assertions: IHttpAssertion[] | undefined
}

/**
 * An element that renders an HTTP editor.
 *
 * @fires send - A custom event when the user requested to send the request. The detail object is the editor's request.
 * @fires abort - When the user requested to abort the ongoing request.
 * @fires authorizationchange - When authorization data change.
 * @fires uichange - When the UI model change.
 */
export default class RequestEditor extends ApiElement {
  /**
   * A promise set when reading payload from the `request` object.
   * It is resolved after the payload is read anf the `payload` property is set.
   */
  protected readingPayloadPromise = Promise.resolve()

  /**
   * A promise set when the payload is being written to the `request` object.
   * It is resolved after the `payload` is set on the request as a serializable object.
   */
  protected writingPayloadPromise = Promise.resolve()

  /**
   * A flag used to determine when the payload is being read for the synchronous code.
   */
  @state() accessor readingPayload = false

  /**
   * The base request object.
   */
  @property({ type: Object }) accessor request: IHttpRequest = new HttpRequest().toJSON()

  /**
   * The request payload.
   * It can be just a string or an object with the payload data depending on the content type.
   */
  @state() accessor payload: DeserializedPayload | undefined

  /**
   * The current content type value.
   */
  @state() accessor contentType: string | undefined

  @property({ type: Array }) accessor authorization: IRequestAuthorization[] | undefined

  @property({ type: Array }) accessor assertions: IHttpAssertion[] | undefined

  @property({ type: Array }) accessor flows: IHttpFlow[] | undefined

  /**
   * Redirect URL for the OAuth2 authorization.
   * If can be also set by dispatching `oauth2-redirect-url-changed`
   * with `value` property on the `detail` object.
   */
  @property({ type: String }) accessor oauth2RedirectUri: string | undefined

  /**
   * When set the editor is in read only mode.
   */
  @property({ type: Boolean, reflect: true }) accessor readOnly = false

  /**
   * A value to be passed to the OAuth 2 `authorizationUri` property in case
   * if current configuration has no value.
   * This is to be used as a default value.
   */
  @property({ type: String, reflect: true }) accessor oauth2AuthorizationUri: string | undefined

  /**
   * A value to be passed to the OAuth 2 `accessTokenUri` property in case
   * if current configuration has no value.
   * This is to be used as a default value.
   */
  @property({ type: String, reflect: true }) accessor oauth2AccessTokenUri: string | undefined

  /**
   * When set it ignores all `content-*` headers when the request method is `GET`.
   * When not set or `false` it renders a warning message.
   */
  @property({ type: Boolean, reflect: true }) accessor ignoreContentOnGet = false

  /**
   * When set the `content-` headers warning dialog is rendered.
   */
  @property({ type: Boolean, reflect: true }) accessor contentHeadersDialogOpened = false

  /**
   * To be set when the request is being transported.
   */
  @property({ type: Boolean, reflect: true }) accessor loading = false

  /**
   * An index of currently opened tab.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor selectedTab: SelectedTab = 'parameters'

  /**
   * Optional instance of the `RequestUiMeta` to store the UI state.
   */
  @property({ type: Object }) accessor ui: IRequestUiMeta | undefined

  /**
   * HTTP request configuration for the transport.
   */
  @property({ type: Object }) accessor config = new RequestConfig().toJSON()

  /**
   * @returns True when the request cannot have the payload on the message.
   */
  get isPayload(): boolean {
    const { request } = this
    if (!request || !request.method) {
      return false
    }
    return !NonPayloadMethods.includes(request.method)
  }

  protected _awaitingOAuth2authorization = false

  /**
   * This is set by the internal logic. When `ignoreContentOnGet` is set and the headers have `content-` headers
   * a dialog is rendered when trying to send the request. When the user chooses to ignore the warning this
   * flag makes sure that `send()` does not check headers.
   *
   * @todo(pawel): This should be done in the request logic module plugin.
   * Plugins can stop request indefinitely or cancel it.
   */
  ignoreValidationOnGet = false

  @state() protected accessor snippets: IHttpRequest | undefined

  constructor() {
    super()
    this.config.enabled = false
    this.handleInternalSend = this.handleInternalSend.bind(this)
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.eventsTarget.addEventListener(EventTypes.Http.Request.send, this.handleInternalSend)
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.eventsTarget.addEventListener(EventTypes.Http.Request.send, this.handleInternalSend)
  }

  override firstUpdated(changedProperties: PropertyValues<this>): void {
    super.firstUpdated(changedProperties)
    this.computeSnippetsRequest()
  }

  protected override willUpdate(changedProperties: PropertyValues<this>): void {
    if (changedProperties.has('request')) {
      this.setupContentType()
      this.readingPayloadPromise = this.readPayload()
    }
    if (changedProperties.has('ui') && this.ui) {
      if (this.ui.selectedEditor) {
        this.selectedTab = this.ui.selectedEditor as SelectedTab
      }
    }
    super.willUpdate(changedProperties)
  }

  protected setupContentType(request = this.request): void {
    if (!request) {
      this.contentType = undefined
      return
    }
    if (!request.headersModel) {
      this.contentType = undefined
      return
    }
    const item = request.headersModel.find((i) => i.name.toLowerCase() === 'content-type')
    if (!item) {
      this.contentType = undefined
      return
    }
    this.contentType = item.value as string
    this.setPayloadMimeOption(request, this.contentType)
  }

  protected async readPayload(): Promise<void> {
    const { request } = this
    if (!request || !request.payload) {
      this.payload = undefined
      return
    }
    this.readingPayload = true
    try {
      this.payload = await PayloadSerializer.deserialize(request.payload)
    } catch {
      this.payload = undefined
    } finally {
      this.readingPayload = false
    }
  }

  /**
   * Validates state of the URL.
   * @return True if the URL has a structure that looks like
   * an URL which means scheme + something
   */
  validateUrl(): boolean {
    const { shadowRoot } = this
    if (!shadowRoot) {
      return true
    }
    const panel = shadowRoot.querySelector('http-url-input')
    if (!panel) {
      return true
    }
    return panel.checkValidity()
  }

  /**
   * Checks if current request requires calling `authorize()` on the OAuth2 method.
   *
   * @return This returns `true` only for valid OAuth 2 method that has no access token.
   */
  requiresAuthorization(): boolean {
    const { authorization = [] } = this
    const oauth = authorization.find((method) => method.enabled && method.type === 'oauth 2')
    if (!oauth) {
      return false
    }
    const authMethod = this.shadowRoot?.querySelector('oauth2-authorization')
    if (!authMethod) {
      return false
    }
    const cnf = oauth.config as IOAuth2Authorization
    if (authMethod.checkValidity() && !cnf.accessToken) {
      return true
    }
    return false
  }

  /**
   * Validates headers for `Content-*` entries against current method.
   * @param request The request object
   * @return True if headers are invalid.
   */
  validateContentHeaders(request: IHttpRequest): boolean {
    const method = request.method || 'get'
    if (method.toLowerCase() !== 'get') {
      return false
    }
    const any = !!request.headersModel && request.headersModel.some((i) => i.name.toLowerCase().startsWith('content-'))
    if (any) {
      return true
    }
    return false
  }

  /**
   * Serializes the current values to the `IHttpRequest`
   */
  async serialize(): Promise<IHttpRequest> {
    if (!this.request) {
      const instance = new HttpRequest()
      return instance.toJSON()
    }
    await this.writingPayloadPromise
    // make sure we copy everything.
    const instance = new HttpRequest(this.request)
    return instance.toJSON()
  }

  /**
   * Dispatches the send request event to the HTTP request engine.
   */
  async send(): Promise<void> {
    if (this.loading || !this.request) {
      return
    }
    if (!this.validateUrl()) {
      return
    }
    if (this.requiresAuthorization()) {
      const authMethod = this.shadowRoot?.querySelector('oauth2-authorization')
      if (authMethod) {
        authMethod.authorize()
        this._awaitingOAuth2authorization = true
      }
      return
    }
    const request = await this.serialize()
    // store the URL in the history
    await Events.HttpClient.Model.UrlHistory.add(request.url as string, this)
    if (!this.ignoreValidationOnGet && this.validateContentHeaders(request)) {
      this.contentHeadersDialogOpened = true
      return
    }
    this.ignoreValidationOnGet = false
    this.notifySend(request)
    CoreEvents.Telemetry.event(
      {
        category: 'Request editor',
        action: 'Send request',
      },
      this
    )
  }

  protected notifySend(request: IHttpRequest): void {
    this.dispatchEvent(
      new CustomEvent<SendDetails>('send', {
        detail: {
          request,
          authorization: this.authorization,
          config: this.config,
          assertions: this.assertions,
        },
      })
    )
  }

  /**
   * If the request does not exist it creates one.
   * @returns The request object.
   */
  protected getRequest(): IHttpRequest {
    if (!this.request) {
      const instance = new HttpRequest()
      this.request = instance.toJSON()
    }
    return this.request
  }

  /**
   * Aborts the request
   */
  abort(): void {
    this.dispatchEvent(new Event('abort'))
  }

  protected handleInternalSend(e: Event): void {
    e.stopPropagation()
    this.send()
  }

  protected handleVerbSelection(e: CustomEvent): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const { verb } = target.dataset
    if (verb) {
      this.selectVerb(verb)
    }
  }

  /**
   * @param verb The HTTP verb (operation, method)
   */
  selectVerb(verb: string): void {
    const request = this.getRequest()
    request.method = verb
    this.requestUpdate()
    this.notifyChange()
    this.notifyPropertyChanged('method', verb)
    if (!this.isPayload && this.selectedTab === 'payload') {
      this.selectedTab = 'headers'
    }
    CoreEvents.Telemetry.event(
      {
        category: 'Request editor',
        action: 'Method selected',
        label: verb,
      },
      this
    )
    this.computeSnippetsRequest()
  }

  /**
   * The handler for the URL editor change event
   */
  protected handlerUrlChange(e: Event): void {
    const panel = e.target as UrlInputEditorElement
    const { value } = panel
    this.selectUrl(value)
  }

  /**
   * Commits the change in the URL.
   * @param value The new URL value
   */
  selectUrl(value: string): void {
    const request = this.getRequest()
    request.url = value
    this.requestUpdate()
    this.notifyChange()
    this.notifyPropertyChanged('url', value)
    this.computeSnippetsRequest()
  }

  protected handleTabSelect(e: CustomEvent<TabSelectionDetail>): void {
    this.selectedTab = e.detail.item.getAttribute('aria-controls') as SelectedTab
    if (this.ui) {
      this.ui.selectedEditor = this.selectedTab
      this.notifyPropertyChanged('ui', this.ui)
    }
    CoreEvents.Telemetry.event(
      {
        category: 'Request editor',
        action: 'Editor switched',
        label: this.selectedTab,
      },
      this
    )
  }

  protected handleParamsChange(e: Event): void {
    const input = e.target as UrlParamsForm
    this.selectUrl(input.value)
  }

  /**
   * The handler for the headers change event from the headers editor.
   */
  handleHeadersChange(e: Event): void {
    e.preventDefault()
    const editor = e.target as HeadersForm
    const { model } = editor
    this.selectHeaders(model)
    if (!this.ui) {
      return
    }
    // const { value, source } = node;
    if (!this.ui.headers) {
      this.ui.headers = {}
    }
    // this.ui.headers.source = source;
    this.notifyPropertyChanged('ui', this.ui)
  }

  selectHeaders(model: IProperty[]): void {
    const request = this.getRequest()
    request.headersModel = model
    const ct = model.find((h) => h.name.toLowerCase() === 'content-type')
    this.contentType = ct && (ct.value as string)
    this.setPayloadMimeOption(request, this.contentType)
    this.requestUpdate()
    this.notifyChange()
    this.computeSnippetsRequest()
  }

  /**
   * The handler for the body editor change event
   */
  protected handleBody(e: Event): void {
    e.preventDefault()
    const node = e.target as BodyEditor
    const { value, model, selected, contentType } = node
    this.writingPayloadPromise = this.updatePayload(value as DeserializedPayload, contentType)
    this.computeSnippetsRequest()
    if (!this.ui) {
      return
    }
    if (!this.ui.body) {
      this.ui.body = {}
    }
    this.ui.body.model = model
    this.ui.body.selected = selected
    this.notifyPropertyChanged('ui', this.ui)
  }

  protected async updatePayload(value: DeserializedPayload, mime?: string): Promise<void> {
    const serialized = await PayloadSerializer.serialize(value)
    const request = this.getRequest()
    request.payload = serialized
    this.setPayloadMimeOption(request, mime)
    this.payload = value
    this.requestUpdate()
    this.notifyChange()
    this.notifyPropertyChanged('payload', value)
  }

  /**
   * When `mime` is set it creates the `payloadOptions` property with the mime.
   * @param request The request to set the mime type
   * @param mime the mime type value
   */
  protected setPayloadMimeOption(request: IHttpRequest | HttpRequest, mime?: string): void {
    if (mime) {
      if (!request.payloadOptions) {
        request.payloadOptions = {}
      }
      request.payloadOptions.mime = mime
    }
  }

  protected handleFlows(e: Event): void {
    e.preventDefault()
    const node = e.target as HttpFlows
    this.selectFlows(node.model)
  }

  selectFlows(flows?: IHttpFlow[]): void {
    this.flows = flows
    this.notifyChange()
  }

  protected handleAssertions(e: Event): void {
    e.preventDefault()
    const node = e.target as HttpAssertions
    this.selectAssertions(node.model)
  }

  selectAssertions(assertions?: IHttpAssertion[]): void {
    this.assertions = assertions
    this.notifyChange()
  }

  protected handleConfig(e: Event): void {
    const node = e.target as RequestConfigElement
    this.selectRequestConfig(node.config)
  }

  selectRequestConfig(config: IRequestConfig): void {
    this.config = config
    this.notifyChange()
  }

  /**
   * Called to notify listeners about a particular property change
   *
   * @param type The property that changed. The resulting event type is
   *             the combination of this value and the `change` suffix.
   * @param value The value of the changed property
   */
  protected notifyPropertyChanged(type: string, value: unknown): void {
    this.dispatchEvent(
      new CustomEvent(`${type}change`, {
        detail: {
          value,
        },
      })
    )
  }

  protected handleContentWarningClose(e: CustomEvent<UiDialogClosingReason>): void {
    this.contentHeadersDialogOpened = false
    if (e.detail.cancelled) {
      return
    }
    this.ignoreValidationOnGet = true
    this.send()
  }

  /**
   * When a request change this recomputes the values for code snippets.
   * These are different from the actual values in the request as
   * they contain authorization values applied to it.
   */
  protected async computeSnippetsRequest(): Promise<void> {
    const serialized = await this.serialize()
    const { authorization = [] } = this
    SecurityProcessor.applyAuthorization(serialized, [...authorization], { immutable: true })
    this.snippets = serialized
  }

  /**
   * Adds a new authorization object to the request for the given type.
   *
   * @returns The created instance of the authorization.
   */
  addAuthorization(type: AuthorizationType): RequestAuthorization {
    const cnf = RequestAuthorization.fromTypedConfig(type, {}, false)
    cnf.enabled = true
    if (!this.authorization) {
      this.authorization = []
    }
    this.authorization.push(cnf.toJSON())
    this.notifyPropertyChanged('authorization', this.authorization)
    this.requestUpdate()
    return cnf
  }

  /**
   * Updates authorization schema on the editor.
   * @param schema The new schema to replace the old one
   * @param isValid Whether the schema is valid.
   */
  updateAuthorization(schema: IRequestAuthorization, isValid: boolean): void {
    const { authorization } = this
    if (!authorization) {
      return
    }
    const index = authorization.findIndex((i) => i.type === schema.type)
    if (index === -1) {
      return
    }
    schema.valid = isValid
    authorization[index] = schema
    this.notifyPropertyChanged('authorization', this.authorization)
  }

  /**
   * Removes authorization schema from the editor.
   * @param type The authorization type to remove.
   */
  removeAuthorization(type: string): void {
    const { authorization } = this
    if (!authorization) {
      return
    }
    const index = authorization.findIndex((i) => i.type === type)
    if (index === -1) {
      return
    }
    authorization.splice(index, 1)
    this.notifyPropertyChanged('authorization', this.authorization)
    this.requestUpdate()
  }

  protected handleAuthAdd(e: CustomEvent): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const selected = target.dataset.type as AuthorizationType | undefined
    if (!selected) {
      return
    }
    this.addAuthorization(selected)
  }

  protected handleAuthChange(e: Event): void {
    const editor = e.target as Authorization
    const { authorization } = editor
    this.updateAuthorization(authorization as IRequestAuthorization, editor.checkValidity())
  }

  protected handleAuthDelete(e: Event): void {
    const editor = e.target as Authorization
    const { authorization } = editor
    if (!authorization) {
      return
    }
    this.removeAuthorization(authorization.type)
  }

  protected override render(): TemplateResult {
    return html`
      ${this.renderUrlEditorArea()}
      <div class="surface2 editors">${this.renderTabs()} ${this.renderCurrentEditor()}</div>
      ${this.renderHeadersDialog()}
    `
  }

  /**
   * @returns The template for the top line with method selector, URL, and options.
   */
  protected renderUrlEditorArea(): TemplateResult {
    return html`
      <div class="url-editor">
        <div class="url-meta">${this.renderUrlMeta()}</div>
        ${this.renderSendButton()}
      </div>
    `
  }

  protected renderUrlMeta(): TemplateResult {
    return html` ${this.renderHttpMethodSelector()} ${this.renderUrlEditor()} `
  }

  /**
   * @returns The template for the HTTP method selector
   */
  protected renderHttpMethodSelector(): TemplateResult {
    const request = this.getRequest()
    const { method = 'GET' } = request

    return html`
      <ui-dropdown-list
        verticalAlign="top"
        @select="${this.handleVerbSelection}"
        class="verb-dropdown"
        closeOnOutsideClick
      >
        <ui-button color="text" class="button">
          ${method}
          <ui-icon icon="arrowDropDown"></ui-icon>
        </ui-button>
        <ui-list slot="dropdown" role="menu"> ${this.renderHttpVerbOptions()} </ui-list>
      </ui-dropdown-list>
    `
  }

  /**
   * @returns The templates for each supported HTTP methods
   */
  protected renderHttpVerbOptions(): TemplateResult[] {
    return HttpMethods.map(
      (method) => html`
        <ui-list-item role="menuitem" image="icon" data-verb="${method}">
          <div slot="start" data-method="${method.toLocaleLowerCase()}" class="http-label"></div>
          ${method}
        </ui-list-item>
      `
    )
  }

  /**
   * @returns The template for the HTTP URL editor
   */
  protected renderUrlEditor(): TemplateResult {
    const request = this.getRequest()
    const { url = '' } = request
    const { eventsTarget } = this
    return html`
      <div class="url-input">
        <http-url-input
          .value="${url}"
          .eventsTarget="${eventsTarget}"
          @change="${this.handlerUrlChange}"
        ></http-url-input>
      </div>
    `
  }

  /**
   * @returns The template for the "send" or "abort" buttons.
   */
  protected renderSendButton(): TemplateResult | string {
    if (this.loading) {
      return html`
        <ui-button color="filled" title="Abort the request" class="send-button" @click="${this.abort}">Abort</ui-button>
      `
    }
    return html`
      <ui-button color="filled" title="Send the request" class="send-button" @click="${this.send}">Send</ui-button>
    `
  }

  /**
   * @returns The template for the request editor tabs
   */
  protected renderTabs(): TemplateResult {
    const { isPayload, selectedTab } = this
    //  ui && ui.selectedEditor ||
    const s = selectedTab
    return html` <ui-tabs @select="${this.handleTabSelect}" class="editor-tabs">
      <ui-tab .selected="${s === 'parameters'}" aria-controls="parameters" id="parameters-tab">Parameters</ui-tab>
      <ui-tab .selected="${s === 'headers'}" aria-controls="headers" id="headers-tab">Headers</ui-tab>
      <ui-tab ?hidden="${!isPayload}" .selected="${s === 'payload'}" aria-controls="payload" id="payload-tab"
        >Body</ui-tab
      >
      <ui-tab .selected="${s === 'authorization'}" aria-controls="authorization" id="authorization-tab"
        >Authorization</ui-tab
      >
      <ui-tab .selected="${s === 'flows'}" aria-controls="flows" id="flows-tab">Flows</ui-tab>
      <ui-tab .selected="${s === 'assertions'}" aria-controls="assertions" id="assertions-tab">Assertions</ui-tab>
      <ui-tab .selected="${s === 'config'}" aria-controls="config" id="config-tab">Config</ui-tab>
      <ui-tab .selected="${s === 'code-snippets'}" aria-controls="code-snippets" id="code-snippets-tab"
        >Code snippets</ui-tab
      >
    </ui-tabs>`
  }

  /**
   * @returns The template for the current editor
   */
  protected renderCurrentEditor(): TemplateResult {
    const { selectedTab, isPayload } = this
    const parametersVisible = selectedTab === 'parameters'
    const headersVisible = selectedTab === 'headers'
    const bodyVisible = isPayload && selectedTab === 'payload'
    const authVisible = selectedTab === 'authorization'
    const codeVisible = selectedTab === 'code-snippets'
    const assertionsVisible = selectedTab === 'assertions'
    const configVisible = selectedTab === 'config'
    const flowsVisible = selectedTab === 'flows'
    return html`
      <div class="panel">
        ${this.renderParameters(parametersVisible)} ${this.renderHeaders(headersVisible)}
        ${this.renderBody(bodyVisible)} ${this.renderAuthorization(authVisible)} ${this.renderHttpFlows(flowsVisible)}
        ${this.renderAssertions(assertionsVisible)} ${this.renderRequestConfig(configVisible)}
        ${this.renderSnippets(codeVisible)}
      </div>
    `
  }

  protected renderParameters(visible: boolean): TemplateResult {
    const request = this.getRequest()
    const { eventsTarget, readOnly } = this
    return html`
      <http-url-params-form
        id="parameters"
        aria-describedby="parameters-tab"
        role="tabpanel"
        ?hidden="${!visible}"
        .value="${request.url || ''}"
        .eventsTarget="${eventsTarget}"
        ?readOnly="${readOnly}"
        @change="${this.handleParamsChange}"
      ></http-url-params-form>
    `
  }

  /**
   * @param visible Whether the panel should not be hidden
   * @returns The template for the headers editor
   */
  protected renderHeaders(visible: boolean): TemplateResult {
    const request = this.getRequest()
    const { eventsTarget, readOnly } = this
    return html` <http-headers-form
      id="headers"
      aria-describedby="headers-tab"
      role="tabpanel"
      .eventsTarget="${eventsTarget}"
      .model="${request.headersModel || []}"
      ?hidden="${!visible}"
      @change="${this.handleHeadersChange}"
      ?readonly="${readOnly}"
    ></http-headers-form>`
  }

  /**
   * @param visible Whether the panel should not be hidden
   * @returns The template for the body editor
   */
  protected renderBody(visible: boolean): TemplateResult {
    const { payload, readOnly, contentType, ui, readingPayload } = this
    if (readingPayload) {
      return html`<p class="loading-body" id="payload">Preparing the editor...</p>`
    }
    const body = ui && ui.body
    const selected = body && (body.selected as 'file' | 'raw' | 'urlEncode' | 'multipart')
    const model = body && body.model

    return html`
      <http-body-editor
        id="payload"
        aria-describedby="payload-tab"
        role="tabpanel"
        ?hidden="${!visible}"
        ?readOnly="${readOnly}"
        .contentType="${contentType}"
        .model="${model}"
        .value="${(!model && payload) || undefined}"
        selected="${ifDefined(selected)}"
        @change="${this.handleBody}"
        @select="${this.handleBody}"
      ></http-body-editor>
    `
  }

  /**
   * @param visible Whether the panel should not be hidden
   * @returns The template for the authorization editor
   */
  protected renderAuthorization(visible: boolean): TemplateResult | string {
    const { authorization } = this
    return html`
      <section ?hidden="${!visible}" id="authorization" aria-describedby="authorization-tab" role="tabpanel">
        ${this.renderAuthorizationActions(authorization)} ${this.renderAuthorizationList(authorization)}
      </section>
    `
  }

  protected renderAuthorizationActions(current: IRequestAuthorization[] = []): TemplateResult {
    const all = ['basic', 'oauth 2', 'open id', 'bearer', 'api key', 'ntlm', 'client certificate']
    const toRender: string[] = all.filter((type) => !current.some((i) => i.type === type))
    return html`
      <ui-dropdown-list verticalAlign="top" @select="${this.handleAuthAdd}" class="auth-dropdown" closeOnOutsideClick>
        <ui-button
          color="tonal"
          title="Add authorization option"
          aria-label="Opens a menu to select authorization option"
          ?disabled="${!toRender.length}"
        >
          <ui-icon icon="arrowDropDown" slot="icon" role="presentation"></ui-icon>
          Add
        </ui-button>
        <ui-list slot="dropdown" role="menu"> ${this.renderAuthorizationOptions(toRender)} </ui-list>
      </ui-dropdown-list>
    `
  }

  protected renderAuthorizationOptions(available: string[]): TemplateResult {
    return html`
      ${available.map(
        (type) => html` <ui-list-item role="menuitem" data-type="${type}"> ${authTypeToLabel(type)} </ui-list-item> `
      )}
    `
  }

  protected renderAuthorizationList(authorization: IRequestAuthorization[] = []): TemplateResult[] {
    if (!Array.isArray(authorization) || !authorization.length) {
      return [this.renderEmptyAuthorization()]
    }
    return authorization.map((item, index) => {
      let content: TemplateResult | undefined
      switch (item.type) {
        case 'basic':
          content = this.renderBasicAuthorization(item)
          break
        case 'bearer':
          content = this.renderBearerAuthorization(item)
          break
        case 'ntlm':
          content = this.renderNtlmAuthorization(item)
          break
        case 'oauth 2':
          content = this.renderOauthAuthorization(item)
          break
        case 'open id':
          content = this.renderOidcAuthorization(item)
          break
        case 'client certificate':
          content = this.renderCCAuthorization(item)
          break
        case 'api key':
          content = this.renderApiKeyAuthorization(item)
          break
        default:
      }
      if (content) {
        const isLast = authorization.length === index + 1
        return html` ${content} ${!isLast ? html`<ui-divider></ui-divider>` : ''} `
      }
      return html``
    })
  }

  protected renderEmptyAuthorization(): TemplateResult {
    return html``
  }

  protected renderBasicAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <basic-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
      ></basic-authorization>
    `
  }

  protected renderBearerAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <bearer-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
      ></bearer-authorization>
    `
  }

  protected renderNtlmAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <ntlm-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
      ></ntlm-authorization>
    `
  }

  protected renderOauthAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <oauth2-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
        .defaultRedirectUri="${this.oauth2RedirectUri}"
      ></oauth2-authorization>
    `
  }

  protected renderOidcAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <oidc-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
        .defaultRedirectUri="${this.oauth2RedirectUri}"
      ></oidc-authorization>
    `
  }

  protected renderCCAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <cc-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
      ></cc-authorization>
    `
  }

  protected renderApiKeyAuthorization(auth: IRequestAuthorization): TemplateResult {
    return html`
      <apikey-authorization
        .authorization="${auth}"
        ?readOnly="${this.readOnly}"
        class="auth-panel"
        canToggle
        canDelete
        @change="${this.handleAuthChange}"
        @delete="${this.handleAuthDelete}"
      ></apikey-authorization>
    `
  }

  protected renderHttpFlows(visible: boolean): TemplateResult | string {
    if (!visible) {
      return ''
    }
    const { flows = [] } = this
    return html`
      <http-flows
        aria-label="HTTP fLows"
        aria-labelledby="flows-tab"
        id="flows"
        role="tabpanel"
        .model="${flows}"
        @change="${this.handleFlows}"
      ></http-flows>
    `
  }

  protected renderAssertions(visible: boolean): TemplateResult | string {
    if (!visible) {
      return ''
    }
    const { assertions = [] } = this
    return html`
      <http-assertions
        aria-label="Assertions"
        aria-labelledby="assertions-tab"
        id="assertions"
        role="tabpanel"
        .model="${assertions}"
        @change="${this.handleAssertions}"
      ></http-assertions>
    `
  }

  protected renderRequestConfig(visible: boolean): TemplateResult | typeof nothing {
    if (!visible) {
      return nothing
    }
    const { config } = this
    return html`
      <http-request-config
        aria-label="Request options"
        aria-labelledby="config-tab"
        id="config"
        role="tabpanel"
        .config="${config}"
        @change="${this.handleConfig}"
      ></http-request-config>
    `
  }

  /**
   * @param visible Whether the panel should be rendered
   * @returns The template for the Code snippets
   */
  protected renderSnippets(visible: boolean): TemplateResult | string {
    return html` <http-snippets
      ?hidden="${!visible}"
      id="code-snippets"
      aria-describedby="code-snippets-tab"
      role="tabpanel"
      .request="${this.snippets}"
    ></http-snippets>`
  }

  /**
   * @returns The template for the invalid content headers dialog
   */
  protected renderHeadersDialog(): TemplateResult | string {
    const { contentHeadersDialogOpened } = this
    return html` <ui-dialog .open="${contentHeadersDialogOpened}" @close="${this.handleContentWarningClose}">
      <ui-icon slot="icon" icon="warning"></ui-icon>
      <span slot="title">Headers are not valid</span>

      <div>
        <p>
          The <b>GET</b> request should not contain <b>content-*</b> headers. It may cause the server to behave
          unexpectedly.
        </p>
        <p><b>Do you want to continue?</b></p>
      </div>

      <ui-button color="text" value="dismiss" slot="button">Cancel request</ui-button>
      <ui-button color="text" value="confirm" slot="button">Continue</ui-button>
    </ui-dialog>`
  }
}
