/**
 * @license
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {fixture, html, assert} from '@open-wc/testing';
import {IronDropdownElement} from '@polymer/iron-dropdown';
import {SinonStubbedMember} from 'sinon';
import {
  BulkActionsModel,
  bulkActionsModelToken,
} from '../../../models/bulk-actions/bulk-actions-model';
import {wrapInProvider} from '../../../models/di-provider-element';
import {getAppContext} from '../../../services/app-context';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import '../../../test/common-test-setup';
import {createChange} from '../../../test/test-data-generators';
import {
  MockPromise,
  mockPromise,
  query,
  queryAll,
  queryAndAssert,
  stubReporting,
  stubRestApi,
  waitEventLoop,
  waitUntil,
  waitUntilCalled,
  waitUntilObserved,
} from '../../../test/test-utils';
import {ChangeInfo, NumericChangeId, TopicName} from '../../../types/common';
import {EventType} from '../../../types/events';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-topic-flow';
import type {GrChangeListTopicFlow} from './gr-change-list-topic-flow';

suite('gr-change-list-topic-flow tests', () => {
  let element: GrChangeListTopicFlow;
  let model: BulkActionsModel;
  let reportingStub: SinonStubbedMember<ReportingService['reportInteraction']>;

  setup(() => {
    reportingStub = stubReporting('reportInteraction');
  });

  async function selectChange(change: ChangeInfo) {
    model.addSelectedChangeNum(change._number);
    await waitUntilObserved(model.selectedChanges$, selected =>
      selected.some(other => other._number === change._number)
    );
    await element.updateComplete;
  }

  async function deselectChange(change: ChangeInfo) {
    model.removeSelectedChangeNum(change._number);
    await waitUntilObserved(
      model.selectedChanges$,
      selected => !selected.some(other => other._number === change._number)
    );
    await element.updateComplete;
  }

  suite('dropdown closed', () => {
    const changes: ChangeInfo[] = [
      {
        ...createChange(),
        _number: 1 as NumericChangeId,
        subject: 'Subject 1',
      },
      {
        ...createChange(),
        _number: 2 as NumericChangeId,
        subject: 'Subject 2',
      },
    ];

    setup(async () => {
      stubRestApi('getDetailedChangesWithActions').resolves(changes);
      model = new BulkActionsModel(getAppContext().restApiService);
      model.sync(changes);

      element = (
        await fixture(
          wrapInProvider(
            html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
            bulkActionsModelToken,
            model
          )
        )
      ).querySelector('gr-change-list-topic-flow')!;
      await selectChange(changes[0]);
      await selectChange(changes[1]);
      await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
      await element.updateComplete;
    });

    test('skips dropdown render when closed', async () => {
      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <gr-button
            id="start-flow"
            flatten=""
            down-arrow=""
            aria-disabled="false"
            role="button"
            tabindex="0"
            >Topic</gr-button
          >
          <iron-dropdown
            aria-disabled="false"
            aria-hidden="true"
            style="outline: none; display: none;"
            vertical-align="auto"
            horizontal-align="auto"
          >
          </iron-dropdown>
        `
      );
    });

    test('dropdown hidden before flow button clicked', async () => {
      const dropdown = queryAndAssert<IronDropdownElement>(
        element,
        'iron-dropdown'
      );
      assert.isFalse(dropdown.opened);
    });

    test('flow button click shows dropdown', async () => {
      const button = queryAndAssert<GrButton>(element, 'gr-button#start-flow');

      button.click();
      await element.updateComplete;

      const dropdown = queryAndAssert<IronDropdownElement>(
        element,
        'iron-dropdown'
      );
      assert.isTrue(dropdown.opened);
    });

    test('flow button click when open hides dropdown', async () => {
      queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
      await waitUntil(() =>
        Boolean(
          queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
        )
      );
      queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
      await waitUntil(
        () =>
          !queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
      );
    });
  });

  suite('changes in existing topics', () => {
    const changesWithTopics: ChangeInfo[] = [
      {
        ...createChange(),
        _number: 1 as NumericChangeId,
        subject: 'Subject 1',
        topic: 'topic1' as TopicName,
      },
      {
        ...createChange(),
        _number: 2 as NumericChangeId,
        subject: 'Subject 2',
        topic: 'topic2' as TopicName,
      },
    ];
    let setChangeTopicPromises: MockPromise<string>[];
    let setChangeTopicStub: sinon.SinonStub;

    async function resolvePromises() {
      setChangeTopicPromises[0].resolve('foo');
      setChangeTopicPromises[1].resolve('foo');
      await element.updateComplete;
    }

    async function rejectPromises() {
      setChangeTopicPromises[0].reject(new Error('error'));
      setChangeTopicPromises[1].reject(new Error('error'));
      await element.updateComplete;
    }

    setup(async () => {
      stubRestApi('getDetailedChangesWithActions').resolves(changesWithTopics);
      setChangeTopicPromises = [];
      setChangeTopicStub = stubRestApi('setChangeTopic');
      for (let i = 0; i < changesWithTopics.length; i++) {
        const promise = mockPromise<string>();
        setChangeTopicPromises.push(promise);
        setChangeTopicStub
          .withArgs(changesWithTopics[i]._number, sinon.match.any)
          .returns(promise);
      }
      model = new BulkActionsModel(getAppContext().restApiService);
      model.sync(changesWithTopics);

      element = (
        await fixture(
          wrapInProvider(
            html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
            bulkActionsModelToken,
            model
          )
        )
      ).querySelector('gr-change-list-topic-flow')!;

      // select changes
      await selectChange(changesWithTopics[0]);
      await selectChange(changesWithTopics[1]);
      await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
      await element.updateComplete;

      // open flow
      queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
      await element.updateComplete;
      await waitEventLoop();
    });

    test('renders existing-topics flow', () => {
      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <gr-button
            id="start-flow"
            flatten=""
            down-arrow=""
            aria-disabled="false"
            role="button"
            tabindex="0"
            >Topic</gr-button
          >
          <iron-dropdown
            aria-disabled="false"
            vertical-align="auto"
            horizontal-align="auto"
          >
            <div slot="dropdown-content">
              <div class="chips">
                <button
                  role="listbox"
                  aria-label="topic1 selection"
                  class="chip"
                >
                  topic1
                </button>
                <button
                  role="listbox"
                  aria-label="topic2 selection"
                  class="chip"
                >
                  topic2
                </button>
              </div>
              <div class="footer">
                <div class="loadingOrError" role="progressbar"></div>
                <div class="buttons">
                  <gr-button
                    id="apply-to-all-button"
                    flatten=""
                    aria-disabled="true"
                    disabled=""
                    role="button"
                    tabindex="-1"
                    >Apply to all</gr-button
                  >
                  <gr-button
                    id="remove-topics-button"
                    flatten=""
                    aria-disabled="true"
                    disabled=""
                    role="button"
                    tabindex="-1"
                    >Remove</gr-button
                  >
                </div>
              </div>
            </div>
          </iron-dropdown>
        `,
        {
          // iron-dropdown sizing seems to vary between local & CI
          ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
        }
      );
    });

    test('apply all button is disabled if all changes have the same topic', async () => {
      assert.isTrue(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );

      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      await element.updateComplete;

      assert.isFalse(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );

      await deselectChange(changesWithTopics[1]);

      const allChanges = model.getState().allChanges;
      const change2 = {
        ...createChange(),
        _number: 2 as NumericChangeId,
        subject: 'Subject 2',
        topic: 'topic1' as TopicName, // same as changesWithTopics[0]
      };
      allChanges.set(2 as NumericChangeId, change2);
      model.setState({
        ...model.getState(),
        allChanges,
      });

      await selectChange(change2);

      assert.isTrue(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );
    });

    test('remove single topic', async () => {
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      await element.updateComplete;
      queryAndAssert<GrButton>(element, '#remove-topics-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Removing topic...'
      );

      await resolvePromises();
      await element.updateComplete;

      // not called for second change which as a different topic
      assert.isTrue(setChangeTopicStub.calledOnce);
      assert.deepEqual(setChangeTopicStub.firstCall.args, [
        changesWithTopics[0]._number,
        '',
      ]);

      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: 'topic1 removed from changes',
        showDismiss: true,
      });
      assert.deepEqual(reportingStub.lastCall.args[1], {
        type: 'removing-topic',
        selectedChangeCount: 2,
      });
    });

    test('remove multiple topics', async () => {
      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      queryAll<HTMLButtonElement>(element, 'button.chip')[1].click();
      await element.updateComplete;
      queryAndAssert<GrButton>(element, '#remove-topics-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Removing topics...'
      );

      await resolvePromises();
      await element.updateComplete;

      // not called for second change which as a different topic
      assert.isTrue(setChangeTopicStub.calledTwice);
      assert.deepEqual(setChangeTopicStub.firstCall.args, [
        changesWithTopics[0]._number,
        '',
      ]);
      assert.deepEqual(setChangeTopicStub.secondCall.args, [
        changesWithTopics[1]._number,
        '',
      ]);
    });

    test('shows error when remove topic fails', async () => {
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      await element.updateComplete;
      queryAndAssert<GrButton>(element, '#remove-topics-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Removing topic...'
      );

      await rejectPromises();
      await element.updateComplete;

      await waitUntil(() => query(element, '.error') !== undefined);
      assert.equal(
        queryAndAssert(element, '.error').textContent,
        'Failed to remove topic'
      );
      assert.equal(
        queryAndAssert(element, 'gr-button#cancel-button').textContent,
        'Cancel'
      );
      assert.isUndefined(query(element, '.loadingText'));
    });

    test('can only apply a single topic', async () => {
      assert.isTrue(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );

      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      await element.updateComplete;

      assert.isFalse(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );

      queryAll<HTMLButtonElement>(element, 'button.chip')[1].click();
      await element.updateComplete;

      assert.isTrue(
        queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
      );
    });

    test('applies topic to all changes', async () => {
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);

      queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
      await element.updateComplete;

      queryAndAssert<GrButton>(element, '#apply-to-all-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Applying to all'
      );

      await resolvePromises();
      await element.updateComplete;

      assert.isTrue(setChangeTopicStub.calledTwice);
      assert.deepEqual(setChangeTopicStub.firstCall.args, [
        changesWithTopics[0]._number,
        'topic1',
      ]);
      assert.deepEqual(setChangeTopicStub.secondCall.args, [
        changesWithTopics[1]._number,
        'topic1',
      ]);

      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: 'topic1 applied to all changes',
        showDismiss: true,
      });
      assert.deepEqual(reportingStub.lastCall.args[1], {
        type: 'apply-topic-to-all',
        selectedChangeCount: 2,
      });
    });
  });

  suite('change have no existing topics', () => {
    const changesWithNoTopics: ChangeInfo[] = [
      {
        ...createChange(),
        _number: 1 as NumericChangeId,
        subject: 'Subject 1',
      },
      {
        ...createChange(),
        _number: 2 as NumericChangeId,
        subject: 'Subject 2',
      },
    ];
    let setChangeTopicPromises: MockPromise<string>[];
    let setChangeTopicStub: sinon.SinonStub;

    async function resolvePromises() {
      setChangeTopicPromises[0].resolve('foo');
      setChangeTopicPromises[1].resolve('foo');
      await element.updateComplete;
    }

    async function rejectPromises() {
      setChangeTopicPromises[0].reject(new Error('error'));
      setChangeTopicPromises[1].reject(new Error('error'));
      await element.updateComplete;
    }

    setup(async () => {
      stubRestApi('getDetailedChangesWithActions').resolves(
        changesWithNoTopics
      );
      setChangeTopicPromises = [];
      setChangeTopicStub = stubRestApi('setChangeTopic');
      for (let i = 0; i < changesWithNoTopics.length; i++) {
        const promise = mockPromise<string>();
        setChangeTopicPromises.push(promise);
        setChangeTopicStub
          .withArgs(changesWithNoTopics[i]._number, sinon.match.any)
          .returns(promise);
      }

      model = new BulkActionsModel(getAppContext().restApiService);
      model.sync(changesWithNoTopics);

      element = (
        await fixture(
          wrapInProvider(
            html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
            bulkActionsModelToken,
            model
          )
        )
      ).querySelector('gr-change-list-topic-flow')!;

      // select changes
      await selectChange(changesWithNoTopics[0]);
      await selectChange(changesWithNoTopics[1]);
      await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
      await element.updateComplete;

      // open flow
      queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
      await element.updateComplete;
      await waitEventLoop();
    });

    test('renders no-existing-topics flow', () => {
      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <gr-button
            id="start-flow"
            flatten=""
            down-arrow=""
            aria-disabled="false"
            role="button"
            tabindex="0"
            >Topic</gr-button
          >
          <iron-dropdown
            aria-disabled="false"
            vertical-align="auto"
            horizontal-align="auto"
          >
            <div slot="dropdown-content">
              <gr-autocomplete
                placeholder="Type topic name to create or filter topics"
                show-blue-focus-border=""
              ></gr-autocomplete>
              <div class="footer">
                <div class="loadingOrError" role="progressbar"></div>
                <div class="buttons">
                  <gr-button
                    id="set-topic-button"
                    flatten=""
                    aria-disabled="true"
                    disabled=""
                    role="button"
                    tabindex="-1"
                    >Set Topic</gr-button
                  >
                </div>
              </div>
            </div>
          </iron-dropdown>
        `,
        {
          // iron-dropdown sizing seems to vary between local & CI
          ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
        }
      );
    });

    test('create new topic', async () => {
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves(
        []
      );
      const autocomplete = queryAndAssert<GrAutocomplete>(
        element,
        'gr-autocomplete'
      );
      autocomplete.setFocus(true);
      autocomplete.text = 'foo';
      await element.updateComplete;
      await waitUntilCalled(getTopicsStub, 'getTopicsStub');
      assert.isFalse(
        queryAndAssert<GrButton>(element, '#set-topic-button').disabled
      );

      queryAndAssert<GrButton>(element, '#set-topic-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Setting topic...'
      );

      await resolvePromises();
      await element.updateComplete;

      assert.isTrue(setChangeTopicStub.calledTwice);
      assert.deepEqual(setChangeTopicStub.firstCall.args, [
        changesWithNoTopics[0]._number,
        'foo',
      ]);
      assert.deepEqual(setChangeTopicStub.secondCall.args, [
        changesWithNoTopics[1]._number,
        'foo',
      ]);

      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: '2 Changes added to foo',
        showDismiss: true,
      });
      assert.deepEqual(reportingStub.lastCall.args[1], {
        type: 'add-topic',
        selectedChangeCount: 2,
      });
    });

    test('shows error when create topic fails', async () => {
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves(
        []
      );
      const autocomplete = queryAndAssert<GrAutocomplete>(
        element,
        'gr-autocomplete'
      );
      autocomplete.setFocus(true);
      autocomplete.text = 'foo';
      await element.updateComplete;
      await waitUntilCalled(getTopicsStub, 'getTopicsStub');
      assert.isFalse(
        queryAndAssert<GrButton>(element, '#set-topic-button').disabled
      );
      queryAndAssert<GrButton>(element, '#set-topic-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Setting topic...'
      );

      await rejectPromises();
      await element.updateComplete;
      await waitUntil(() => query(element, '.error') !== undefined);

      assert.equal(
        queryAndAssert(element, '.error').textContent,
        'Failed to set topic'
      );
      assert.equal(
        queryAndAssert(element, 'gr-button#cancel-button').textContent,
        'Cancel'
      );
      assert.isUndefined(query(element, '.loadingText'));
    });

    test('apply topic', async () => {
      const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves([
        {...createChange(), topic: 'foo' as TopicName},
      ]);
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      const autocomplete = queryAndAssert<GrAutocomplete>(
        element,
        'gr-autocomplete'
      );

      autocomplete.setFocus(true);
      autocomplete.text = 'foo';
      await element.updateComplete;
      await waitUntilCalled(getTopicsStub, 'getTopicsStub');

      assert.isFalse(
        queryAndAssert<GrButton>(element, '#set-topic-button').disabled
      );
      queryAndAssert<GrButton>(element, '#set-topic-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Setting topic...'
      );

      await resolvePromises();

      assert.isTrue(setChangeTopicStub.calledTwice);
      assert.deepEqual(setChangeTopicStub.firstCall.args, [
        changesWithNoTopics[0]._number,
        'foo',
      ]);
      assert.deepEqual(setChangeTopicStub.secondCall.args, [
        changesWithNoTopics[1]._number,
        'foo',
      ]);

      await waitUntilCalled(alertStub, 'alertStub');
      assert.deepEqual(alertStub.lastCall.args[0].detail, {
        message: '2 Changes added to foo',
        showDismiss: true,
      });
      assert.deepEqual(reportingStub.lastCall.args[1], {
        type: 'add-topic',
        selectedChangeCount: 2,
      });
    });

    test('shows error when setting topic fails', async () => {
      const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves([
        {...createChange(), topic: 'foo' as TopicName},
      ]);
      const alertStub = sinon.stub();
      element.addEventListener(EventType.SHOW_ALERT, alertStub);
      const autocomplete = queryAndAssert<GrAutocomplete>(
        element,
        'gr-autocomplete'
      );

      autocomplete.setFocus(true);
      autocomplete.text = 'foo';
      await element.updateComplete;
      await waitUntilCalled(getTopicsStub, 'getTopicsStub');
      assert.isFalse(
        queryAndAssert<GrButton>(element, '#set-topic-button').disabled
      );
      queryAndAssert<GrButton>(element, '#set-topic-button').click();
      await element.updateComplete;

      assert.equal(
        queryAndAssert(element, '.loadingText').textContent,
        'Setting topic...'
      );

      await rejectPromises();
      await element.updateComplete;

      await waitUntil(() => query(element, '.error') !== undefined);
      assert.equal(
        queryAndAssert(element, '.error').textContent,
        'Failed to set topic'
      );
      assert.equal(
        queryAndAssert(element, 'gr-button#cancel-button').textContent,
        'Cancel'
      );
      assert.isUndefined(query(element, '.loadingText'));
    });
  });
});
