// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-lit-render-outside-of-view */

import * as Common from '../../../core/common/common.js';
import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import type {NameValue} from '../../../core/sdk/NetworkRequest.js';
import type * as SDK from '../../../core/sdk/sdk.js';
import * as Protocol from '../../../generated/protocol.js';
import * as IssuesManager from '../../../models/issues_manager/issues_manager.js';
import * as Persistence from '../../../models/persistence/persistence.js';
import * as TextUtils from '../../../models/text_utils/text_utils.js';
import type * as Workspace from '../../../models/workspace/workspace.js';
import * as NetworkForward from '../../../panels/network/forward/forward.js';
import * as Sources from '../../../panels/sources/sources.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as UI from '../../../ui/legacy/legacy.js';
import {html, nothing, render} from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import {
  compareHeaders,
  EditingAllowedStatus,
  type HeaderDescriptor,
  type HeaderDetailsDescriptor,
  type HeaderEditedEvent,
  type HeaderEditorDescriptor,
  type HeaderRemovedEvent,
  type HeaderSectionRowData,
  isValidHeaderName,
} from './HeaderSectionRow.js';
import responseHeaderSectionStyles from './ResponseHeaderSection.css.js';

const UIStrings = {
  /**
   * @description Label for a button which allows adding an HTTP header.
   */
  addHeader: 'Add header',
  /**
   * @description Explanation text for which cross-origin policy to set.
   */
  chooseThisOptionIfTheResourceAnd:
      'Choose this option if the resource and the document are served from the same site.',
  /**
   * @description Explanation text for which cross-origin policy to set.
   */
  onlyChooseThisOptionIfAn:
      'Only choose this option if an arbitrary website including this resource does not impose a security risk.',
  /**
   * @description Message in the Headers View of the Network panel when a cross-origin opener policy blocked loading a sandbox iframe.
   */
  thisDocumentWasBlockedFrom:
      'The document was blocked from loading in a popup opened by a sandboxed iframe because this document specified a cross-origin opener policy.',
  /**
   * @description Message in the Headers View of the Network panel when a cross-origin embedder policy header needs to be set.
   */
  toEmbedThisFrameInYourDocument:
      'To embed this frame in your document, the response needs to enable the cross-origin embedder policy by specifying the following response header:',
  /**
   * @description Message in the Headers View of the Network panel when a cross-origin resource policy header needs to be set.
   */
  toUseThisResourceFromADifferent:
      'To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers:',
  /**
   * @description Message in the Headers View of the Network panel when the cross-origin resource policy header is too strict.
   */
  toUseThisResourceFromADifferentOrigin:
      'To use this resource from a different origin, the server may relax the cross-origin resource policy response header:',
  /**
   * @description Message in the Headers View of the Network panel when the cross-origin resource policy header is too strict.
   */
  toUseThisResourceFromADifferentSite:
      'To use this resource from a different site, the server may relax the cross-origin resource policy response header:',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/network/components/ResponseHeaderSection.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);

export const RESPONSE_HEADER_SECTION_DATA_KEY = 'ResponseHeaderSection';

export interface ResponseHeaderSectionData {
  request: SDK.NetworkRequest.NetworkRequest;
  toReveal?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string};
}

class ResponseHeaderSectionBase extends HTMLElement {
  protected readonly shadow = this.attachShadow({mode: 'open'});
  protected headerDetails: HeaderDetailsDescriptor[] = [];

  protected setHeaders(headers: NameValue[]): void {
    headers.sort(function(a, b) {
      return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase());
    });

    this.headerDetails = headers.map(header => ({
                                       name: Platform.StringUtilities.toLowerCaseString(header.name),
                                       value: header.value.replace(/\s/g, ' '),
                                     }));
  }

  protected highlightHeaders(data: ResponseHeaderSectionData): void {
    if (data.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.RESPONSE) {
      this.headerDetails.filter(header => compareHeaders(header.name, data.toReveal?.header?.toLowerCase()))
          .forEach(header => {
            header.highlight = true;
          });
    }
  }
}

export class EarlyHintsHeaderSection extends ResponseHeaderSectionBase {
  #request?: SDK.NetworkRequest.NetworkRequest;

  set data(data: ResponseHeaderSectionData) {
    this.#request = data.request;

    this.setHeaders(this.#request.earlyHintsHeaders);
    this.highlightHeaders(data);

    this.#render();
  }

  #render(): void {
    if (!this.#request) {
      return;
    }

    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    render(html`
      <style>${responseHeaderSectionStyles}</style>
      ${this.headerDetails.map(header => html`
        <devtools-header-section-row .data=${{
        header,
      } as HeaderSectionRowData}></devtools-header-section-row>
      `)}
    `, this.shadow, { host: this });
    // clang-format on
  }
}

customElements.define('devtools-early-hints-header-section', EarlyHintsHeaderSection);

export class ResponseHeaderSection extends ResponseHeaderSectionBase {
  #request?: SDK.NetworkRequest.NetworkRequest;
  #headerEditors: HeaderEditorDescriptor[] = [];
  #uiSourceCode: Workspace.UISourceCode.UISourceCode|null = null;
  #overrides: Persistence.NetworkPersistenceManager.HeaderOverride[] = [];
  #isEditingAllowed = EditingAllowedStatus.DISABLED;

  set data(data: ResponseHeaderSectionData) {
    this.#request = data.request;
    this.#isEditingAllowed =
        Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl(this.#request.url()) ?
        EditingAllowedStatus.FORBIDDEN :
        EditingAllowedStatus.DISABLED;
    // If the request has been locally overridden, its 'sortedResponseHeaders'
    // contains no 'set-cookie' headers, because they have been filtered out by
    // the Chromium backend. DevTools therefore uses previously stored values.
    const headers = this.#request.sortedResponseHeaders.concat(this.#request.setCookieHeaders);
    this.setHeaders(headers);

    const headersWithIssues = [];
    if (this.#request.wasBlocked()) {
      const headerWithIssues =
          BlockedReasonDetails.get((this.#request.blockedReason() as Protocol.Network.BlockedReason));
      if (headerWithIssues) {
        if (IssuesManager.RelatedIssue.hasIssueOfCategory(
                this.#request, IssuesManager.Issue.IssueCategory.CROSS_ORIGIN_EMBEDDER_POLICY)) {
          const followLink = (): void => {
            Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.LEARN_MORE_LINK_COEP);
            if (this.#request) {
              void IssuesManager.RelatedIssue.reveal(
                  this.#request, IssuesManager.Issue.IssueCategory.CROSS_ORIGIN_EMBEDDER_POLICY);
            }
          };
          if (headerWithIssues.blockedDetails) {
            headerWithIssues.blockedDetails.reveal = followLink;
          }
        }
        headersWithIssues.push(headerWithIssues);
      }
    }

    function mergeHeadersWithIssues(
        headers: HeaderDetailsDescriptor[], headersWithIssues: HeaderDetailsDescriptor[]): HeaderDetailsDescriptor[] {
      let i = 0, j = 0;
      const result: HeaderDetailsDescriptor[] = [];
      while (i < headers.length && j < headersWithIssues.length) {
        if (headers[i].name < headersWithIssues[j].name) {
          result.push({...headers[i++], headerNotSet: false});
        } else if (headers[i].name > headersWithIssues[j].name) {
          result.push({...headersWithIssues[j++], headerNotSet: true});
        } else {
          result.push({...headersWithIssues[j++], ...headers[i++], headerNotSet: false});
        }
      }
      while (i < headers.length) {
        result.push({...headers[i++], headerNotSet: false});
      }
      while (j < headersWithIssues.length) {
        result.push({...headersWithIssues[j++], headerNotSet: true});
      }
      return result;
    }

    this.headerDetails = mergeHeadersWithIssues(this.headerDetails, headersWithIssues);

    const blockedResponseCookies = this.#request.blockedResponseCookies();
    const blockedCookieLineToReasons = new Map<string, Protocol.Network.SetCookieBlockedReason[]>(
        blockedResponseCookies?.map(c => [c.cookieLine.replace(/\s/g, ' '), c.blockedReasons]));
    for (const header of this.headerDetails) {
      if (header.name === 'set-cookie' && header.value) {
        const matchingBlockedReasons = blockedCookieLineToReasons.get(header.value);
        if (matchingBlockedReasons) {
          header.setCookieBlockedReasons = matchingBlockedReasons;
        }
      }
    }

    this.highlightHeaders(data);

    const dataAssociatedWithRequest = this.#request.getAssociatedData(RESPONSE_HEADER_SECTION_DATA_KEY);
    if (dataAssociatedWithRequest) {
      this.#headerEditors = dataAssociatedWithRequest as HeaderEditorDescriptor[];
    } else {
      this.#headerEditors = this.headerDetails.map(header => ({
                                                     name: header.name,
                                                     value: header.value,
                                                     originalValue: header.value,
                                                     valueEditable: this.#isEditingAllowed,
                                                   }));
      this.#markOverrides();
    }

    void this.#loadOverridesFileInfo();
    this.#request.setAssociatedData(RESPONSE_HEADER_SECTION_DATA_KEY, this.#headerEditors);
    this.#render();
  }

  #resetEditorState(): void {
    if (!this.#request) {
      return;
    }
    this.#isEditingAllowed =
        Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl(this.#request.url()) ?
        EditingAllowedStatus.FORBIDDEN :
        EditingAllowedStatus.DISABLED;
    this.#headerEditors = this.headerDetails.map(header => ({
                                                   name: header.name,
                                                   value: header.value,
                                                   originalValue: header.value,
                                                   valueEditable: this.#isEditingAllowed,
                                                 }));
    this.#markOverrides();
    this.#request.setAssociatedData(RESPONSE_HEADER_SECTION_DATA_KEY, this.#headerEditors);
  }

  async #loadOverridesFileInfo(): Promise<void> {
    if (!this.#request) {
      return;
    }
    this.#uiSourceCode =
        Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().getHeadersUISourceCodeFromUrl(
            this.#request.url());
    if (!this.#uiSourceCode) {
      this.#resetEditorState();
      this.#render();
      return;
    }
    try {
      const contentData =
          await this.#uiSourceCode.requestContentData().then(TextUtils.ContentData.ContentData.contentDataOrEmpty);
      this.#overrides = JSON.parse(contentData.text || '[]') as Persistence.NetworkPersistenceManager.HeaderOverride[];
      if (!this.#overrides.every(Persistence.NetworkPersistenceManager.isHeaderOverride)) {
        throw new Error('Type mismatch after parsing');
      }
      if (Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').get() &&
          this.#isEditingAllowed === EditingAllowedStatus.DISABLED) {
        this.#isEditingAllowed = EditingAllowedStatus.ENABLED;
      }
      for (const header of this.#headerEditors) {
        header.valueEditable = this.#isEditingAllowed;
      }
    } catch {
      console.error(
          'Failed to parse', this.#uiSourceCode?.url() || 'source code file', 'for locally overriding headers.');
      this.#resetEditorState();
    } finally {
      this.#render();
    }
  }

  #markOverrides(): void {
    if (!this.#request || this.#request.originalResponseHeaders.length === 0) {
      return;
    }
    const originalHeaders =
        this.#request.originalResponseHeaders.map(header => ({
                                                    name: Platform.StringUtilities.toLowerCaseString(header.name),
                                                    value: header.value.replace(/\s/g, ' '),
                                                  }));
    originalHeaders.sort(function(a, b) {
      return Platform.StringUtilities.compare(a.name, b.name);
    });

    // Loop over actual headers and original headers simultaneously and mark each actual header as
    // overridden if there is no identical original header.
    // If there are multiple headers with the same name, concatenate their values first before
    // comparing them.
    let indexActual = 0;
    let indexOriginal = 0;
    while (indexActual < this.headerDetails.length) {
      const currentName = this.headerDetails[indexActual].name;
      let actualValue = this.headerDetails[indexActual].value || '';
      const headerNotSet = this.headerDetails[indexActual].headerNotSet;
      while (indexActual < this.headerDetails.length - 1 && this.headerDetails[indexActual + 1].name === currentName) {
        indexActual++;
        actualValue += `, ${this.headerDetails[indexActual].value}`;
      }

      while (indexOriginal < originalHeaders.length && originalHeaders[indexOriginal].name < currentName) {
        indexOriginal++;
      }
      if (indexOriginal < originalHeaders.length && originalHeaders[indexOriginal].name === currentName) {
        let originalValue = originalHeaders[indexOriginal].value;
        while (indexOriginal < originalHeaders.length - 1 && originalHeaders[indexOriginal + 1].name === currentName) {
          indexOriginal++;
          originalValue += `, ${originalHeaders[indexOriginal].value}`;
        }
        indexOriginal++;
        if (currentName !== 'set-cookie' && !headerNotSet && !compareHeaders(actualValue, originalValue)) {
          this.#headerEditors.filter(header => compareHeaders(header.name, currentName)).forEach(header => {
            header.isOverride = true;
          });
        }
      } else if (currentName !== 'set-cookie' && !headerNotSet) {
        this.#headerEditors.filter(header => compareHeaders(header.name, currentName)).forEach(header => {
          header.isOverride = true;
        });
      }
      indexActual++;
    }

    // Special case for 'set-cookie' headers: compare each header individually
    // and don't treat all 'set-cookie' headers as a single unit.
    this.#headerEditors.filter(header => header.name === 'set-cookie').forEach(header => {
      if (this.#request?.originalResponseHeaders.find(
              originalHeader => Platform.StringUtilities.toLowerCaseString(originalHeader.name) === 'set-cookie' &&
                  compareHeaders(originalHeader.value, header.value)) === undefined) {
        header.isOverride = true;
      }
    });
  }

  #onHeaderEdited(event: HeaderEditedEvent): void {
    const target = event.target as HTMLElement;
    if (target.dataset.index === undefined) {
      return;
    }
    const index = Number(target.dataset.index);
    if (isValidHeaderName(event.headerName)) {
      this.#updateOverrides(event.headerName, event.headerValue, index);
      Host.userMetrics.actionTaken(Host.UserMetrics.Action.HeaderOverrideHeaderEdited);
    }
  }

  #fileNameFromUrl(url: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.RawPathString {
    const rawPath =
        Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().rawPathFromUrl(url, true);
    const lastIndexOfSlash = rawPath.lastIndexOf('/');
    return Common.ParsedURL.ParsedURL.substring(rawPath, lastIndexOfSlash + 1);
  }

  #commitOverrides(): void {
    this.#uiSourceCode?.setWorkingCopy(JSON.stringify(this.#overrides, null, 2));
    this.#uiSourceCode?.commitWorkingCopy();
  }

  #removeEntryFromOverrides(
      rawFileName: Platform.DevToolsPath.RawPathString, headerName: Platform.StringUtilities.LowerCaseString,
      headerValue: string): void {
    for (let blockIndex = this.#overrides.length - 1; blockIndex >= 0; blockIndex--) {
      const block = this.#overrides[blockIndex];
      if (block.applyTo !== rawFileName) {
        continue;
      }
      const foundIndex = block.headers.findIndex(
          header => compareHeaders(header.name, headerName) && compareHeaders(header.value, headerValue));
      if (foundIndex < 0) {
        continue;
      }
      block.headers.splice(foundIndex, 1);
      if (block.headers.length === 0) {
        this.#overrides.splice(blockIndex, 1);
      }
      return;
    }
  }

  #onHeaderRemoved(event: HeaderRemovedEvent): void {
    const target = event.target as HTMLElement;
    if (target.dataset.index === undefined || !this.#request) {
      return;
    }
    const index = Number(target.dataset.index);
    const rawFileName = this.#fileNameFromUrl(this.#request.url());
    this.#removeEntryFromOverrides(rawFileName, event.headerName, event.headerValue);
    this.#commitOverrides();
    this.#headerEditors[index].isDeleted = true;
    this.#render();
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.HeaderOverrideHeaderRemoved);
  }

  #updateOverrides(headerName: Platform.StringUtilities.LowerCaseString, headerValue: string, index: number): void {
    if (!this.#request) {
      return;
    }
    // If 'originalResponseHeaders' are not populated (because there was no
    // request interception), fill them with a copy of 'sortedResponseHeaders'.
    // This ensures we have access to the original values when undoing edits.
    if (this.#request.originalResponseHeaders.length === 0) {
      this.#request.originalResponseHeaders =
          this.#request.sortedResponseHeaders.map(headerEntry => ({...headerEntry}));
    }

    const previousName = this.#headerEditors[index].name;
    const previousValue = this.#headerEditors[index].value;
    this.#headerEditors[index].name = headerName;
    this.#headerEditors[index].value = headerValue;

    let headersToUpdate: HeaderEditorDescriptor[] = [];
    if (headerName === 'set-cookie') {
      // Special case for 'set-cookie' headers: each such header is treated
      // separately without looking at other 'set-cookie' headers.
      headersToUpdate.push({name: headerName, value: headerValue, valueEditable: this.#isEditingAllowed});
    } else {
      // If multiple headers have the same name 'foo', we treat them as a unit.
      // If there are overrides for 'foo', all original 'foo' headers are removed
      // and replaced with the override(s) for 'foo'.
      headersToUpdate = this.#headerEditors.filter(
          header => compareHeaders(header.name, headerName) &&
              (!compareHeaders(header.value, header.originalValue) || header.isOverride));
    }

    const rawFileName = this.#fileNameFromUrl(this.#request.url());

    // If the last override-block matches 'rawFileName', use this last block.
    // Otherwise just append a new block at the end. We are not using earlier
    // blocks, because they could be overruled by later blocks, which contain
    // wildcards in the filenames they apply to.
    let block: Persistence.NetworkPersistenceManager.HeaderOverride|null = null;
    const [lastOverride] = this.#overrides.slice(-1);
    if (lastOverride?.applyTo === rawFileName) {
      block = lastOverride;
    } else {
      block = {
        applyTo: rawFileName,
        headers: [],
      };
      this.#overrides.push(block);
    }

    if (headerName === 'set-cookie') {
      // Special case for 'set-cookie' headers: only remove the one specific
      // header which is currently being modified, keep all other headers
      // (including other 'set-cookie' headers).
      const foundIndex = block.headers.findIndex(
          header => compareHeaders(header.name, previousName) && compareHeaders(header.value, previousValue));
      if (foundIndex >= 0) {
        block.headers.splice(foundIndex, 1);
      }
    } else {
      // Keep header overrides for all headers with a different name.
      block.headers = block.headers.filter(header => !compareHeaders(header.name, headerName));
    }

    // If a header name has been edited (only possible when adding headers),
    // remove the previous override entry.
    if (!compareHeaders(this.#headerEditors[index].name, previousName)) {
      for (let i = 0; i < block.headers.length; ++i) {
        if (compareHeaders(block.headers[i].name, previousName) &&
            compareHeaders(block.headers[i].value, previousValue)) {
          block.headers.splice(i, 1);
          break;
        }
      }
    }

    // Append freshly edited header overrides.
    for (const header of headersToUpdate) {
      block.headers.push({name: header.name, value: header.value || ''});
    }

    if (block.headers.length === 0) {
      this.#overrides.pop();
    }
    this.#commitOverrides();
  }

  #onAddHeaderClick(): void {
    this.#headerEditors.push({
      name: Platform.StringUtilities.toLowerCaseString(i18n.i18n.lockedString('header-name')),
      value: i18n.i18n.lockedString('header value'),
      isOverride: true,
      nameEditable: true,
      valueEditable: EditingAllowedStatus.ENABLED,
    });
    const index = this.#headerEditors.length - 1;
    this.#updateOverrides(this.#headerEditors[index].name, this.#headerEditors[index].value || '', index);
    this.#render();

    const rows = this.shadow.querySelectorAll('devtools-header-section-row');
    const [lastRow] = Array.from(rows).slice(-1);
    lastRow?.focus();
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.HeaderOverrideHeaderAdded);
  }

  #render(): void {
    if (!this.#request) {
      return;
    }

    const headerDescriptors: HeaderDescriptor[] = this.#headerEditors.map(
        (headerEditor, index) => ({...this.headerDetails[index], ...headerEditor, isResponseHeader: true}));

    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    render(html`
      <style>${responseHeaderSectionStyles}</style>
      ${headerDescriptors.map((header, index) => html`
        <devtools-header-section-row
            .data=${{header} as HeaderSectionRowData}
            @headeredited=${this.#onHeaderEdited}
            @headerremoved=${this.#onHeaderRemoved}
            @enableheaderediting=${this.#onEnableHeaderEditingClick}
            data-index=${index}
            jslog=${VisualLogging.item('response-header')}
        ></devtools-header-section-row>
      `)}
      ${this.#isEditingAllowed === EditingAllowedStatus.ENABLED ? html`
        <devtools-button
          class="add-header-button"
          .variant=${Buttons.Button.Variant.OUTLINED}
          .iconName=${'plus'}
          @click=${this.#onAddHeaderClick}
          jslog=${VisualLogging.action('add-header').track({click: true})}>
          ${i18nString(UIStrings.addHeader)}
        </devtools-button>
      ` : nothing}
    `, this.shadow, {host: this});
    // clang-format on
  }

  async #onEnableHeaderEditingClick(): Promise<void> {
    if (!this.#request) {
      return;
    }
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.HeaderOverrideEnableEditingClicked);
    const requestUrl = this.#request.url();
    const networkPersistenceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance();
    if (networkPersistenceManager.project()) {
      Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(true);
      await networkPersistenceManager.getOrCreateHeadersUISourceCodeFromUrl(requestUrl);
    } else {  // If folder for local overrides has not been provided yet
      UI.InspectorView.InspectorView.instance().displaySelectOverrideFolderInfobar(async () => {
        await Sources.SourcesNavigator.OverridesNavigatorView.instance().setupNewWorkspace();
        await networkPersistenceManager.getOrCreateHeadersUISourceCodeFromUrl(requestUrl);
      });
    }
  }
}

customElements.define('devtools-response-header-section', ResponseHeaderSection);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-response-header-section': ResponseHeaderSection;
    'devtools-early-hints-header-section': EarlyHintsHeaderSection;
  }
}

const BlockedReasonDetails = new Map<Protocol.Network.BlockedReason, HeaderDetailsDescriptor>([
  [
    Protocol.Network.BlockedReason.CoepFrameResourceNeedsCoepHeader,
    {
      name: Platform.StringUtilities.toLowerCaseString('cross-origin-embedder-policy'),
      value: null,
      blockedDetails: {
        explanation: i18nLazyString(UIStrings.toEmbedThisFrameInYourDocument),
        examples: [{
          codeSnippet: 'Cross-Origin-Embedder-Policy: require-corp',
        }],
        link: {url: 'https://web.dev/coop-coep/'},
      },
    },
  ],
  [
    Protocol.Network.BlockedReason.CorpNotSameOriginAfterDefaultedToSameOriginByCoep,
    {
      name: Platform.StringUtilities.toLowerCaseString('cross-origin-resource-policy'),
      value: null,
      blockedDetails: {
        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferent),
        examples: [
          {
            codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
            comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
          },
          {
            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
          },
        ],
        link: {url: 'https://web.dev/coop-coep/'},
      },
    },
  ],
  [
    Protocol.Network.BlockedReason.CoopSandboxedIframeCannotNavigateToCoopPage,
    {
      name: Platform.StringUtilities.toLowerCaseString('cross-origin-opener-policy'),
      value: null,
      headerValueIncorrect: false,
      blockedDetails: {
        explanation: i18nLazyString(UIStrings.thisDocumentWasBlockedFrom),
        examples: [],
        link: {url: 'https://web.dev/coop-coep/'},
      },
    },
  ],
  [
    Protocol.Network.BlockedReason.CorpNotSameSite,
    {
      name: Platform.StringUtilities.toLowerCaseString('cross-origin-resource-policy'),
      value: null,
      headerValueIncorrect: true,
      blockedDetails: {
        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentSite),
        examples: [
          {
            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
          },
        ],
        link: null,
      },
    },
  ],
  [
    Protocol.Network.BlockedReason.CorpNotSameOrigin,
    {
      name: Platform.StringUtilities.toLowerCaseString('cross-origin-resource-policy'),
      value: null,
      headerValueIncorrect: true,
      blockedDetails: {
        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentOrigin),
        examples: [
          {
            codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
            comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
          },
          {
            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
          },
        ],
        link: null,
      },
    },
  ],
]);
