/**
 * @license
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  createAccountWithIdNameAndEmail,
  createChange,
  createGroupInfo,
  createRevisions,
} from '../../test/test-data-generators';
import {
  ChangeInfo,
  NumericChangeId,
  ChangeStatus,
  HttpMethod,
  AccountInfo,
  ReviewerState,
  GroupInfo,
  Hashtag,
} from '../../api/rest-api';
import {BulkActionsModel, LoadingState} from './bulk-actions-model';
import {getAppContext} from '../../services/app-context';
import '../../test/common-test-setup';
import {
  stubRestApi,
  waitEventLoop,
  waitUntilObserved,
} from '../../test/test-utils';
import {mockPromise} from '../../test/test-utils';
import {SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {ReviewInput} from '../../types/common';
import {assert} from '@open-wc/testing';

suite('bulk actions model test', () => {
  let bulkActionsModel: BulkActionsModel;
  setup(() => {
    bulkActionsModel = new BulkActionsModel(getAppContext().restApiService);
  });

  test('does not request detailed changes when no changes are synced', () => {
    const detailedActionsStub = stubRestApi('getDetailedChangesWithActions');

    bulkActionsModel.sync([]);

    assert.isTrue(detailedActionsStub.notCalled);
  });

  test('add changes before sync does not add them', () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;

    assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);

    assert.throws(() => bulkActionsModel.addSelectedChangeNum(c1._number));
    assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);

    bulkActionsModel.sync([c1, c2]);

    bulkActionsModel.addSelectedChangeNum(c2._number);
    assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
      c2._number,
    ]);

    bulkActionsModel.removeSelectedChangeNum(c2._number);
    assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
  });

  test('add and remove selected changes', () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;
    bulkActionsModel.sync([c1, c2]);

    assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
    assert.deepEqual(bulkActionsModel.getState().selectableChangeNums, [1, 2]);

    bulkActionsModel.addSelectedChangeNum(c1._number);
    assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
      c1._number,
    ]);

    bulkActionsModel.addSelectedChangeNum(c2._number);
    assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
      c1._number,
      c2._number,
    ]);

    bulkActionsModel.removeSelectedChangeNum(c1._number);
    assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
      c2._number,
    ]);

    bulkActionsModel.removeSelectedChangeNum(c2._number);
    assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
  });

  test('toggle selected changes', async () => {
    const change1 = createChange();
    change1._number = 1 as NumericChangeId;
    const change2 = createChange();
    change2._number = 2 as NumericChangeId;
    bulkActionsModel.sync([change1, change2]);

    // toggle first change on
    bulkActionsModel.toggleSelectedChangeNum(change1._number);

    let selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      selectedChangeNums => selectedChangeNums.includes(change1._number)
    );
    assert.sameMembers(selectedChangeNums, [change1._number]);

    // toggle second change on
    bulkActionsModel.toggleSelectedChangeNum(change2._number);

    selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      selectedChangeNums => selectedChangeNums.includes(change2._number)
    );
    assert.sameMembers(selectedChangeNums, [change1._number, change2._number]);

    // toggle first change off
    bulkActionsModel.toggleSelectedChangeNum(change1._number);

    selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      selectedChangeNums => !selectedChangeNums.includes(change1._number)
    );
    assert.sameMembers(selectedChangeNums, [change2._number]);
  });

  test('clears selected change numbers', async () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;
    bulkActionsModel.sync([c1, c2]);
    bulkActionsModel.addSelectedChangeNum(c1._number);
    bulkActionsModel.addSelectedChangeNum(c2._number);
    let selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 2
    );
    let totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 2
    );
    assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
    assert.equal(totalChangeCount, 2);

    bulkActionsModel.clearSelectedChangeNums();
    selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 0
    );
    totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 2
    );

    assert.isEmpty(selectedChangeNums);
    assert.equal(totalChangeCount, 2);
  });

  test('selects all changes', async () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;
    bulkActionsModel.sync([c1, c2]);
    let selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 0
    );
    let totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 2
    );
    assert.isEmpty(selectedChangeNums);
    assert.equal(totalChangeCount, 2);

    bulkActionsModel.selectAll();
    selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 2
    );
    totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 2
    );

    assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
    assert.equal(totalChangeCount, 2);
  });

  suite('abandon changes', () => {
    let detailedActionsStub: SinonStubbedMember<
      RestApiService['getDetailedChangesWithActions']
    >;
    setup(async () => {
      detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
      const c1 = createChange();
      c1._number = 1 as NumericChangeId;
      const c2 = createChange();
      c2._number = 2 as NumericChangeId;

      detailedActionsStub.returns(
        Promise.resolve([
          {...c1, actions: {abandon: {method: HttpMethod.POST}}},
          {...c2, status: ChangeStatus.ABANDONED},
        ])
      );

      bulkActionsModel.sync([c1, c2]);

      bulkActionsModel.addSelectedChangeNum(c1._number);
      bulkActionsModel.addSelectedChangeNum(c2._number);
    });

    test('already abandoned change does not call executeChangeAction', () => {
      const actionStub = stubRestApi('executeChangeAction');
      bulkActionsModel.abandonChanges();
      assert.equal(actionStub.callCount, 1);
      assert.deepEqual(actionStub.lastCall.args.slice(0, 5), [
        1 as NumericChangeId,
        HttpMethod.POST,
        '/abandon',
        undefined,
        {message: ''},
      ]);
    });
  });

  suite('add reviewers', () => {
    const accounts: AccountInfo[] = [
      createAccountWithIdNameAndEmail(0),
      createAccountWithIdNameAndEmail(1),
    ];
    const groups: GroupInfo[] = [createGroupInfo('groupId')];
    const changes: ChangeInfo[] = [
      {
        ...createChange(),
        _number: 1 as NumericChangeId,
        subject: 'Subject 1',
        reviewers: {
          REVIEWER: [accounts[0]],
          CC: [accounts[1]],
        },
      },
      {
        ...createChange(),
        _number: 2 as NumericChangeId,
        subject: 'Subject 2',
      },
    ];
    let saveChangeReviewStub: sinon.SinonStub;

    setup(async () => {
      saveChangeReviewStub = stubRestApi('saveChangeReview').resolves(
        new Response()
      );
      stubRestApi('getDetailedChangesWithActions').resolves([
        {...changes[0], actions: {abandon: {method: HttpMethod.POST}}},
        {...changes[1], status: ChangeStatus.ABANDONED},
      ]);
      bulkActionsModel.sync(changes);
      bulkActionsModel.addSelectedChangeNum(changes[0]._number);
      bulkActionsModel.addSelectedChangeNum(changes[1]._number);
    });

    test('adds reviewers/cc only to changes that need it', async () => {
      bulkActionsModel.addReviewers(
        new Map([
          [ReviewerState.REVIEWER, [accounts[0], groups[0]]],
          [ReviewerState.CC, [accounts[1]]],
        ]),
        '<GERRIT_ACCOUNT_12345> replied on the change'
      );

      assert.isTrue(saveChangeReviewStub.calledTwice);
      // changes[0] only adds the group since it already has the other
      // reviewer/CCs
      assert.sameDeepOrderedMembers(saveChangeReviewStub.firstCall.args, [
        changes[0]._number,
        'current',
        {
          reviewers: [{reviewer: groups[0].id, state: ReviewerState.REVIEWER}],
          ignore_automatic_attention_set_rules: true,
          add_to_attention_set: [
            {
              reason: '<GERRIT_ACCOUNT_12345> replied on the change',
              user: groups[0].id,
            },
          ],
        },
      ]);
      assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
        changes[1]._number,
        'current',
        {
          reviewers: [
            {reviewer: accounts[0]._account_id, state: ReviewerState.REVIEWER},
            {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
            {reviewer: accounts[1]._account_id, state: ReviewerState.CC},
          ],
          ignore_automatic_attention_set_rules: true,
          add_to_attention_set: [
            {
              reason: '<GERRIT_ACCOUNT_12345> replied on the change',
              user: accounts[0]._account_id,
            },
            {
              reason: '<GERRIT_ACCOUNT_12345> replied on the change',
              user: groups[0].id,
            },
          ],
        },
      ]);
    });
  });

  suite('voteChanges', () => {
    let detailedActionsStub: SinonStubbedMember<
      RestApiService['getDetailedChangesWithActions']
    >;
    setup(async () => {
      const c1 = {...createChange(), revisions: createRevisions(10)};
      c1._number = 1 as NumericChangeId;
      const c2 = {...createChange(), revisions: createRevisions(4)};
      c2._number = 2 as NumericChangeId;

      detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
      detailedActionsStub.returns(
        Promise.resolve([
          {...c1, actions: {abandon: {method: HttpMethod.POST}}},
          {...c2, status: ChangeStatus.ABANDONED},
        ])
      );

      await bulkActionsModel.sync([c1, c2]);

      bulkActionsModel.addSelectedChangeNum(c1._number);
      bulkActionsModel.addSelectedChangeNum(c2._number);
    });

    test('vote changes', () => {
      const reviewStub = stubRestApi('saveChangeReview');
      const reviewInput: ReviewInput = {
        labels: {
          a: 1,
        },
      };
      bulkActionsModel.voteChanges(reviewInput);
      assert.equal(reviewStub.callCount, 2);
      assert.deepEqual(reviewStub.firstCall.args.slice(0, 3), [
        1 as NumericChangeId,
        'current',
        {
          labels: {
            a: 1,
          },
        },
      ]);

      assert.deepEqual(reviewStub.secondCall.args.slice(0, 3), [
        2 as NumericChangeId,
        'current',
        {
          labels: {
            a: 1,
          },
        },
      ]);
    });
  });

  suite('add hashtags', () => {
    const change1: ChangeInfo = {
      ...createChange(),
      _number: 1 as NumericChangeId,
      hashtags: ['existingHashtag' as Hashtag],
    };
    const change2: ChangeInfo = {
      ...createChange(),
      _number: 2 as NumericChangeId,
      hashtags: ['existingHashtag' as Hashtag],
    };
    const existingHashtag = 'existingHashtag' as Hashtag;
    const newHashtag = 'newHashtag' as Hashtag;
    let detailedActionsStub: SinonStubbedMember<
      RestApiService['getDetailedChangesWithActions']
    >;
    setup(async () => {
      detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
      detailedActionsStub.returns(Promise.resolve([change1, change2]));

      await bulkActionsModel.sync([change1, change2]);
      bulkActionsModel.addSelectedChangeNum(change1._number);
      bulkActionsModel.addSelectedChangeNum(change2._number);
      stubRestApi('setChangeHashtag').resolves([existingHashtag, newHashtag]);
    });

    test('server-acked hashtags are added to the model', async () => {
      await Promise.all(bulkActionsModel.addHashtags([newHashtag]));

      const updatedChanges = await waitUntilObserved(
        bulkActionsModel.selectedChanges$,
        changes => changes.some(change => change.hashtags?.includes(newHashtag))
      );

      assert.deepEqual(updatedChanges, [
        {...change1, hashtags: [existingHashtag, newHashtag]},
        {...change2, hashtags: [existingHashtag, newHashtag]},
      ]);
    });
  });

  test('stale changes are removed from the model', async () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;
    bulkActionsModel.sync([c1, c2]);

    bulkActionsModel.addSelectedChangeNum(c1._number);
    bulkActionsModel.addSelectedChangeNum(c2._number);

    let selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 2
    );
    let totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 2
    );

    assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
    assert.equal(totalChangeCount, 2);

    bulkActionsModel.sync([c1]);
    selectedChangeNums = await waitUntilObserved(
      bulkActionsModel.selectedChangeNums$,
      s => s.length === 1
    );
    totalChangeCount = await waitUntilObserved(
      bulkActionsModel.totalChangeCount$,
      totalChangeCount => totalChangeCount === 1
    );

    assert.sameMembers(selectedChangeNums, [c1._number]);
    assert.equal(totalChangeCount, 1);
  });

  test('sync fetches new changes', async () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;

    assert.equal(
      bulkActionsModel.getState().loadingState,
      LoadingState.NOT_SYNCED
    );

    bulkActionsModel.sync([c1, c2]);
    await waitUntilObserved(
      bulkActionsModel.loadingState$,
      s => s === LoadingState.LOADING
    );

    await waitUntilObserved(
      bulkActionsModel.loadingState$,
      s => s === LoadingState.LOADED
    );
    const model = bulkActionsModel.getState();

    assert.strictEqual(
      model.allChanges.get(1 as NumericChangeId)?.subject,
      'Subject 1'
    );
    assert.strictEqual(
      model.allChanges.get(2 as NumericChangeId)?.subject,
      'Subject 2'
    );
  });

  test('sync ignores outdated fetch responses', async () => {
    const c1 = createChange();
    c1._number = 1 as NumericChangeId;
    const c2 = createChange();
    c2._number = 2 as NumericChangeId;

    const responsePromise1 = mockPromise<ChangeInfo[]>();
    let promise = responsePromise1;
    const getChangesStub = stubRestApi(
      'getDetailedChangesWithActions'
    ).callsFake(() => promise);
    bulkActionsModel.sync([c1]);
    assert.strictEqual(getChangesStub.callCount, 1);
    await waitUntilObserved(
      bulkActionsModel.loadingState$,
      s => s === LoadingState.LOADING
    );
    const responsePromise2 = mockPromise<ChangeInfo[]>();

    promise = responsePromise2;
    bulkActionsModel.sync([c1, c2]);
    assert.strictEqual(getChangesStub.callCount, 2);

    responsePromise2.resolve([
      {...createChange(), _number: 1, subject: 'Subject 1'},
      {...createChange(), _number: 2, subject: 'Subject 2'},
    ] as ChangeInfo[]);

    await waitUntilObserved(
      bulkActionsModel.loadingState$,
      s => s === LoadingState.LOADED
    );
    const model = bulkActionsModel.getState();
    assert.strictEqual(
      model.allChanges.get(1 as NumericChangeId)?.subject,
      'Subject 1'
    );
    assert.strictEqual(
      model.allChanges.get(2 as NumericChangeId)?.subject,
      'Subject 2'
    );

    // Resolve the old promise.
    responsePromise1.resolve([
      {...createChange(), _number: 1, subject: 'Subject 1-old'},
    ] as ChangeInfo[]);
    await waitEventLoop();
    const model2 = bulkActionsModel.getState();

    // No change should happen.
    assert.strictEqual(
      model2.allChanges.get(1 as NumericChangeId)?.subject,
      'Subject 1'
    );
    assert.strictEqual(
      model2.allChanges.get(2 as NumericChangeId)?.subject,
      'Subject 2'
    );
  });
});
