// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../../ui/kit/kit.js';
import '../../ui/components/expandable_list/expandable_list.js';
import '../../ui/components/report_view/report_view.js';

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import type * as StackTrace from '../../models/stack_trace/stack_trace.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as PanelCommon from '../../panels/common/common.js';
import * as NetworkForward from '../../panels/network/forward/forward.js';
import * as CspEvaluator from '../../third_party/csp_evaluator/csp_evaluator.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import type * as ExpandableList from '../../ui/components/expandable_list/expandable_list.js';
import type * as ReportView from '../../ui/components/report_view/report_view.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import * as ApplicationComponents from './components/components.js';
import frameDetailsReportViewStyles from './frameDetailsReportView.css.js';
import {OriginTrialTreeView} from './OriginTrialTreeView.js';

const {widget} = UI.Widget;

const UIStrings = {
  /**
   * @description Section header in the Frame Details view
   */
  additionalInformation: 'Additional Information',
  /**
   * @description Explanation for why the additional information section is being shown
   */
  thisAdditionalDebugging:
      'This additional (debugging) information is shown because the \'Protocol Monitor\' experiment is enabled.',
  /**
   * @description Label for subtitle of frame details view
   */
  frameId: 'Frame ID',
  /**
   * @description Name of a network resource type
   */
  document: 'Document',
  /**
   * @description A web URL (for a lot of languages this does not need to be translated, please translate only where necessary)
   */
  url: 'URL',
  /**
   * /**
   * @description Title for a link to the Sources panel
   */
  clickToOpenInSourcesPanel: 'Click to open in Sources panel',
  /**
   * @description Title for a link to the Network panel
   */
  clickToOpenInNetworkPanel: 'Click to open in Network panel',
  /**
   * @description Title for unreachable URL field
   */
  unreachableUrl: 'Unreachable URL',
  /**
   * @description Title for a link that applies a filter to the network panel
   */
  clickToOpenInNetworkPanelMight: 'Click to open in Network panel (might require page reload)',
  /**
   * @description The origin of a URL (https://web.dev/same-site-same-origin/#origin)
   *(for a lot of languages this does not need to be translated, please translate only where necessary)
   */
  origin: 'Origin',
  /**
   * /**
   * @description Related node label in Timeline UIUtils of the Performance panel
   */
  ownerElement: 'Owner Element',
  /**
   * @description Title for ad frame type field
   */
  adStatus: 'Ad Status',
  /**
   * @description Description for ad frame type
   */
  rootDescription: 'This frame has been identified as the root frame of an ad',
  /**
   * @description Value for ad frame type
   */
  root: 'root',
  /**
   * @description Description for ad frame type
   */
  childDescription: 'This frame has been identified as a child frame of an ad',
  /**
   * @description Value for ad frame type
   */
  child: 'child',
  /**
   * @description Section header in the Frame Details view
   */
  securityIsolation: 'Security & Isolation',
  /**
   * @description Section header in the Frame Details view
   */
  contentSecurityPolicy: 'Content Security Policy (CSP)',
  /**
   * @description Row title for in the Frame Details view
   */
  secureContext: 'Secure Context',
  /**
   * @description Text in Timeline indicating that input has happened recently
   */
  yes: 'Yes',
  /**
   * @description Text in Timeline indicating that input has not happened recently
   */
  no: 'No',
  /**
   * @description Label for whether a frame is cross-origin isolated
   *(https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/)
   *(for a lot of languages this does not need to be translated, please translate only where necessary)
   */
  crossoriginIsolated: 'Cross-Origin Isolated',
  /**
   * @description Explanatory text in the Frame Details view
   */
  localhostIsAlwaysASecureContext: '`Localhost` is always a secure context',
  /**
   * @description Explanatory text in the Frame Details view
   */
  aFrameAncestorIsAnInsecure: 'A frame ancestor is an insecure context',
  /**
   * @description Explanatory text in the Frame Details view
   */
  theFramesSchemeIsInsecure: 'The frame\'s scheme is insecure',
  /**
   * @description This label specifies the server endpoints to which the server is reporting errors
   *and warnings through the Report-to API. Following this label will be the URL of the server.
   */
  reportingTo: 'reporting to',
  /**
   * @description Section header in the Frame Details view
   */
  apiAvailability: 'API availability',
  /**
   * @description Explanation of why cross-origin isolation is important
   *(https://web.dev/why-coop-coep/)
   *(for a lot of languages 'cross-origin isolation' does not need to be translated, please translate only where necessary)
   */
  availabilityOfCertainApisDepends: 'Availability of certain APIs depends on the document being cross-origin isolated.',
  /**
   * @description Description of the SharedArrayBuffer status
   */
  availableTransferable: 'available, transferable',
  /**
   * @description Description of the SharedArrayBuffer status
   */
  availableNotTransferable: 'available, not transferable',
  /**
   * @description Explanation for the SharedArrayBuffer availability status
   */
  unavailable: 'unavailable',
  /**
   * @description Tooltip for the SharedArrayBuffer availability status
   */
  sharedarraybufferConstructorIs:
      '`SharedArrayBuffer` constructor is available and `SABs` can be transferred via `postMessage`',
  /**
   * @description Tooltip for the SharedArrayBuffer availability status
   */
  sharedarraybufferConstructorIsAvailable:
      '`SharedArrayBuffer` constructor is available but `SABs` cannot be transferred via `postMessage`',
  /**
   * @description Explanation why SharedArrayBuffer will not be available in the future
   *(https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/)
   *(for a lot of languages 'cross-origin isolation' does not need to be translated, please translate only where necessary)
   */
  willRequireCrossoriginIsolated: '⚠️ will require cross-origin isolated context in the future',
  /**
   * @description Explanation why SharedArrayBuffer is not available
   *(https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/)
   *(for a lot of languages 'cross-origin isolation' does not need to be translated, please translate only where necessary).
   */
  requiresCrossoriginIsolated: 'requires cross-origin isolated context',
  /**
   * @description Explanation for the SharedArrayBuffer availability status in case the transfer of a SAB requires the
   * permission policy `cross-origin-isolated` to be enabled (e.g. because the message refers to the situation in an iframe).
   */
  transferRequiresCrossoriginIsolatedPermission:
      '`SharedArrayBuffer` transfer requires enabling the permission policy:',
  /**
   * @description Explanation for the Measure Memory availability status
   */
  available: 'available',
  /**
   * @description Tooltip for the Measure Memory availability status
   */
  thePerformanceAPI: 'The `performance.measureUserAgentSpecificMemory()` API is available',
  /**
   * @description Tooltip for the Measure Memory availability status
   */
  thePerformancemeasureuseragentspecificmemory:
      'The `performance.measureUserAgentSpecificMemory()` API is not available',
  /**
   * @description Entry in the API availability section of the frame details view
   */
  measureMemory: 'Measure Memory',
  /**
   * @description Text that is usually a hyperlink to more documentation
   */
  learnMore: 'Learn more',
  /**
   * @description Label for a stack trace. If a frame is created programmatically (i.e. via JavaScript), there is a
   * stack trace for the line of code which caused the creation of the iframe. This is the stack trace we are showing here.
   */
  creationStackTrace: 'Frame Creation `Stack Trace`',
  /**
   * @description Tooltip for 'Frame Creation Stack Trace' explaining that the stack
   *trace shows where in the code the frame has been created programmatically
   */
  creationStackTraceExplanation:
      'This frame was created programmatically. The `stack trace` shows where this happened.',
  /**
   * @description Text descripting why a frame has been indentified as an advertisement.
   */
  parentIsAdExplanation: 'This frame is considered an ad frame because its parent frame is an ad frame.',
  /**
   * @description Text descripting why a frame has been indentified as an advertisement.
   */
  matchedBlockingRuleExplanation:
      'This frame is considered an ad frame because its current (or previous) main document is an ad resource.',
  /**
   * @description Text descripting why a frame has been indentified as an advertisement.
   */
  createdByAdScriptExplanation:
      'There was an ad script in the `(async) stack` when this frame was created. Examining the creation `stack trace` of this frame might provide more insight.',
  /**
   * @description Label for the link(s) to the ad script(s) that led to this frame's creation.
   */
  creatorAdScriptAncestry: 'Creator Ad Script Ancestry',
  /**
   * @description Label for the filterlist rule that identified the root script in 'Creator Ad Script Ancestry' as an ad.
   */
  rootScriptFilterlistRule: 'Root Script Filterlist Rule',
  /**
   * @description Text describing the absence of a value.
   */
  none: 'None',
  /**
   * @description Explanation of what origin trials are
   *(https://developer.chrome.com/docs/web-platform/origin-trials/)
   *(please don't translate 'origin trials').
   */
  originTrialsExplanation: 'Origin trials give you access to a new or experimental feature.',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/FrameDetailsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export interface FrameDetailsReportViewData {
  frame: SDK.ResourceTreeModel.ResourceTreeFrame;
  target?: SDK.Target.Target;
  adScriptAncestry: Protocol.Network.AdAncestry|null;
}

export interface FrameDetailsViewInput {
  frame: SDK.ResourceTreeModel.ResourceTreeFrame;
  target: SDK.Target.Target|null;
  creationStackTrace: StackTrace.StackTrace.StackTrace|null;
  adScriptAncestry: Protocol.Network.AdAncestry|null;
  linkTargetDOMNode: SDK.DOMModel.DOMNode|null;
  permissionsPolicies: Protocol.Page.PermissionsPolicyFeatureState[]|null;
  protocolMonitorExperimentEnabled: boolean;
  trials: Protocol.Page.OriginTrial[]|null;
  securityIsolationInfo: Protocol.Network.SecurityIsolationStatus|null;
  onRevealInNetwork?: () => void;
  onRevealInSources: () => void;
}

export type View = (input: FrameDetailsViewInput, output: undefined, target: HTMLElement) => void;

export const DEFAULT_VIEW: View = (input, _output, target) => {
  if (!input.frame) {
    return;
  }
  // clang-format off
  render(html`
    <style>${frameDetailsReportViewStyles}</style>
    <devtools-report .data=${{reportTitle: input.frame.displayName()} as ReportView.ReportView.ReportData}
    jslog=${VisualLogging.pane('frames')}>
      ${renderDocumentSection(input)}
      ${renderIsolationSection(input)}
      ${renderApiAvailabilitySection(input.frame)}
      ${renderOriginTrial(input.trials)}
      ${input.permissionsPolicies ?
          widget(ApplicationComponents.PermissionsPolicySection.PermissionsPolicySection, {
             policies: input.permissionsPolicies,
             showDetails: false}) : nothing}
      ${input.protocolMonitorExperimentEnabled ? renderAdditionalInfoSection(input.frame) : nothing}
    </devtools-report>
  `, target);
  // clang-format on
};

function renderOriginTrial(trials: Protocol.Page.OriginTrial[]|null): LitTemplate {
  if (!trials) {
    return nothing;
  }

  const data = {trials};
  // clang-format off
  return html`
    <devtools-report-section-header>
      ${i18n.i18n.lockedString('Origin trials')}
    </devtools-report-section-header>
    <devtools-report-section>
      <span class="report-section">
        ${i18nString(UIStrings.originTrialsExplanation)}
        <devtools-link href="https://developer.chrome.com/docs/web-platform/origin-trials/" class="link"
                jslogcontext="learn-more.origin-trials">
          ${i18nString(UIStrings.learnMore)}
        </devtools-link>
      </span>
    </devtools-report-section>
    <devtools-widget class="span-cols" ${widget(OriginTrialTreeView, {data})}>
    </devtools-widget>
    <devtools-report-divider></devtools-report-divider>`;
  // clang-format on
}

function renderDocumentSection(input: FrameDetailsViewInput): LitTemplate {
  if (!input.frame) {
    return nothing;
  }

  // clang-format off
  return html`
      <devtools-report-section-header>${i18nString(UIStrings.document)}</devtools-report-section-header>
      <devtools-report-key>${i18nString(UIStrings.url)}</devtools-report-key>
      <devtools-report-value>
        <div class="inline-items">
          ${!input.frame?.unreachableUrl() ? renderSourcesLinkForURL(input.onRevealInSources) : nothing}
          ${input.onRevealInNetwork ? renderNetworkLinkForURL(input.onRevealInNetwork) : nothing}
          <div class="text-ellipsis" title=${input.frame.url}>${input.frame.url}</div>
        </div>
      </devtools-report-value>
      ${maybeRenderUnreachableURL(input.frame?.unreachableUrl())}
      ${maybeRenderOrigin(input.frame?.securityOrigin)}
      ${renderOwnerElement(input.linkTargetDOMNode)}
      ${maybeRenderCreationStacktrace(input.creationStackTrace)}
      ${maybeRenderAdStatus(input.frame?.adFrameType(), input.frame?.adFrameStatus())}
      ${maybeRenderCreatorAdScriptAncestry(input.frame?.adFrameType(), input.target, input.adScriptAncestry)}
      <devtools-report-divider></devtools-report-divider>`;
  // clang-format on
}

function renderSourcesLinkForURL(onRevealInSources: () => void): LitTemplate {
  return ApplicationComponents.PermissionsPolicySection.renderIconLink(
      'label',
      i18nString(UIStrings.clickToOpenInSourcesPanel),
      onRevealInSources,
      'reveal-in-sources',
  );
}

function renderNetworkLinkForURL(onRevealInNetwork: () => void): LitTemplate {
  return ApplicationComponents.PermissionsPolicySection.renderIconLink(
      'arrow-up-down-circle', i18nString(UIStrings.clickToOpenInNetworkPanel), onRevealInNetwork, 'reveal-in-network');
}

function maybeRenderUnreachableURL(unreachableUrl: Platform.DevToolsPath.UrlString): LitTemplate {
  if (!unreachableUrl) {
    return nothing;
  }
  return html`
      <devtools-report-key>${i18nString(UIStrings.unreachableUrl)}</devtools-report-key>
      <devtools-report-value>
        <div class="inline-items">
          ${renderNetworkLinkForUnreachableURL(unreachableUrl)}
          <div class="text-ellipsis" title=${unreachableUrl}>${unreachableUrl}</div>
        </div>
      </devtools-report-value>
    `;
}

function renderNetworkLinkForUnreachableURL(unreachableUrlString: Platform.DevToolsPath.UrlString): LitTemplate {
  const unreachableUrl = Common.ParsedURL.ParsedURL.fromString(unreachableUrlString);
  if (unreachableUrl) {
    return ApplicationComponents.PermissionsPolicySection.renderIconLink(
        'arrow-up-down-circle',
        i18nString(UIStrings.clickToOpenInNetworkPanelMight),
        ():
            void => {
              void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
                {
                  filterType: NetworkForward.UIFilter.FilterType.Domain,
                  filterValue: unreachableUrl.domain(),
                },
                {
                  filterType: null,
                  filterValue: unreachableUrl.path,
                },
              ]));
            },
        'unreachable-url.reveal-in-network',
    );
  }
  return nothing;
}

function maybeRenderOrigin(securityOrigin: string|null): LitTemplate {
  if (securityOrigin && securityOrigin !== '://') {
    return html`
        <devtools-report-key>${i18nString(UIStrings.origin)}</devtools-report-key>
        <devtools-report-value>
          <div class="text-ellipsis" title=${securityOrigin}>${securityOrigin}</div>
        </devtools-report-value>
      `;
  }
  return nothing;
}

function renderOwnerElement(linkTargetDOMNode: SDK.DOMModel.DOMNode|null): LitTemplate {
  if (linkTargetDOMNode) {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
      return html`
        <devtools-report-key>${i18nString(UIStrings.ownerElement)}</devtools-report-key>
        <devtools-report-value class="without-min-width">
          <div class="inline-items">
            ${widget(PanelCommon.DOMLinkifier.DOMNodeLink, {node: linkTargetDOMNode})}
          </div>
        </devtools-report-value>
      `;
    // clang-format on
  }
  return nothing;
}

function maybeRenderCreationStacktrace(stackTrace: StackTrace.StackTrace.StackTrace|null): LitTemplate {
  if (stackTrace) {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
      return html`
        <devtools-report-key title=${i18nString(UIStrings.creationStackTraceExplanation)}>${
          i18nString(UIStrings.creationStackTrace)}</devtools-report-key>
        <devtools-report-value jslog=${VisualLogging.section('frame-creation-stack-trace')}>
          ${widget(Components.JSPresentationUtils.StackTracePreviewContent,
                   {stackTrace, options: {expandable: true}})}
        </devtools-report-value>
      `;
    // clang-format on
  }
  return nothing;
}

function getAdFrameTypeStrings(type: Protocol.Page.AdFrameType.Child|Protocol.Page.AdFrameType.Root):
    {value: Platform.UIString.LocalizedString, description: Platform.UIString.LocalizedString} {
  switch (type) {
    case Protocol.Page.AdFrameType.Child:
      return {value: i18nString(UIStrings.child), description: i18nString(UIStrings.childDescription)};
    case Protocol.Page.AdFrameType.Root:
      return {value: i18nString(UIStrings.root), description: i18nString(UIStrings.rootDescription)};
  }
}

function getAdFrameExplanationString(explanation: Protocol.Page.AdFrameExplanation): Platform.UIString.LocalizedString {
  switch (explanation) {
    case Protocol.Page.AdFrameExplanation.CreatedByAdScript:
      return i18nString(UIStrings.createdByAdScriptExplanation);
    case Protocol.Page.AdFrameExplanation.MatchedBlockingRule:
      return i18nString(UIStrings.matchedBlockingRuleExplanation);
    case Protocol.Page.AdFrameExplanation.ParentIsAd:
      return i18nString(UIStrings.parentIsAdExplanation);
  }
}

function maybeRenderAdStatus(
    adFrameType: Protocol.Page.AdFrameType|undefined,
    adFrameStatus: Protocol.Page.AdFrameStatus|undefined): LitTemplate {
  if (adFrameType === undefined || adFrameType === Protocol.Page.AdFrameType.None) {
    return nothing;
  }
  const typeStrings = getAdFrameTypeStrings(adFrameType);
  const rows = [html`<div title=${typeStrings.description}>${typeStrings.value}</div>`];
  for (const explanation of adFrameStatus?.explanations || []) {
    rows.push(html`<div>${getAdFrameExplanationString(explanation)}</div>`);
  }

  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    return html`
      <devtools-report-key>${i18nString(UIStrings.adStatus)}</devtools-report-key>
      <devtools-report-value class="ad-status-list" jslog=${VisualLogging.section('ad-status')}>
        <devtools-expandable-list .data=${
          {rows, title: i18nString(UIStrings.adStatus)} as ExpandableList.ExpandableList.ExpandableListData}>
        </devtools-expandable-list>
      </devtools-report-value>`;
  // clang-format on
}

function maybeRenderCreatorAdScriptAncestry(
    adFrameType: Protocol.Page.AdFrameType|null, target: SDK.Target.Target|null,
    adScriptAncestry: Protocol.Network.AdAncestry|null): LitTemplate {
  if (adFrameType === Protocol.Page.AdFrameType.None) {
    return nothing;
  }

  if (!target || !adScriptAncestry || adScriptAncestry.ancestryChain.length === 0) {
    return nothing;
  }

  const rows = adScriptAncestry.ancestryChain.map(adScriptId => {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    return html`<div>
      ${widget(Components.Linkifier.ScriptLocationLink,
              {target, scriptId: adScriptId.scriptId, options: {jslogContext: 'ad-script'}})}
    </div>`;
    // clang-format on
  });

  const shouldRenderFilterlistRule = (adScriptAncestry.rootScriptFilterlistRule !== undefined);

  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    return html`
      <devtools-report-key>${i18nString(UIStrings.creatorAdScriptAncestry)}</devtools-report-key>
      <devtools-report-value class="creator-ad-script-ancestry-list" jslog=${VisualLogging.section('creator-ad-script-ancestry')}>
        <devtools-expandable-list .data=${
          {rows, title: i18nString(UIStrings.creatorAdScriptAncestry)} as ExpandableList.ExpandableList.ExpandableListData}>
        </devtools-expandable-list>
      </devtools-report-value>
      ${shouldRenderFilterlistRule ? html`
        <devtools-report-key>${i18nString(UIStrings.rootScriptFilterlistRule)}</devtools-report-key>
        <devtools-report-value jslog=${VisualLogging.section('root-script-filterlist-rule')}>${adScriptAncestry.rootScriptFilterlistRule}</devtools-report-value>
      ` : nothing}
    `;
  // clang-format on
}

function renderIsolationSection(input: FrameDetailsViewInput): LitTemplate {
  if (!input.frame) {
    return nothing;
  }
  return html`
      <devtools-report-section-header>${i18nString(UIStrings.securityIsolation)}</devtools-report-section-header>
      <devtools-report-key>${i18nString(UIStrings.secureContext)}</devtools-report-key>
      <devtools-report-value>
        ${input.frame.isSecureContext() ? i18nString(UIStrings.yes) : i18nString(UIStrings.no)}\xA0${
      maybeRenderSecureContextExplanation(input.frame)}
      </devtools-report-value>
      <devtools-report-key>${i18nString(UIStrings.crossoriginIsolated)}</devtools-report-key>
      <devtools-report-value>
        ${input.frame.isCrossOriginIsolated() ? i18nString(UIStrings.yes) : i18nString(UIStrings.no)}
      </devtools-report-value>
      ${maybeRenderCoopCoepCSPStatus(input.securityIsolationInfo)}
      <devtools-report-divider></devtools-report-divider>
    `;
}

function maybeRenderSecureContextExplanation(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): LitTemplate {
  const explanation = getSecureContextExplanation(frame);
  if (explanation) {
    return html`<span class="inline-comment">${explanation}</span>`;
  }
  return nothing;
}

function getSecureContextExplanation(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null):
    Platform.UIString.LocalizedString|null {
  switch (frame?.getSecureContextType()) {
    case Protocol.Page.SecureContextType.Secure:
      return null;
    case Protocol.Page.SecureContextType.SecureLocalhost:
      return i18nString(UIStrings.localhostIsAlwaysASecureContext);
    case Protocol.Page.SecureContextType.InsecureAncestor:
      return i18nString(UIStrings.aFrameAncestorIsAnInsecure);
    case Protocol.Page.SecureContextType.InsecureScheme:
      return i18nString(UIStrings.theFramesSchemeIsInsecure);
  }
  return null;
}

function maybeRenderCoopCoepCSPStatus(info: Protocol.Network.SecurityIsolationStatus|null): LitTemplate {
  if (info) {
    return html`
          ${
        maybeRenderCrossOriginStatus(
            info.coep, i18n.i18n.lockedString('Cross-Origin Embedder Policy (COEP)'),
            Protocol.Network.CrossOriginEmbedderPolicyValue.None)}
          ${
        maybeRenderCrossOriginStatus(
            info.coop, i18n.i18n.lockedString('Cross-Origin Opener Policy (COOP)'),
            Protocol.Network.CrossOriginOpenerPolicyValue.UnsafeNone)}
          ${renderCSPSection(info.csp)}
        `;
  }
  return nothing;
}

function maybeRenderCrossOriginStatus(
    info: Protocol.Network.CrossOriginEmbedderPolicyStatus|Protocol.Network.CrossOriginOpenerPolicyStatus|undefined,
    policyName: string,
    noneValue: Protocol.Network.CrossOriginEmbedderPolicyValue|
    Protocol.Network.CrossOriginOpenerPolicyValue): LitTemplate {
  if (!info) {
    return nothing;
  }
  function crossOriginValueToString(
      value: Protocol.Network.CrossOriginEmbedderPolicyValue|Protocol.Network.CrossOriginOpenerPolicyValue): string {
    switch (value) {
      case Protocol.Network.CrossOriginEmbedderPolicyValue.Credentialless:
        return 'credentialless';
      case Protocol.Network.CrossOriginEmbedderPolicyValue.None:
        return 'none';
      case Protocol.Network.CrossOriginEmbedderPolicyValue.RequireCorp:
        return 'require-corp';
      case Protocol.Network.CrossOriginOpenerPolicyValue.NoopenerAllowPopups:
        return 'noopenener-allow-popups';
      case Protocol.Network.CrossOriginOpenerPolicyValue.SameOrigin:
        return 'same-origin';
      case Protocol.Network.CrossOriginOpenerPolicyValue.SameOriginAllowPopups:
        return 'same-origin-allow-popups';
      case Protocol.Network.CrossOriginOpenerPolicyValue.SameOriginPlusCoep:
        return 'same-origin-plus-coep';
      case Protocol.Network.CrossOriginOpenerPolicyValue.RestrictProperties:
        return 'restrict-properties';
      case Protocol.Network.CrossOriginOpenerPolicyValue.RestrictPropertiesPlusCoep:
        return 'restrict-properties-plus-coep';
      case Protocol.Network.CrossOriginOpenerPolicyValue.UnsafeNone:
        return 'unsafe-none';
    }
  }

  const isEnabled = info.value !== noneValue;
  const isReportOnly = (!isEnabled && info.reportOnlyValue !== noneValue);
  const endpoint = isEnabled ? info.reportingEndpoint : info.reportOnlyReportingEndpoint;
  return html`
      <devtools-report-key>${policyName}</devtools-report-key>
      <devtools-report-value>
        ${crossOriginValueToString(isEnabled ? info.value : info.reportOnlyValue)}
        ${isReportOnly ? html`<span class="inline-comment">report-only</span>` : nothing}
        ${endpoint ? html`<span class="inline-name">${i18nString(UIStrings.reportingTo)}</span>${endpoint}` : nothing}
      </devtools-report-value>
    `;
}

function renderEffectiveDirectives(directives: string): LitTemplate[] {
  const parsedDirectives = new CspEvaluator.CspParser.CspParser(directives).csp.directives;
  const result = [];
  for (const directive in parsedDirectives) {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
      result.push(html`
          <div>
            <span class="bold">${directive}</span>
            ${': ' + parsedDirectives[directive]?.join(', ')}
          </div>`);
    // clang-format on
  }
  return result;
}

function renderSingleCSP(cspInfo: Protocol.Network.ContentSecurityPolicyStatus, divider: boolean): LitTemplate {
  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    return html`
      <devtools-report-key>
        ${cspInfo.isEnforced ? i18n.i18n.lockedString('Content-Security-Policy') : html`
          ${i18n.i18n.lockedString('Content-Security-Policy-Report-Only')}
          <devtools-button
            .iconName=${'help'}
            class='help-button'
            .accessibleLabel=${i18nString(UIStrings.learnMore)}
            .variant=${Buttons.Button.Variant.ICON}
            .size=${Buttons.Button.Size.SMALL}
            @click=${()=> {window.location.href = 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only';}}
            jslog=${VisualLogging.link('learn-more.csp-report-only').track({click: true})}
            ></devtools-button>`}
      </devtools-report-key>
      <devtools-report-value>
        ${cspInfo.source === Protocol.Network.ContentSecurityPolicySource.HTTP ?
          i18n.i18n.lockedString('HTTP header') : i18n.i18n.lockedString('Meta tag')}
        ${renderEffectiveDirectives(cspInfo.effectiveDirectives)}
      </devtools-report-value>
      ${divider ? html`<devtools-report-divider class="subsection-divider"></devtools-report-divider>` : nothing}
    `;
  // clang-format on
}

function renderCSPSection(cspInfos: Protocol.Network.ContentSecurityPolicyStatus[]|undefined): LitTemplate {
  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    return html`
      <devtools-report-divider></devtools-report-divider>
      <devtools-report-section-header>
        ${i18nString(UIStrings.contentSecurityPolicy)}
      </devtools-report-section-header>
      ${(cspInfos?.length) ? cspInfos.map((cspInfo, index) => renderSingleCSP(cspInfo, index < cspInfos?.length - 1)) : html`
        <devtools-report-key>
          ${i18n.i18n.lockedString('Content-Security-Policy')}
        </devtools-report-key>
        <devtools-report-value>
          ${i18nString(UIStrings.none)}
        </devtools-report-value>
      `}
    `;
  // clang-format on
}

function renderApiAvailabilitySection(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): LitTemplate {
  if (!frame) {
    return nothing;
  }

  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    return html`
      <devtools-report-section-header>
        ${i18nString(UIStrings.apiAvailability)}
      </devtools-report-section-header>
      <devtools-report-section>
        <span class="report-section">
          ${i18nString(UIStrings.availabilityOfCertainApisDepends)}
          <devtools-link
            href="https://web.dev/why-coop-coep/" class="link"
            jslogcontext="learn-more.coop-coep">
            ${i18nString(UIStrings.learnMore)}
          </devtools-link>
        </span>
      </devtools-report-section>
      ${renderSharedArrayBufferAvailability(frame)}
      ${renderMeasureMemoryAvailability(frame)}
      <devtools-report-divider></devtools-report-divider>`;
  // clang-format on
}

function renderSharedArrayBufferAvailability(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): LitTemplate {
  if (frame) {
    const features = frame.getGatedAPIFeatures();
    if (features) {
      const sabAvailable = features.includes(Protocol.Page.GatedAPIFeatures.SharedArrayBuffers);
      const sabTransferAvailable =
          sabAvailable && features.includes(Protocol.Page.GatedAPIFeatures.SharedArrayBuffersTransferAllowed);
      const availabilityText = sabTransferAvailable ?
          i18nString(UIStrings.availableTransferable) :
          (sabAvailable ? i18nString(UIStrings.availableNotTransferable) : i18nString(UIStrings.unavailable));
      const tooltipText = sabTransferAvailable ?
          i18nString(UIStrings.sharedarraybufferConstructorIs) :
          (sabAvailable ? i18nString(UIStrings.sharedarraybufferConstructorIsAvailable) : '');

      function renderHint(frame: SDK.ResourceTreeModel.ResourceTreeFrame): LitTemplate {
        switch (frame.getCrossOriginIsolatedContextType()) {
          case Protocol.Page.CrossOriginIsolatedContextType.Isolated:
            return nothing;
          case Protocol.Page.CrossOriginIsolatedContextType.NotIsolated:
            if (sabAvailable) {
              // clang-format off
                return html`
                  <span class="inline-comment">
                    ${i18nString(UIStrings.willRequireCrossoriginIsolated)}
                  </span>`;
              // clang-format on
            }
            return html`<span class="inline-comment">${i18nString(UIStrings.requiresCrossoriginIsolated)}</span>`;
          case Protocol.Page.CrossOriginIsolatedContextType.NotIsolatedFeatureDisabled:
            if (!sabTransferAvailable) {
              // clang-format off
                return html`
                  <span class="inline-comment">
                    ${i18nString(UIStrings.transferRequiresCrossoriginIsolatedPermission)}
                    <code> cross-origin-isolated</code>
                  </span>`;
              // clang-format on
            }
            break;
        }
        return nothing;
      }

      // SharedArrayBuffer is an API name, so we don't translate it.
      return html`
          <devtools-report-key>SharedArrayBuffers</devtools-report-key>
          <devtools-report-value title=${tooltipText}>
            ${availabilityText}\xA0${renderHint(frame)}
          </devtools-report-value>
        `;
    }
  }
  return nothing;
}

function renderMeasureMemoryAvailability(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): LitTemplate {
  if (frame) {
    const measureMemoryAvailable = frame.isCrossOriginIsolated();
    const availabilityText =
        measureMemoryAvailable ? i18nString(UIStrings.available) : i18nString(UIStrings.unavailable);
    const tooltipText = measureMemoryAvailable ? i18nString(UIStrings.thePerformanceAPI) :
                                                 i18nString(UIStrings.thePerformancemeasureuseragentspecificmemory);
    return html`
        <devtools-report-key>${i18nString(UIStrings.measureMemory)}</devtools-report-key>
        <devtools-report-value>
          <span title=${tooltipText}>${
        availabilityText}</span>\xA0<devtools-link class="link" href="https://web.dev/monitor-total-page-memory-usage/" jslogcontext="learn-more.monitor-memory-usage">${
        i18nString(UIStrings.learnMore)}</devtools-link>
        </devtools-report-value>
      `;
  }
  return nothing;
}

function renderAdditionalInfoSection(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): LitTemplate {
  if (!frame) {
    return nothing;
  }

  return html`
      <devtools-report-section-header
        title=${i18nString(UIStrings.thisAdditionalDebugging)}
      >${i18nString(UIStrings.additionalInformation)}</devtools-report-section-header>
      <devtools-report-key>${i18nString(UIStrings.frameId)}</devtools-report-key>
      <devtools-report-value>
        <div class="text-ellipsis" title=${frame.id}>${frame.id}</div>
      </devtools-report-value>
      <devtools-report-divider></devtools-report-divider>
    `;
}

export class FrameDetailsReportView extends UI.Widget.Widget {
  #frame?: SDK.ResourceTreeModel.ResourceTreeFrame;
  #target: SDK.Target.Target|null = null;
  #creationStackTrace: StackTrace.StackTrace.StackTrace|null = null;
  #securityIsolationInfo: Protocol.Network.SecurityIsolationStatus|null = null;
  #linkTargetDOMNode: SDK.DOMModel.DOMNode|null = null;
  #trials: Protocol.Page.OriginTrial[]|null = null;
  #protocolMonitorExperimentEnabled = false;
  #permissionsPolicies: Protocol.Page.PermissionsPolicyFeatureState[]|null = null;
  #linkifier = new Components.Linkifier.Linkifier();
  #adScriptAncestry: Protocol.Network.AdAncestry|null = null;
  #view: View;

  constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
    super(element, {useShadowDom: true});
    this.#protocolMonitorExperimentEnabled =
        Root.Runtime.experiments.isEnabled(Root.ExperimentNames.ExperimentName.PROTOCOL_MONITOR);
    this.#view = view;
  }

  set frame(frame: SDK.ResourceTreeModel.ResourceTreeFrame) {
    this.#frame = frame;
    void this.#frame.getPermissionsPolicyState().then(permissionsPolicies => {
      this.#permissionsPolicies = permissionsPolicies;
      this.requestUpdate();
    });
    const {creationStackTrace: rawCreationStackTrace, creationStackTraceTarget: creationTarget} =
        frame.getCreationStackTraceData();
    if (rawCreationStackTrace) {
      void Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
          .createStackTraceFromProtocolRuntime(rawCreationStackTrace, creationTarget)
          .then(creationStackTrace => {
            this.#creationStackTrace = creationStackTrace;
            this.requestUpdate();
          });
    }
    const networkManager = frame.resourceTreeModel().target().model(SDK.NetworkManager.NetworkManager);
    void networkManager?.getSecurityIsolationStatus(frame.id).then(securityIsolationInfo => {
      this.#securityIsolationInfo = securityIsolationInfo;
      this.requestUpdate();
    });
    void frame.getOwnerDOMNodeOrDocument().then(linkTargetDOMNode => {
      this.#linkTargetDOMNode = linkTargetDOMNode;
      this.requestUpdate();
    });
    void frame.getOriginTrials().then(trials => {
      this.#trials = trials;
      this.requestUpdate();
    });
    this.requestUpdate();
  }

  get frame(): SDK.ResourceTreeModel.ResourceTreeFrame|undefined {
    return this.#frame;
  }

  override async performUpdate(): Promise<void> {
    const result = await this.#frame?.parentFrame()?.getAdScriptAncestry(this.#frame?.id);
    if (result && result.ancestryChain.length > 0) {
      this.#adScriptAncestry = result;
      this.#target = this.#frame?.resourceTreeModel().target() ?? null;
    }

    const frame = this.#frame;
    if (!frame) {
      return;
    }
    const frameRequest = frame.resourceForURL(frame.url)?.request;

    const input = {
      frame,
      target: this.#target,
      creationStackTrace: this.#creationStackTrace,
      protocolMonitorExperimentEnabled: this.#protocolMonitorExperimentEnabled,
      permissionsPolicies: this.#permissionsPolicies,
      adScriptAncestry: this.#adScriptAncestry,
      linkifier: this.#linkifier,
      linkTargetDOMNode: this.#linkTargetDOMNode,
      trials: this.#trials,
      securityIsolationInfo: this.#securityIsolationInfo,
      onRevealInNetwork: frameRequest ?
          () => {
            const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.tab(
                frameRequest, NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT);
            return Common.Revealer.reveal(requestLocation);
          } :
          undefined,
      onRevealInSources: async () => {
        const sourceCode = this.#uiSourceCodeForFrame(frame);
        if (sourceCode) {
          await Common.Revealer.reveal(sourceCode);
        }
      },
    };
    this.#view(input, undefined, this.contentElement);
  }

  #uiSourceCodeForFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame): Workspace.UISourceCode.UISourceCode|null {
    for (const project of Workspace.Workspace.WorkspaceImpl.instance().projects()) {
      const projectTarget = Bindings.NetworkProject.NetworkProject.getTargetForProject(project);
      if (projectTarget && projectTarget === frame.resourceTreeModel().target()) {
        const uiSourceCode = project.uiSourceCodeForURL(frame.url);
        if (uiSourceCode) {
          return uiSourceCode;
        }
      }
    }
    return null;
  }
}
