/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../../test/common-test-setup';
import './gr-change-metadata';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {ChangeRole, GrChangeMetadata} from './gr-change-metadata';
import {
  createServerInfo,
  createUserConfig,
  createParsedChange,
  createAccountWithId,
  createCommitInfoWithRequiredCommit,
  createWebLinkInfo,
  createGerritInfo,
  createGitPerson,
  createCommit,
  createRevision,
  createAccountDetailWithId,
  createConfig,
} from '../../../test/test-data-generators';
import {
  ChangeStatus,
  SubmitType,
  GpgKeyInfoStatus,
  InheritedBooleanInfoConfiguredValue,
} from '../../../constants/constants';
import {
  EmailAddress,
  AccountId,
  CommitId,
  ServerInfo,
  RevisionInfo,
  ParentCommitInfo,
  TopicName,
  RevisionPatchSetNum,
  NumericChangeId,
  LabelValueToDescriptionMap,
  Hashtag,
  CommitInfo,
} from '../../../types/common';
import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
import {PluginApi} from '../../../api/plugin';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {
  queryAndAssert,
  resetPlugins,
  stubRestApi,
  waitUntilCalled,
} from '../../../test/test-utils';
import {ParsedChangeInfo} from '../../../types/types';
import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
import {GrButton} from '../../shared/gr-button/gr-button';
import {nothing} from 'lit';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';

suite('gr-change-metadata tests', () => {
  let element: GrChangeMetadata;

  setup(async () => {
    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
    stubRestApi('getConfig').returns(
      Promise.resolve({
        ...createServerInfo(),
        user: {
          ...createUserConfig(),
          anonymouscowardname: 'test coward name',
        },
      })
    );
    element = await fixture(html`<gr-change-metadata></gr-change-metadata>`);
    element.change = createParsedChange();
    await element.updateComplete;
  });

  test('renders', async () => {
    await element.updateComplete;
    assert.shadowDom.equal(
      element,
      /* HTML */ `<div>
      <div class="metadata-header">
        <h3 class="heading-3 metadata-title">Change Info</h3>
        <gr-button
          class="show-all-button"
          link=""
          role="button"
          tabindex="0"
          aria-disabled="false"
        >
          Show all <gr-icon icon="expand_more"></gr-icon>
          <gr-icon hidden="" icon="expand_less"></gr-icon>
        </gr-button>
      </div>
      <section class="hideDisplay">
        <span class="title">
          <gr-tooltip-content
            has-tooltip=""
            title="Last update of (meta)data for this change."
          >
            Updated
          </gr-tooltip-content>
        </span>
        <span class="value">
          <gr-date-formatter showyesterday="" withtooltip="">
          </gr-date-formatter>
        </span>
      </section>
      <section>
        <span class="title">
          <gr-tooltip-content
            has-tooltip=""
            title="This user created or uploaded the first patchset of this change."
          >
            Owner
          </gr-tooltip-content>
        </span>
        <span class="value">
          <gr-account-chip highlightattention=""
            ><gr-vote-chip circle-shape="" slot="vote-chip"> </gr-vote-chip
          ></gr-account-chip>
        </span>
      </section>
      <section>
        <span class="title">
          <gr-tooltip-content
            has-tooltip=""
            title="This user wrote the code change."
          >
            Author
          </gr-tooltip-content>
        </span>
        <span class="value">
          <gr-account-chip><gr-vote-chip circle-shape="" slot="vote-chip"></gr-vote-chip
          ></gr-account-chip>
        </span>
      </section>
      <section>
        <span class="title">
          <gr-tooltip-content
            has-tooltip=""
            title="This user committed the code change to the Git repository (typically to the local Git repo before uploading)."
          >
            Committer
          </gr-tooltip-content>
        </span>
        <span class="value">
          <gr-account-chip><gr-vote-chip circle-shape="" slot="vote-chip"></gr-account-chip>
        </span>
      </section>
      <section>
        <span class="title"> Reviewers </span>
        <span class="value">
          <gr-reviewer-list reviewers-only=""> </gr-reviewer-list>
        </span>
      </section>
      <section class="hideDisplay">
        <span class="title"> CC </span>
        <span class="value">
          <gr-reviewer-list ccs-only=""> </gr-reviewer-list>
        </span>
      </section>
      <section>
          <span class="title">
            Repo | Branch
          </span>
          <span class="value">
            <a href="/q/project:test-project">
              test-project
            </a>
            |
            <a href="/q/project:test-project+branch:test-branch+status:open">
              test-branch
            </a>
          </span>
        </section>
      <section class="hideDisplay">
        <span class="title">Parent</span>
        <span class="value">
          <ol  class="nonMerge notCurrent parentList"></ol>
        </span>
      </section>
      <section class="hideDisplay strategy">
        <span class="title"> Strategy </span> <span class="value"> </span>
      </section>
      <section class="hashtag hideDisplay">
        <span class="title"> Hashtags </span>
        <span class="value"> </span>
      </section>
      <div class="separatedSection">
      <gr-submit-requirements></gr-submit-requirements>
      </div>
      <gr-endpoint-decorator name="change-metadata-item">
        <gr-endpoint-param name="labels"> </gr-endpoint-param>
        <gr-endpoint-param name="change"> </gr-endpoint-param>
        <gr-endpoint-param name="revision"> </gr-endpoint-param>
      </gr-endpoint-decorator>
    </div>`
    );
  });

  test('computeMergedCommitInfo', () => {
    const dummyRevs: {[revisionId: string]: RevisionInfo} = {
      1: createRevision(1),
      2: createRevision(2),
    };
    assert.deepEqual(
      element.computeMergedCommitInfo('0' as CommitId, dummyRevs),
      undefined
    );
    assert.deepEqual(
      element.computeMergedCommitInfo('1' as CommitId, dummyRevs),
      dummyRevs[1].commit
    );

    // Regression test for issue 5337.
    const commit = element.computeMergedCommitInfo('2' as CommitId, dummyRevs);
    assert.notDeepEqual(commit, dummyRevs[2] as unknown as CommitInfo);
    assert.deepEqual(commit, dummyRevs[2].commit);
  });

  test('show strategy for open change', async () => {
    element.change = {
      ...createParsedChange(),
      status: ChangeStatus.NEW,
      submit_type: SubmitType.CHERRY_PICK,
      labels: {},
    };
    await element.updateComplete;
    const strategy = element.shadowRoot?.querySelector('.strategy');
    assert.ok(strategy);
    assert.isFalse(strategy?.hasAttribute('hidden'));
    assert.equal(strategy?.children[1].textContent, 'Cherry Pick');
  });

  test('hide strategy for closed change', async () => {
    element.change = {
      ...createParsedChange(),
      status: ChangeStatus.MERGED,
      labels: {},
    };
    await element.updateComplete;
    assert.isNull(element.shadowRoot?.querySelector('.strategy'));
  });

  test('weblinks hidden when no weblinks', async () => {
    element.commitInfo = createCommitInfoWithRequiredCommit();
    element.serverConfig = createServerInfo();
    await element.updateComplete;
    assert.isNull(element.webLinks);
  });

  test('weblinks hidden when only gitiles weblink', async () => {
    element.commitInfo = {
      ...createCommitInfoWithRequiredCommit(),
      web_links: [{...createWebLinkInfo(), name: 'gitiles', url: '#'}],
    };
    element.serverConfig = createServerInfo();
    await element.updateComplete;
    assert.isNull(element.webLinks);
    assert.equal(element.computeWebLinks().length, 0);
  });

  test('weblinks hidden when sole weblink is set as primary', async () => {
    const browser = 'browser';
    element.commitInfo = {
      ...createCommitInfoWithRequiredCommit(),
      web_links: [{...createWebLinkInfo(), name: browser, url: '#'}],
    };
    element.serverConfig = {
      ...createServerInfo(),
      gerrit: {
        ...createGerritInfo(),
        primary_weblink_name: browser,
      },
    };
    await element.updateComplete;
    assert.isNull(element.webLinks);
  });

  test('weblinks are visible when other weblinks', async () => {
    element.commitInfo = {
      ...createCommitInfoWithRequiredCommit(),
      web_links: [{...createWebLinkInfo(), name: 'test', url: '#'}],
    };
    await element.updateComplete;
    const webLinks = element.webLinks!;
    assert.isFalse(webLinks.hasAttribute('hidden'));
    assert.equal(element.computeWebLinks().length, 1);
  });

  test('weblinks are visible when gitiles and other weblinks', async () => {
    element.commitInfo = {
      ...createCommitInfoWithRequiredCommit(),
      web_links: [
        {...createWebLinkInfo(), name: 'test', url: '#'},
        {...createWebLinkInfo(), name: 'gitiles', url: '#'},
      ],
    };
    await element.updateComplete;
    const webLinks = element.webLinks!;
    assert.isFalse(webLinks.hasAttribute('hidden'));
    // Only the non-gitiles weblink is returned.
    assert.equal(element.computeWebLinks().length, 1);
  });

  suite('getNonOwnerRole', () => {
    let change: ParsedChangeInfo | undefined;

    setup(() => {
      change = {
        ...createParsedChange(),
        owner: {
          ...createAccountWithId(),
          email: 'abc@def' as EmailAddress,
          _account_id: 1019328 as AccountId,
        },
        revisions: {
          rev1: {
            ...createRevision(),
            uploader: {
              ...createAccountWithId(),
              email: 'ghi@def' as EmailAddress,
              _account_id: 1011123 as AccountId,
            },
            commit: {
              ...createCommit(),
              author: {...createGitPerson(), email: 'jkl@def' as EmailAddress},
              committer: {
                ...createGitPerson(),
                email: 'ghi@def' as EmailAddress,
              },
            },
          },
        },
        current_revision: 'rev1' as CommitId,
      };
    });

    suite('role=uploader', () => {
      test('getNonOwnerRole for uploader', () => {
        element.change = change;
        assert.deepEqual(element.getNonOwnerRole(ChangeRole.UPLOADER), {
          ...createAccountWithId(),
          email: 'ghi@def' as EmailAddress,
          _account_id: 1011123 as AccountId,
        });
      });

      test('getNonOwnerRole that it does not return uploader', () => {
        // Set the uploader email to be the same as the owner.
        change!.revisions.rev1.uploader!._account_id = 1019328 as AccountId;
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.UPLOADER));
      });

      test('computeShowRoleClass show uploader', () => {
        element.change = change;
        assert.notEqual(element.renderNonOwner(ChangeRole.UPLOADER), nothing);
      });

      test('computeShowRoleClass hide uploader', () => {
        // Set the uploader email to be the same as the owner.
        change!.revisions.rev1.uploader!._account_id = 1019328 as AccountId;
        element.change = change;
        assert.equal(element.renderNonOwner(ChangeRole.UPLOADER), nothing);
      });
    });

    suite('role=committer', () => {
      test('getNonOwnerRole for committer', () => {
        change!.revisions.rev1.uploader!.email = 'ghh@def' as EmailAddress;
        element.change = change;
        assert.deepEqual(element.getNonOwnerRole(ChangeRole.COMMITTER), {
          ...createGitPerson(),
          email: 'ghi@def' as EmailAddress,
        });
      });

      test('getNonOwnerRole is null if committer is same as uploader', () => {
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
      });

      test('getNonOwnerRole that it does not return committer', () => {
        // Set the committer email to be the same as the owner.
        change!.revisions.rev1.commit!.committer.email =
          'abc@def' as EmailAddress;
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
      });

      test('getNonOwnerRole null for committer with no commit', () => {
        delete change!.revisions.rev1.commit;
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
      });
    });

    suite('role=author', () => {
      test('getNonOwnerRole for author', () => {
        element.change = change;
        assert.deepEqual(element.getNonOwnerRole(ChangeRole.AUTHOR), {
          ...createGitPerson(),
          email: 'jkl@def' as EmailAddress,
        });
      });

      test('getNonOwnerRole that it does not return author', () => {
        // Set the author email to be the same as the owner.
        change!.revisions.rev1.commit!.author.email = 'abc@def' as EmailAddress;
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
      });

      test('getNonOwnerRole null for author with no commit', () => {
        delete change!.revisions.rev1.commit;
        element.change = change;
        assert.isNotOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
      });
    });
  });

  suite('Push Certificate Validation', () => {
    let serverConfig: ServerInfo | undefined;
    let change: ParsedChangeInfo | undefined;

    setup(() => {
      serverConfig = {
        ...createServerInfo(),
        receive: {
          enable_signed_push: 'true',
        },
      };
      change = {
        ...createParsedChange(),
        revisions: {
          rev1: {
            ...createRevision(1),
            push_certificate: {
              certificate: 'Push certificate',
              key: {
                status: GpgKeyInfoStatus.BAD,
                problems: ['No public keys found for key ID E5E20E52'],
              },
            },
          },
        },
        current_revision: 'rev1' as CommitId,
        status: ChangeStatus.NEW,
        labels: {},
        mergeable: true,
      };
      element.repoConfig = {
        ...createConfig(),
        enable_signed_push: {
          configured_value: 'TRUE' as InheritedBooleanInfoConfiguredValue,
          value: true,
        },
      };
    });

    test('Push Certificate Validation test BAD', () => {
      change!.revisions.rev1!.push_certificate = {
        certificate: 'Push certificate',
        key: {
          status: GpgKeyInfoStatus.BAD,
          problems: ['No public keys found for key ID E5E20E52'],
        },
      };
      element.change = change;
      element.serverConfig = serverConfig;
      const result = element.computePushCertificateValidation();
      assert.equal(
        result?.message,
        'Push certificate is invalid:\n' +
          'No public keys found for key ID E5E20E52'
      );
      assert.equal(result?.icon, 'close');
      assert.equal(result?.class, 'invalid');
    });

    test('Push Certificate Validation test TRUSTED', () => {
      change!.revisions.rev1!.push_certificate = {
        certificate: 'Push certificate',
        key: {
          status: GpgKeyInfoStatus.TRUSTED,
        },
      };
      element.change = change;
      element.serverConfig = serverConfig;
      const result = element.computePushCertificateValidation();
      assert.equal(
        result?.message,
        'Push certificate is valid and key is trusted'
      );
      assert.equal(result?.icon, 'check');
      assert.equal(result?.class, 'trusted');
    });

    test('Push Certificate Validation is missing test', () => {
      change!.revisions.rev1 = createRevision(1);
      element.change = change;
      element.serverConfig = serverConfig;
      const result = element.computePushCertificateValidation();
      assert.equal(
        result?.message,
        'This patch set was created without a push certificate'
      );
      assert.equal(result?.icon, 'help');
      assert.equal(result?.class, 'help filled');
    });

    test('computePushCertificateValidation returns undefined', () => {
      element.change = change;
      delete serverConfig!.receive!.enable_signed_push;
      element.serverConfig = serverConfig;
      assert.isUndefined(element.computePushCertificateValidation());
    });

    test('isEnabledSignedPushOnRepo', () => {
      change!.revisions.rev1!.push_certificate = {
        certificate: 'Push certificate',
        key: {
          status: GpgKeyInfoStatus.TRUSTED,
        },
      };
      element.change = change;
      element.serverConfig = serverConfig;
      element.repoConfig!.enable_signed_push!.configured_value =
        InheritedBooleanInfoConfiguredValue.INHERIT;
      element.repoConfig!.enable_signed_push!.inherited_value = true;
      assert.isTrue(element.isEnabledSignedPushOnRepo());

      element.repoConfig!.enable_signed_push!.inherited_value = false;
      assert.isFalse(element.isEnabledSignedPushOnRepo());

      element.repoConfig!.enable_signed_push!.configured_value =
        InheritedBooleanInfoConfiguredValue.TRUE;
      assert.isTrue(element.isEnabledSignedPushOnRepo());

      element.repoConfig = undefined;
      assert.isFalse(element.isEnabledSignedPushOnRepo());
    });
  });

  test('computeParents', () => {
    const parents: ParentCommitInfo[] = [
      {...createCommit(), commit: '123' as CommitId, subject: 'abc'},
    ];
    const revision: RevisionInfo = {
      ...createRevision(1),
      commit: {...createCommit(), parents},
    };
    element.change = undefined;
    element.revision = revision;
    assert.equal(element.computeParents(), parents);
    const change = (current_revision: CommitId): ParsedChangeInfo => {
      return {
        ...createParsedChange(),
        current_revision,
        revisions: {456: revision},
      };
    };
    const changebadrevision = change('789' as CommitId);
    element.change = changebadrevision;
    element.revision = createRevision();
    assert.deepEqual(element.computeParents(), []);
    const changenocommit: ParsedChangeInfo = {
      ...createParsedChange(),
      current_revision: '456' as CommitId,
      revisions: {456: createRevision()},
    };
    element.change = changenocommit;
    element.revision = undefined;
    assert.deepEqual(element.computeParents(), []);
    const changegood = change('456' as CommitId);
    element.change = changegood;
    element.revision = undefined;
    assert.equal(element.computeParents(), parents);
  });

  test('currentParents', async () => {
    const revision = (parent: CommitId): RevisionInfo => {
      return {
        ...createRevision(),
        commit: {
          ...createCommit(),
          parents: [{...createCommit(), commit: parent, subject: 'abc'}],
        },
      };
    };
    element.change = {
      ...createParsedChange(),
      current_revision: '456' as CommitId,
      revisions: {456: revision('111' as CommitId)},
      owner: {},
    };
    element.revision = revision('222' as CommitId);
    await element.updateComplete;
    assert.equal(element.currentParents[0].commit, '222');
    element.revision = revision('333' as CommitId);
    await element.updateComplete;
    assert.equal(element.currentParents[0].commit, '333');
    element.revision = undefined;
    await element.updateComplete;
    assert.equal(element.currentParents[0].commit, '111');
    element.change = createParsedChange();
    await element.updateComplete;
    assert.deepEqual(element.currentParents, []);
  });

  test('computeParentListClass', () => {
    const parent: ParentCommitInfo = {
      ...createCommit(),
      commit: 'abc123' as CommitId,
      subject: 'My parent commit',
    };
    element.currentParents = [parent];
    element.parentIsCurrent = true;
    assert.equal(
      element.computeParentListClass(),
      'parentList nonMerge current'
    );
    element.currentParents = [parent];
    element.parentIsCurrent = false;
    assert.equal(
      element.computeParentListClass(),
      'parentList nonMerge notCurrent'
    );
    element.currentParents = [parent, parent];
    element.parentIsCurrent = false;
    assert.equal(
      element.computeParentListClass(),
      'parentList merge notCurrent'
    );
    element.currentParents = [parent, parent];
    element.parentIsCurrent = true;
    assert.equal(element.computeParentListClass(), 'parentList merge current');
  });

  test('showAddTopic', () => {
    const change = createParsedChange();
    element.change = undefined;
    element.settingTopic = false;
    element.topicReadOnly = false;
    assert.isTrue(element.showAddTopic());
    // do not show for 'readonly'
    element.change = undefined;
    element.settingTopic = false;
    element.topicReadOnly = true;
    assert.isFalse(element.showAddTopic());
    element.change = change;
    element.settingTopic = false;
    element.topicReadOnly = false;
    assert.isTrue(element.showAddTopic());
    element.change = change;
    element.settingTopic = true;
    element.topicReadOnly = false;
    assert.isFalse(element.showAddTopic());
    change.topic = 'foo' as TopicName;
    element.change = change;
    element.settingTopic = true;
    element.topicReadOnly = false;
    assert.isFalse(element.showAddTopic());
    element.change = change;
    element.settingTopic = false;
    element.topicReadOnly = false;
    assert.isFalse(element.showAddTopic());
  });

  test('showTopicChip', async () => {
    const change = createParsedChange();
    element.change = change;
    element.settingTopic = true;
    await element.updateComplete;
    assert.isFalse(element.showTopicChip());
    element.change = change;
    element.settingTopic = false;
    await element.updateComplete;
    assert.isFalse(element.showTopicChip());
    element.change = change;
    element.settingTopic = true;
    await element.updateComplete;
    assert.isFalse(element.showTopicChip());
    change.topic = 'foo' as TopicName;
    element.change = change;
    element.settingTopic = true;
    await element.updateComplete;
    assert.isFalse(element.showTopicChip());
    element.change = change;
    element.settingTopic = false;
    await element.updateComplete;
    assert.isTrue(element.showTopicChip());
  });

  test('showCherryPickOf', async () => {
    element.change = undefined;
    await element.updateComplete;
    assert.isFalse(element.showCherryPickOf());
    const change = createParsedChange();
    element.change = change;
    await element.updateComplete;
    assert.isFalse(element.showCherryPickOf());
    change.cherry_pick_of_change = 123 as NumericChangeId;
    change.cherry_pick_of_patch_set = 1 as RevisionPatchSetNum;
    element.change = change;
    await element.updateComplete;
    assert.isTrue(element.showCherryPickOf());
  });

  suite('Topic removal', () => {
    let change: ParsedChangeInfo;
    setup(() => {
      change = {
        ...createParsedChange(),
        actions: {
          topic: {enabled: false},
        },
        topic: 'the topic' as TopicName,
        status: ChangeStatus.NEW,
        submit_type: SubmitType.CHERRY_PICK,
        labels: {
          test: {
            all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}],
            default_value: 0,
            values: [] as unknown as LabelValueToDescriptionMap,
          },
        },
        removable_reviewers: [],
      };
    });

    test('computeTopicReadOnly', () => {
      let mutable = false;
      element.mutable = mutable;
      element.change = change;
      assert.isTrue(element.computeTopicReadOnly());
      mutable = true;
      element.mutable = mutable;
      assert.isTrue(element.computeTopicReadOnly());
      change.actions!.topic!.enabled = true;
      element.mutable = mutable;
      element.change = change;
      assert.isFalse(element.computeTopicReadOnly());
      mutable = false;
      element.mutable = mutable;
      assert.isTrue(element.computeTopicReadOnly());
    });

    test('topic read only hides delete button', async () => {
      element.account = createAccountDetailWithId();
      element.change = change;
      await element.updateComplete;
      const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
      const button = queryAndAssert<GrButton>(chip, 'gr-button');
      assert.isTrue(button.hasAttribute('hidden'));
    });

    test('topic not read only does not hide delete button', async () => {
      element.account = createAccountDetailWithId();
      change.actions!.topic!.enabled = true;
      element.change = change;
      await element.updateComplete;
      const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
      const button = queryAndAssert<GrButton>(chip, 'gr-button');
      assert.isFalse(button.hasAttribute('hidden'));
    });
  });

  suite('Hashtag removal', () => {
    let change: ParsedChangeInfo;
    setup(() => {
      change = {
        ...createParsedChange(),
        actions: {
          hashtags: {enabled: false},
        },
        hashtags: ['test-hashtag' as Hashtag],
        labels: {
          test: {
            all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}],
            default_value: 0,
            values: [] as unknown as LabelValueToDescriptionMap,
          },
        },
        removable_reviewers: [],
      };
    });

    test('computeHashtagReadOnly', async () => {
      await element.updateComplete;
      let mutable = false;
      element.change = change;
      element.mutable = mutable;
      await element.updateComplete;
      assert.isTrue(element.computeHashtagReadOnly());
      mutable = true;
      element.change = change;
      element.mutable = mutable;
      await element.updateComplete;
      assert.isTrue(element.computeHashtagReadOnly());
      change.actions!.hashtags!.enabled = true;
      element.change = change;
      element.mutable = mutable;
      await element.updateComplete;
      assert.isFalse(element.computeHashtagReadOnly());
      mutable = false;
      element.change = change;
      element.mutable = mutable;
      await element.updateComplete;
      assert.isTrue(element.computeHashtagReadOnly());
    });

    test('hashtag read only hides delete button', async () => {
      element.account = createAccountDetailWithId();
      element.change = change;
      await element.updateComplete;
      assert.isTrue(element.mutable, 'Mutable');
      assert.isFalse(
        element.change.actions?.hashtags?.enabled,
        'hashtags disabled'
      );
      assert.isTrue(element.hashtagReadOnly, 'hashtag read only');
      const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
      const button = queryAndAssert<GrButton>(chip, 'gr-button');
      assert.isTrue(button.hasAttribute('hidden'), 'button hidden');
    });

    test('hashtag not read only does not hide delete button', async () => {
      await element.updateComplete;
      element.account = createAccountDetailWithId();
      change.actions!.hashtags!.enabled = true;
      element.change = change;
      await element.updateComplete;
      const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
      const button = queryAndAssert<GrButton>(chip, 'gr-button');
      assert.isFalse(button.hasAttribute('hidden'));
    });
  });

  suite('remove reviewer votes', () => {
    setup(async () => {
      sinon.stub(element, 'computeTopicReadOnly').returns(true);
      element.change = {
        ...createParsedChange(),
        topic: 'the topic' as TopicName,
        labels: {
          test: {
            all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}],
            default_value: 0,
            values: [] as unknown as LabelValueToDescriptionMap,
          },
        },
        removable_reviewers: [],
      };
      await element.updateComplete;
    });

    test('changing topic', async () => {
      const newTopic = 'the new topic' as TopicName;
      const setChangeTopicStub = stubRestApi('setChangeTopic').returns(
        Promise.resolve(newTopic)
      );
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);

      element.handleTopicChanged(new CustomEvent('test', {detail: newTopic}));

      assert.isTrue(
        setChangeTopicStub.calledWith(42 as NumericChangeId, newTopic)
      );
      await setChangeTopicStub.lastCall.returnValue;
      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: 'Saving topic and reloading ...',
        showDismiss: true,
      });
    });

    test('topic removal', async () => {
      const newTopic = 'the new topic' as TopicName;
      const setChangeTopicStub = stubRestApi('setChangeTopic').returns(
        Promise.resolve(newTopic)
      );
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      await element.updateComplete;
      const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
      const remove = queryAndAssert<GrButton>(chip, '#remove');

      remove.click();

      assert.isTrue(chip?.disabled);
      assert.isTrue(setChangeTopicStub.calledWith(42 as NumericChangeId));
      await setChangeTopicStub.lastCall.returnValue;
      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: 'Removing topic and reloading ...',
        showDismiss: true,
      });
    });

    test('changing hashtag', async () => {
      await element.updateComplete;
      const newHashtag: Hashtag[] = ['new hashtag' as Hashtag];
      const setChangeHashtagStub = stubRestApi('setChangeHashtag').returns(
        Promise.resolve(newHashtag)
      );
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      element.handleHashtagChanged(
        new CustomEvent('test', {detail: 'new hashtag'})
      );
      assert.isTrue(
        setChangeHashtagStub.calledWith(42 as NumericChangeId, {
          add: ['new hashtag' as Hashtag],
        })
      );
      await setChangeHashtagStub.lastCall.returnValue;
      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: 'Saving hashtag and reloading ...',
        showDismiss: true,
      });
    });
  });

  test('editTopic', async () => {
    element.account = createAccountDetailWithId();
    element.change = {
      ...createParsedChange(),
      actions: {topic: {enabled: true}},
    };
    await element.updateComplete;

    const label = element.shadowRoot!.querySelector(
      '.topicEditableLabel'
    ) as GrEditableLabel;
    assert.ok(label);
    const openStub = sinon.stub(label, 'open');
    element.editTopic();
    await element.updateComplete;

    assert.isTrue(openStub.called);
  });

  suite('plugin endpoints', () => {
    setup(async () => {
      resetPlugins();
      element = await fixture(html`<gr-change-metadata></gr-change-metadata>`);
      element.change = createParsedChange();
      element.revision = createRevision();
      await element.updateComplete;
    });

    teardown(() => {
      resetPlugins();
    });

    test('endpoint params', async () => {
      interface MetadataGrEndpointDecorator extends GrEndpointDecorator {
        plugin: PluginApi;
        change: ParsedChangeInfo;
        revision: RevisionInfo;
      }
      let plugin: PluginApi;
      window.Gerrit.install(
        p => {
          plugin = p;
        },
        '0.1',
        'http://some/plugins/url.js'
      );
      await element.updateComplete;
      const hookEl = (await plugin!
        .hook('change-metadata-item')
        .getLastAttached()) as MetadataGrEndpointDecorator;
      getPluginLoader().loadPlugins([]);
      await element.updateComplete;
      assert.strictEqual(hookEl.plugin, plugin!);
      assert.strictEqual(hookEl.change, element.change);
      assert.strictEqual(hookEl.revision, element.revision);
    });
  });
});
