import { html, TemplateResult } from 'lit'
import { ClassInfo, classMap } from 'lit/directives/class-map.js'
import { styleMap } from 'lit/directives/style-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { property } from 'lit/decorators.js'
import type { Har, Page, Entry, Request, Response, Header, QueryString, Timings } from 'har-format'
import { DataCalculator } from '@api-client/core/lib/calculators/DataCalculator.js'
import ApiElement from '../ApiElement.js'
import { UiListSelection } from '../../md/list/internals/List.js'
import UiTabs, { TabSelectionDetail } from '../../md/tabs/internals/Tabs.js'
import { SortableEntry, sortEntires } from './HarUtils.js'

import '../../md/tabs/ui-tab.js'
import '../../md/tabs/ui-tabs.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/divider/ui-divider.js'

export enum HarTab {
  request = 'request',
  response = 'response',
  requestContent = 'request-content',
  responseContent = 'response-content',
  cookies = 'cookies',
}

export interface RenderedPage {
  page: Page
  entries: RenderedEntry[]
  totalTime: number
}

export interface RenderedEntry extends SortableEntry {
  requestTime: string
  visualTimings?: RenderedEntryTimings
  requestFormattedDate: string

  requestSizes: EntrySizing
  responseSizes: EntrySizing
}

export interface RenderedEntryTimings {
  total: number
  totalValue: number
  delay?: number
  blocked?: number
  connect?: number
  dns?: number
  ssl?: number
  send?: number
  receive?: number
  wait?: number
}

export interface EntrySizing {
  headers: string
  headersComputed: boolean
  body: string
  bodyComputed: boolean
  sum: string
  sumComputed: boolean
}

/** used when generating keys for entires */
let nextId = 0

/**
 * An element that renders the UI for HAR data.
 */
export default class HarViewer extends ApiElement {
  protected harInternal?: Har

  protected ignorePagesInternal?: boolean

  /**
   * The HAR object to render.
   */
  get har(): Har | undefined {
    return this.harInternal
  }

  @property({ type: Object })
  set har(value: Har | undefined) {
    const old = this.harInternal
    if (old === value) {
      return
    }
    this.harInternal = value
    this.processHar()
  }

  /**
   * When set it ignores pages matching and renders all requests in a single table.
   */
  get ignorePages(): boolean | undefined {
    return this.ignorePagesInternal
  }

  @property({ type: Boolean })
  set ignorePages(value: boolean | undefined) {
    const old = this.ignorePagesInternal
    if (old === value) {
      return
    }
    this.ignorePagesInternal = value
    this.processHar()
  }

  entries?: RenderedEntry[]

  pages?: RenderedPage[]

  openedPages: string[] = []

  openedEntries: string[] = []

  protected selectedTabs = new Map<string, HarTab>()

  /**
   * Called when the `har` or `ignorePages` changed.
   */
  processHar(): void {
    this.entries = undefined
    this.pages = undefined
    const { har, ignorePages } = this
    if (!har || !har.log) {
      this.requestUpdate()
      return
    }
    const { log } = har
    const { pages, entries } = log
    if (!entries || !entries.length) {
      this.requestUpdate()
      return
    }
    const items = sortEntires(entries)
    if (ignorePages || !pages || !pages.length) {
      this.computeEntriesOnly(items)
    } else {
      this.computePages(pages, items)
    }
  }

  /**
   * Performs computations to render entries only.
   * @param entries The list of entries to process.
   */
  computeEntriesOnly(entries: SortableEntry[]): void {
    const totalTime = this.computeTotalTime(entries[0], entries[entries.length - 1])
    this.entries = this.computeRenderedEntries(entries, totalTime)
    this.openedEntries = []
    this.requestUpdate()
  }

  /**
   * Performs computations to render entries by page.
   * @param pages The list of pages to process.
   * @param entries The list of entries to process.
   */
  computePages(pages: Page[], entries: SortableEntry[]): void {
    const result: RenderedPage[] = []
    const opened: string[] = []
    pages.forEach((page) => {
      opened.push(page.id)
      const items = entries.filter((entry) => entry.pageref === page.id)
      const totalTime = this.computeTotalTime(items[0], items[items.length - 1])
      const item: RenderedPage = {
        page,
        entries: this.computeRenderedEntries(items, totalTime),
        totalTime,
      }
      result.push(item)
    })
    this.pages = result
    this.openedPages = opened
    this.openedEntries = []
    this.requestUpdate()
  }

  /**
   * @param entries The entries to perform computations on.
   * @param totalTime The total time of all entries rendered in the group
   */
  computeRenderedEntries(entries: SortableEntry[], totalTime: number): RenderedEntry[] {
    const result: RenderedEntry[] = []
    if (!Array.isArray(entries) || !entries.length) {
      return result
    }
    // This expects entires to be sorted by time (as required by the spec).
    const [startEntry] = entries
    const startTime = startEntry.timestamp
    entries.forEach((entry) => {
      const d = new Date(entry.timestamp)
      const visualTimings = this.computeVisualTimes(entry.timings, entry.timestamp - startTime, totalTime)
      const numType: 'numeric' | '2-digit' = 'numeric'
      const options: Intl.DateTimeFormatOptions = {
        hour: numType,
        minute: numType,
        second: numType,
        fractionalSecondDigits: 3,
      }
      const format = new Intl.DateTimeFormat(undefined, options)
      const requestTime = format.format(d)
      const format2 = new Intl.DateTimeFormat(undefined, {
        timeStyle: 'medium',
        dateStyle: 'medium',
      })
      const requestFormattedDate = format2.format(d)
      const requestSizes = this.computeEntrySizeInfo(entry.request)
      const responseSizes = this.computeEntrySizeInfo(entry.response)
      const id = String(nextId++)
      const item: RenderedEntry = {
        requestTime,
        requestFormattedDate,
        requestSizes,
        responseSizes,
        ...entry,
        id,
      }
      if (visualTimings) {
        item.visualTimings = visualTimings
      }
      result.push(item)
    })
    return result
  }

  protected computeEntrySizeInfo(info: Request | Response): EntrySizing {
    const result: EntrySizing = {
      headersComputed: false,
      bodyComputed: false,
      body: '',
      headers: '',
      sum: '',
      sumComputed: false,
    }

    let { headersSize = 0, bodySize = 0 } = info
    const { headers } = info
    if (headersSize < 1) {
      const parts: string[] = []
      ;(headers || []).forEach((header) => {
        const key = header.name
        if (key && key.trim() !== '') {
          let value
          if (Array.isArray(header.value)) {
            value = header.value.join(',')
          } else {
            value = header.value
          }
          let line = ''
          line += `${key}: `
          if (typeof value !== 'undefined') {
            value = value.split('\n').join(' ')
            line += value
          }
          parts.push(line)
        }
      })
      const hdrStr = parts.join('\n')
      headersSize = DataCalculator.stringSize(hdrStr)
      result.headersComputed = true
    }
    if (bodySize < 1) {
      const typedRequest = info as Request
      const typedResponse = info as Response
      if (typedResponse.content) {
        if (typedResponse.content.size) {
          bodySize = typedResponse.content.size
        } else if (typedResponse.content.text) {
          bodySize = DataCalculator.stringSize(typedResponse.content.text)
          result.bodyComputed = true
        }
      } else if (typedRequest.postData && typedRequest.postData.text) {
        bodySize = DataCalculator.stringSize(typedRequest.postData.text)
        result.bodyComputed = true
      }
    }
    if (bodySize < 0) {
      bodySize = 0
    }
    result.body = DataCalculator.bytesToSize(bodySize)
    result.headers = DataCalculator.bytesToSize(headersSize)
    result.sum = DataCalculator.bytesToSize(headersSize + bodySize)
    result.sumComputed = result.bodyComputed || result.headersComputed
    return result
  }

  /**
   * @param code The status code to test for classes.
   * @returns List of classes to be set on the status code
   */
  computeStatusClasses(code: number): ClassInfo {
    const classes = {
      'status-code': true,
      'error': code >= 500 || code === 0,
      'warning': code >= 400 && code < 500,
      'info': code >= 300 && code < 400,
    }
    return classes
  }

  /**
   * Computes the total time of page requests.
   * @param first The earliest entry in the range
   * @param last The latest entry in the range
   * @returns The total time of the page. Used to build the timeline.
   */
  computeTotalTime(first: Entry, last: Entry): number {
    if (first === last) {
      return this.sumTimings(last.timings)
    }
    const startTime = new Date(first.startedDateTime).getTime()
    const endTime = new Date(last.startedDateTime).getTime()
    const lastDuration = this.sumTimings(last.timings)
    return endTime - startTime + lastDuration
  }

  /**
   * @param timings The entry's timings object.
   * @param delay The timestamp when the first request started.
   * @param total The number of milliseconds all entries took.
   */
  computeVisualTimes(timings: Timings, delay: number, total: number): RenderedEntryTimings | undefined {
    if (!timings) {
      return undefined
    }
    const timingsSum = this.sumTimings(timings)
    const totalPercent = (timingsSum / total) * 100
    const result: RenderedEntryTimings = {
      total: totalPercent,
      totalValue: timingsSum,
    }
    if (delay) {
      result.delay = (delay / total) * 100
    }
    if (typeof timings.blocked === 'number' && timings.blocked > 0) {
      result.blocked = (timings.blocked / timingsSum) * 100
    }
    if (typeof timings.connect === 'number' && timings.connect > 0) {
      result.connect = (timings.connect / timingsSum) * 100
    }
    if (typeof timings.dns === 'number' && timings.dns > 0) {
      result.dns = (timings.dns / timingsSum) * 100
    }
    if (typeof timings.receive === 'number' && timings.receive > 0) {
      result.receive = (timings.receive / timingsSum) * 100
    }
    if (typeof timings.send === 'number' && timings.send > 0) {
      result.send = (timings.send / timingsSum) * 100
    }
    if (typeof timings.ssl === 'number' && timings.ssl > 0) {
      result.ssl = (timings.ssl / timingsSum) * 100
    }
    if (typeof timings.wait === 'number' && timings.wait > 0) {
      result.wait = (timings.wait / timingsSum) * 100
    }
    return result
  }

  /**
   * Sums all timing values.
   * @param timings The timings object to compute
   * @returns The total time, excluding -1s
   */
  protected sumTimings(timings: Timings): number {
    let result = 0
    if (!timings) {
      return result
    }
    if (typeof timings.blocked === 'number' && timings.blocked > 0) {
      result += timings.blocked
    }
    if (typeof timings.connect === 'number' && timings.connect > 0) {
      result += timings.connect
    }
    if (typeof timings.dns === 'number' && timings.dns > 0) {
      result += timings.dns
    }
    if (typeof timings.receive === 'number' && timings.receive > 0) {
      result += timings.receive
    }
    if (typeof timings.send === 'number' && timings.send > 0) {
      result += timings.send
    }
    if (typeof timings.ssl === 'number' && timings.ssl > 0) {
      result += timings.ssl
    }
    if (typeof timings.wait === 'number' && timings.wait > 0) {
      result += timings.wait
    }
    return result
  }

  /**
   * A handler for the page label click to toggle the page entries.
   */
  protected pageClickHandler(e: Event): void {
    const node = e.currentTarget as HTMLElement
    const id = node.dataset.page
    if (id) {
      this.togglePage(id)
    }
  }

  /**
   * A handler for the page label keydown to toggle the page entries on space key.
   */
  protected pageKeydownHandler(e: KeyboardEvent): void {
    if (e.code !== 'Space') {
      return
    }
    const node = e.target as HTMLElement
    const id = node.dataset.page
    if (id) {
      this.togglePage(id)
    }
  }

  /**
   * Toggles the visibility of the page entries.
   * @param id The id of the page.
   */
  protected togglePage(id: string): void {
    const allOpened = this.openedPages
    const index = allOpened.indexOf(id)
    if (index === -1) {
      allOpened.push(id)
    } else {
      allOpened.splice(index, 1)
    }
    this.requestUpdate()
  }

  /**
   * Handler for the list item selection event.
   */
  protected handleEntrySelection(e: CustomEvent<UiListSelection>): void {
    const { item } = e.detail
    const { entry } = item.dataset
    if (!entry) {
      return
    }
    const { openedEntries = [] } = this
    if (openedEntries.includes(entry)) {
      const index = openedEntries.indexOf(entry)
      openedEntries.splice(index, 1)
    } else {
      openedEntries.push(entry)
    }
    this.openedEntries = openedEntries
    this.requestUpdate()
  }

  /**
   * Handler for the list item selection event.
   */
  protected handleDetailsTabSelection(e: CustomEvent<TabSelectionDetail>): void {
    const { selected } = e.detail.item.dataset
    const { entry } = (e.target as UiTabs).dataset
    if (!selected || !entry) {
      return
    }
    this.selectedTabs.set(entry, selected as HarTab)
    this.requestUpdate()
  }

  override render(): TemplateResult {
    const { pages } = this
    if (Array.isArray(pages) && pages.length) {
      return this.renderPages(pages)
    }
    const { entries } = this
    if (Array.isArray(entries) && entries.length) {
      return this.renderEntries(entries)
    }
    return html``
  }

  /**
   * @returns Template for the pages table
   */
  protected renderPages(pages: RenderedPage[]): TemplateResult {
    return html` <div class="pages">${pages.map((info) => this.renderPage(info))}</div> `
  }

  /**
   * @returns Template for a single page
   */
  protected renderPage(info: RenderedPage): TemplateResult {
    const allOpened = this.openedPages
    const opened = allOpened.includes(info.page.id)
    return html`
      <section class="page">
        ${this.renderPageHeader(info.page, info.totalTime)}
        <div class="page-content" ?hidden="${!opened}">
          <div class="page-entries">${this.renderEntriesList(info.entries)}</div>
        </div>
      </section>
    `
  }

  /**
   * @returns Template for the pages table
   */
  protected renderPageHeader(page: Page, totalTime: number): TemplateResult {
    return html`
      <div
        class="page-header"
        @click="${this.pageClickHandler}"
        @keydown="${this.pageKeydownHandler}"
        tabindex="0"
        data-page="${page.id}"
      >
        <span class="label">${page.title || 'Unknown page'}</span>
        ${this.renderLoadingTime(totalTime)}
      </div>
    `
  }

  /**
   * @returns Template for the entries table
   */
  protected renderEntries(entries: RenderedEntry[]): TemplateResult {
    return html` <section class="entries-list">${this.renderEntriesList(entries)}</section> `
  }

  /**
   * @returns The template for the entries list
   */
  protected renderEntriesList(entries: RenderedEntry[]): TemplateResult {
    return html`
      <ui-list @select="${this.handleEntrySelection}" aria-label="Select a list item to see details" role="list">
        ${entries.map((item) => this.renderEntry(item))}
      </ui-list>
    `
  }

  /**
   * @returns The template for a single entry
   */
  protected renderEntry(entry: RenderedEntry): TemplateResult {
    const { request, response, timings, visualTimings, id, responseSizes } = entry
    const allSelected = this.openedEntries
    const selected = allSelected.includes(`${id}`)
    return html`
    <ui-list-item class="entry-item" data-entry="${id}">
      <div class="time">
        ${entry.requestTime}
      </div>
      <div class="entry-body">
        <div class="entry-detail-line">
          ${this.renderStatusLabel(response.status, response.statusText)}
          ${this.renderLoadingTime(entry.time)}
          ${this.renderResponseSize(responseSizes)}
        </div>
        <div class="entry-location" title="${request.url}">${request.method} ${request.url}</div>
      </div>
      <div class="entry-timings">
        ${this.timingsTemplate(timings, visualTimings, selected)}</div>
      </div>
    </ui-list-item>
    ${selected ? this.entryDetails(entry) : ''}
    <ui-divider type="inset"></ui-divider>
    `
  }

  /**
   * @param status The response status code
   * @param statusText The response reason part of the status.
   * @returns The template for the status message
   */
  protected renderStatusLabel(status: number, statusText = ''): TemplateResult {
    const codeClasses = this.computeStatusClasses(status)
    return html`
      <span class="${classMap(codeClasses)}">${status}</span>
      <span class="message">${statusText}</span>
    `
  }

  /**
   * @param value The response loading time
   * @returns Template for the loading time message
   */
  protected renderLoadingTime(value: number): TemplateResult | string {
    if (Number.isNaN(value)) {
      return ''
    }
    const roundedValue = Math.round(value || 0)
    return html`<span class="loading-time-label">Time: ${roundedValue} ms</span>`
  }

  /**
   * @returns Template for the response size
   */
  protected renderResponseSize(sizing: EntrySizing): TemplateResult | string {
    return html`<span class="response-size-label">Size: ${sizing.sum}</span>`
  }

  /**
   * @param timings The entry's timings
   * @param visualTimings The computed visual timings for the template
   * @param fullWidth When set then it renders the timeline in the whole available space.
   * @returns The template for the timings timeline
   */
  protected timingsTemplate(
    timings: Timings,
    visualTimings?: RenderedEntryTimings,
    fullWidth = false
  ): TemplateResult | string {
    if (!visualTimings) {
      return ''
    }
    const { total, delay, blocked, connect, dns, receive, send, ssl, wait } = visualTimings
    const styles = {
      width: fullWidth ? '100%' : `${total}%`,
    }
    return html`
      ${fullWidth ? '' : this.timingTemplate(delay, 'delay')}
      <div class="entry-timings-value" style="${styleMap(styles)}">
        ${this.timingTemplate(blocked, 'blocked', 'Blocked', timings)}
        ${this.timingTemplate(dns, 'dns', 'DNS', timings)}
        ${this.timingTemplate(connect, 'connect', 'Connecting', timings)}
        ${this.timingTemplate(ssl, 'ssl', 'SSL negotiation', timings)}
        ${this.timingTemplate(send, 'send', 'Sending', timings)}
        ${this.timingTemplate(wait, 'wait', 'Waiting', timings)}
        ${this.timingTemplate(receive, 'receive', 'Receiving', timings)}
      </div>
    `
  }

  /**
   * @param width
   * @param type Added to the class name.
   * @param label The label to use in the title attribute
   * @param timings The entry's timings object
   * @returns The template for a timing timeline item
   */
  protected timingTemplate(
    width: number | undefined,
    type: keyof RenderedEntryTimings | keyof Timings,
    label?: string,
    timings?: Timings
  ): TemplateResult | string {
    if (!width) {
      return ''
    }
    const styles = {
      width: `${width}%`,
    }
    const classes = {
      'timing-entry': true,
      [type]: true,
    }
    const time = timings && timings[type as keyof Timings]
    const title = typeof time === 'number' ? `${label}: ${Math.round(time)}ms` : undefined
    return html` <div class="${classMap(classes)}" style="${styleMap(styles)}" title="${ifDefined(title)}"></div> `
  }

  /**
   * @param entry The entry to render
   * @returns The template for an entry details.
   */
  protected entryDetails(entry: RenderedEntry): TemplateResult {
    const { id } = entry
    const selectedTab = this.selectedTabs.get(String(id)) || HarTab.request
    return html`
      <section class="entry-details">
        ${this.entryDetailsTabsTemplate(entry, selectedTab)}
        <div class="details-content" tabindex="0">${this.entryDetailsContentTemplate(entry, selectedTab)}</div>
      </section>
    `
  }

  /**
   * @param entry The entry to render
   * @param selected The index of the selected tab
   * @returns The template for entry details content tabs.
   */
  protected entryDetailsTabsTemplate(entry: RenderedEntry, selected: string): TemplateResult {
    const { id, request, response } = entry
    const { postData, cookies: requestCookies } = request
    const { content, cookies: responseCookies } = response
    const hashRequestContent = !!postData && !!postData.text
    const hashResponseContent = !!content && !!content.text
    const hasRequestCookies = Array.isArray(requestCookies) && !!requestCookies.length
    const hasResponseCookies = Array.isArray(responseCookies) && !!responseCookies.length
    const hasCookies = hasRequestCookies || hasResponseCookies
    return html`
      <ui-tabs priority="secondary" @change="${this.handleDetailsTabSelection}" data-entry="${id}">
        <ui-tab .selected="${selected === HarTab.request}" data-selected="${HarTab.request}">Request</ui-tab>
        <ui-tab .selected="${selected === HarTab.response}" data-selected="${HarTab.response}">Response</ui-tab>
        <ui-tab .selected="${selected === HarTab.requestContent}" ?hidden="${!hashRequestContent}"
          >Request content</ui-tab
        >
        <ui-tab .selected="${selected === HarTab.responseContent}" ?hidden="${!hashResponseContent}"
          >Response content</ui-tab
        >
        <ui-tab .selected="${selected === HarTab.cookies}" ?hidden="${!hasCookies}">Cookies</ui-tab>
      </ui-tabs>
    `
  }

  /**
   * @param entry The entry to render
   * @param selected The index of the selected tab
   * @returns The template for entry details content.
   */
  protected entryDetailsContentTemplate(entry: RenderedEntry, selected?: HarTab): TemplateResult | string {
    switch (selected) {
      case HarTab.request:
        return this.entryDetailsRequestTemplate(entry)
      case HarTab.response:
        return this.entryDetailsResponseTemplate(entry)
      case HarTab.requestContent:
        return this.entryDetailsRequestBodyTemplate(entry)
      case HarTab.responseContent:
        return this.entryDetailsResponseBodyTemplate(entry)
      case HarTab.cookies:
        return this.entryDetailsCookiesTemplate(entry)
      default:
        return ''
    }
  }

  /**
   * @param entry The entry to render
   * @returns The template for entry's request content.
   */
  protected entryDetailsRequestTemplate(entry: RenderedEntry): TemplateResult {
    const { request, requestFormattedDate, serverIPAddress, requestSizes } = entry
    const { headers, url, method, httpVersion, queryString } = request
    return html`
      <div class="entry-details-title">Request on ${requestFormattedDate}</div>
      <dl class="details-list">
        <dt>General</dt>
        <dd>
          ${this.definitionTemplate('URL', url)} ${this.definitionTemplate('HTTP version', httpVersion)}
          ${this.definitionTemplate('Operation', method)} ${this.definitionTemplate('Remote Address', serverIPAddress)}
        </dd>
        ${this.headersTemplate(headers)} ${this.queryParamsTemplate(queryString)} ${this.sizesTemplate(requestSizes)}
      </dl>
    `
  }

  /**
   * @param entry The entry to render
   * @returns The template for entry's response content.
   */
  protected entryDetailsResponseTemplate(entry: RenderedEntry): TemplateResult {
    const { response, responseSizes } = entry
    const { headers } = response
    return html` <dl class="details-list">${this.headersTemplate(headers)} ${this.sizesTemplate(responseSizes)}</dl> `
  }

  /**
   * @param entry The entry to render
   * @returns The template for entry's request body preview.
   */
  protected entryDetailsRequestBodyTemplate(entry: RenderedEntry): TemplateResult {
    const { request } = entry
    const { postData } = request
    if (!postData || !postData.text) {
      return html`<p>No request body data.</p>`
    }
    return html` <pre><code class="body-preview">${postData.text}</code></pre> `
  }

  /**
   * @param entry The entry to render
   * @returns The template for entry's response body preview.
   */
  protected entryDetailsResponseBodyTemplate(entry: RenderedEntry): TemplateResult {
    const { response } = entry
    const { content } = response
    if (!content || !content.text) {
      return html`<p>No request body data.</p>`
    }
    return html` <pre><code class="body-preview">${content.text}</code></pre> `
  }

  /**
   * @param entry The entry to render
   * @returns The template for entry's cookies.
   */
  protected entryDetailsCookiesTemplate(entry: RenderedEntry): TemplateResult {
    const { request, response } = entry
    const { cookies: requestCookies } = request
    const { cookies: responseCookies } = response
    const hasRequestCookies = Array.isArray(requestCookies) && !!requestCookies.length
    const hasResponseCookies = Array.isArray(responseCookies) && !!responseCookies.length

    return html`
      <dl class="details-list">
        <dt>Request cookies</dt>
        <dd>
          ${hasRequestCookies
            ? requestCookies.map((item) => this.definitionTemplate(item.name, item.value))
            : html`No cookies recorded.`}
        </dd>

        <dt>Response cookies</dt>
        <dd>
          ${hasResponseCookies
            ? responseCookies.map((item) => this.definitionTemplate(item.name, item.value))
            : html`No cookies recorded.`}
        </dd>
      </dl>
    `
  }

  /**
   * @param term Definition label
   * @param value Definition value
   * @returns The template for the definition.
   */
  protected definitionTemplate(term: string, value?: string): TemplateResult | string {
    if (!value) {
      return ''
    }
    return html` <p class="definition"><dfn>${term}:</dfn> ${value}</p> `
  }

  /**
   * @returns The template for the list of headers.
   */
  protected headersTemplate(headers: Header[]): TemplateResult | string {
    return html`
      <dt>Headers</dt>
      <dd>
        ${Array.isArray(headers) && headers.length
          ? headers.map((item) => this.definitionTemplate(item.name, item.value))
          : html`No headers recorded.`}
      </dd>
    `
  }

  /**
   * @returns The template for the query parameters.
   */
  protected queryParamsTemplate(params: QueryString[]): TemplateResult | string {
    if (!Array.isArray(params) || !params.length) {
      return ''
    }
    return html`
      <dt>Query parameters</dt>
      <dd>${params.map((item) => this.definitionTemplate(item.name, item.value))}</dd>
    `
  }

  /**
   * @returns The template for sizes information
   */
  protected sizesTemplate(sizes: EntrySizing): TemplateResult {
    return html` <dt>Size</dt>
      <dd>
        ${this.definitionTemplate('Headers', sizes.headers)} ${this.definitionTemplate('Body', sizes.body)}
        ${this.definitionTemplate('Total', sizes.sum)}
      </dd>`
  }
}
