/**
 * @license
 * Copyright 2021 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {fixture, html, assert} from '@open-wc/testing';
import {SinonStubbedMember} from 'sinon';
import {PluginApi} from '../../../api/plugin';
import {ChangeStatus} from '../../../constants/constants';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import '../../../test/common-test-setup';
import {
  createChange,
  createCommitInfoWithRequiredCommit,
  createParsedChange,
  createRelatedChangeAndCommitInfo,
  createRelatedChangesInfo,
  createRevision,
  createSubmittedTogetherInfo,
} from '../../../test/test-data-generators';
import {
  query,
  queryAndAssert,
  resetPlugins,
  stubRestApi,
  waitEventLoop,
} from '../../../test/test-utils';
import {
  ChangeId,
  ChangeInfo,
  CommitId,
  NumericChangeId,
  PatchSetNumber,
  RelatedChangeAndCommitInfo,
  RelatedChangesInfo,
  SubmittedTogetherInfo,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
import {getChangeNumber} from '../../../utils/change-util';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import './gr-related-changes-list';
import {
  ChangeMarkersInList,
  GrRelatedChangesList,
  Section,
} from './gr-related-changes-list';
import {GrRelatedCollapse} from './gr-related-collapse';

suite('gr-related-changes-list', () => {
  let element: GrRelatedChangesList;

  setup(async () => {
    element = await fixture(
      html`<gr-related-changes-list></gr-related-changes-list>`
    );
  });

  suite('show when collapsed', () => {
    function genBoolArray(
      instructions: Array<{
        len: number;
        v: boolean;
      }>
    ) {
      return instructions.flatMap(inst =>
        Array.from({length: inst.len}, () => inst.v)
      );
    }

    function checkShowWhenCollapsed(
      expected: boolean[],
      markersPredicate: (index: number) => ChangeMarkersInList,
      msg: string
    ) {
      for (let i = 0; i < expected.length; i++) {
        assert.equal(
          markersPredicate(i).showWhenCollapsed,
          expected[i],
          `change on pos (${i}) ${msg}`
        );
      }
    }

    test('size 5', () => {
      const markersPredicate = element.markersPredicateFactory(10, 4, 5);
      const expectedCollapsing = genBoolArray([
        {len: 2, v: false},
        {len: 5, v: true},
        {len: 3, v: false},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing,
        markersPredicate,
        'highlight 4, size 10, size 5'
      );

      const markersPredicate2 = element.markersPredicateFactory(10, 8, 5);
      const expectedCollapsing2 = genBoolArray([
        {len: 5, v: false},
        {len: 5, v: true},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing2,
        markersPredicate2,
        'highlight 8, size 10, size 5'
      );

      const markersPredicate3 = element.markersPredicateFactory(10, 1, 5);
      const expectedCollapsing3 = genBoolArray([
        {len: 5, v: true},
        {len: 5, v: false},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing3,
        markersPredicate3,
        'highlight 1, size 10, size 5'
      );
    });

    test('size 4', () => {
      const markersPredicate = element.markersPredicateFactory(10, 4, 4);
      const expectedCollapsing = genBoolArray([
        {len: 2, v: false},
        {len: 4, v: true},
        {len: 4, v: false},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing,
        markersPredicate,
        'highlight 4, len 10, size 4'
      );

      const markersPredicate2 = element.markersPredicateFactory(10, 8, 4);
      const expectedCollapsing2 = genBoolArray([
        {len: 6, v: false},
        {len: 4, v: true},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing2,
        markersPredicate2,
        'highlight 8, len 10, size 4'
      );

      const markersPredicate3 = element.markersPredicateFactory(10, 1, 4);
      const expectedCollapsing3 = genBoolArray([
        {len: 4, v: true},
        {len: 6, v: false},
      ]);
      checkShowWhenCollapsed(
        expectedCollapsing3,
        markersPredicate3,
        'highlight 1, len 10, size 4'
      );
    });
  });

  suite('section size', () => {
    test('1 section', () => {
      const sectionSize = element.sectionSizeFactory(20, 0, 0, 0, 0);
      assert.equal(sectionSize(Section.RELATED_CHANGES), 15);
      const sectionSize2 = element.sectionSizeFactory(0, 0, 10, 0, 0);
      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
    });
    test('2 sections', () => {
      const sectionSize = element.sectionSizeFactory(20, 20, 0, 0, 0);
      assert.equal(sectionSize(Section.RELATED_CHANGES), 11);
      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
      const sectionSize2 = element.sectionSizeFactory(4, 0, 10, 0, 0);
      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
    });
    test('many sections', () => {
      const sectionSize = element.sectionSizeFactory(20, 20, 3, 3, 3);
      assert.equal(sectionSize(Section.RELATED_CHANGES), 3);
      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
      const sectionSize2 = element.sectionSizeFactory(4, 1, 10, 1, 1);
      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
      assert.equal(sectionSize2(Section.SAME_TOPIC), 4);
    });
  });

  suite('test first non-empty list', () => {
    const relatedChangeInfo: RelatedChangesInfo = {
      ...createRelatedChangesInfo(),
      changes: [createRelatedChangeAndCommitInfo()],
    };
    const submittedTogether: SubmittedTogetherInfo = {
      ...createSubmittedTogetherInfo(),
      changes: [createChange()],
    };

    setup(() => {
      element.change = createParsedChange();
      element.latestPatchNum = 1 as PatchSetNumber;
    });

    test('render', async () => {
      stubRestApi('getRelatedChanges').returns(
        Promise.resolve(relatedChangeInfo)
      );
      stubRestApi('getChangesSubmittedTogether').returns(
        Promise.resolve(submittedTogether)
      );
      stubRestApi('getChangeCherryPicks').returns(
        Promise.resolve([createChange()])
      );
      await element.reload();

      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <gr-endpoint-decorator name="related-changes-section">
            <gr-endpoint-param name="change"> </gr-endpoint-param>
            <gr-endpoint-slot name="top"> </gr-endpoint-slot>
            <section id="relatedChanges">
              <gr-related-collapse class="first" title="Relation chain">
                <div class="relatedChangeLine show-when-collapsed">
                  <span class="marker space"> </span>
                  <gr-related-change
                    show-change-status=""
                    show-submittable-check=""
                  >
                    Test commit subject
                  </gr-related-change>
                </div>
              </gr-related-collapse>
            </section>
            <section id="submittedTogether">
              <gr-related-collapse title="Submitted together">
                <div class="relatedChangeLine show-when-collapsed">
                  <span
                    aria-label="Arrow marking current change"
                    class="arrowToCurrentChange marker"
                    role="img"
                  >
                    ➔
                  </span>
                  <gr-related-change show-submittable-check="">
                    Test subject
                  </gr-related-change>
                  <span class="repo" title="test-project">test-project</span>
                  <span class="branch">&nbsp;|&nbsp;test-branch&nbsp;</span>
                </div>
              </gr-related-collapse>
              <div class="note" hidden="">(+ )</div>
            </section>
            <section id="cherryPicks">
              <gr-related-collapse title="Cherry picks">
                <div class="relatedChangeLine show-when-collapsed">
                  <span class="marker space"> </span>
                  <gr-related-change>
                    test-branch: Test subject
                  </gr-related-change>
                </div>
              </gr-related-collapse>
            </section>
            <gr-endpoint-slot name="bottom"> </gr-endpoint-slot>
          </gr-endpoint-decorator>
        `
      );
    });

    test('first list', async () => {
      stubRestApi('getRelatedChanges').returns(
        Promise.resolve(relatedChangeInfo)
      );
      await element.reload();
      const section = queryAndAssert<HTMLElement>(element, '#relatedChanges');
      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
        section,
        'gr-related-collapse'
      );
      assert.isTrue(relatedChanges.classList.contains('first'));
    });

    test('first empty second non-empty', async () => {
      stubRestApi('getRelatedChanges').returns(
        Promise.resolve(createRelatedChangesInfo())
      );
      stubRestApi('getChangesSubmittedTogether').returns(
        Promise.resolve(submittedTogether)
      );
      await element.reload();
      const relatedChanges = query<HTMLElement>(element, '#relatedChanges');
      assert.notExists(relatedChanges);
      const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
        queryAndAssert<HTMLElement>(element, '#submittedTogether'),
        'gr-related-collapse'
      );
      assert.isTrue(submittedTogetherSection.classList.contains('first'));
    });

    test('first non-empty second empty third non-empty', async () => {
      stubRestApi('getRelatedChanges').returns(
        Promise.resolve(relatedChangeInfo)
      );
      stubRestApi('getChangesSubmittedTogether').returns(
        Promise.resolve(createSubmittedTogetherInfo())
      );
      stubRestApi('getChangeCherryPicks').returns(
        Promise.resolve([createChange()])
      );
      await element.reload();

      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
        queryAndAssert<HTMLElement>(element, '#relatedChanges'),
        'gr-related-collapse'
      );
      assert.isTrue(relatedChanges.classList.contains('first'));
      const submittedTogetherSection = query<HTMLElement>(
        element,
        '#submittedTogether'
      );
      assert.notExists(submittedTogetherSection);
      const cherryPicks = queryAndAssert<GrRelatedCollapse>(
        queryAndAssert<HTMLElement>(element, '#cherryPicks'),
        'gr-related-collapse'
      );
      assert.isFalse(cherryPicks.classList.contains('first'));
    });
  });

  test('_changesEqual', () => {
    const change1: ChangeInfo = {
      ...createChange(),
      change_id: '123' as ChangeId,
      _number: 0 as NumericChangeId,
    };
    const change2: ChangeInfo = {
      ...createChange(),
      change_id: '456' as ChangeId,
      _number: 1 as NumericChangeId,
    };
    const change3: ChangeInfo = {
      ...createChange(),
      change_id: '123' as ChangeId,
      _number: 2 as NumericChangeId,
    };
    const change4: RelatedChangeAndCommitInfo = {
      ...createRelatedChangeAndCommitInfo(),
      change_id: '123' as ChangeId,
      _change_number: 1 as NumericChangeId,
    };

    assert.isTrue(element._changesEqual(change1, change1));
    assert.isFalse(element._changesEqual(change1, change2));
    assert.isFalse(element._changesEqual(change1, change3));
    assert.isTrue(element._changesEqual(change2, change4));
  });

  test('_getChangeNumber', () => {
    const change1: ChangeInfo = {
      ...createChange(),
      change_id: '123' as ChangeId,
      _number: 0 as NumericChangeId,
    };
    const change2: ChangeInfo = {
      ...createChange(),
      change_id: '456' as ChangeId,
      _number: 1 as NumericChangeId,
    };
    assert.equal(getChangeNumber(change1), 0);
    assert.equal(getChangeNumber(change2), 1);
  });

  suite('get conflicts tests', () => {
    let element: GrRelatedChangesList;
    let conflictsStub: SinonStubbedMember<RestApiService['getChangeConflicts']>;

    setup(async () => {
      element = await fixture(
        html`<gr-related-changes-list></gr-related-changes-list>`
      );
      conflictsStub = stubRestApi('getChangeConflicts').returns(
        Promise.resolve(undefined)
      );
    });

    test('request conflicts if open and mergeable', () => {
      element.latestPatchNum = 7 as PatchSetNumber;
      element.change = {
        ...createParsedChange(),
        change_id: '123' as ChangeId,
        status: ChangeStatus.NEW,
      };
      element.mergeable = true;
      element.reload();
      assert.isTrue(conflictsStub.called);
    });

    test('does not request conflicts if closed and mergeable', () => {
      element.latestPatchNum = 7 as PatchSetNumber;
      element.change = {
        ...createParsedChange(),
        change_id: '123' as ChangeId,
        status: ChangeStatus.NEW,
      };
      element.reload();
      assert.isFalse(conflictsStub.called);
    });

    test('does not request conflicts if open and not mergeable', () => {
      element.latestPatchNum = 7 as PatchSetNumber;
      element.change = {
        ...createParsedChange(),
        change_id: '123' as ChangeId,
        status: ChangeStatus.NEW,
      };
      element.mergeable = false;
      element.reload();
      assert.isFalse(conflictsStub.called);
    });

    test('doesnt request conflicts if closed and not mergeable', () => {
      element.latestPatchNum = 7 as PatchSetNumber;
      element.change = {
        ...createParsedChange(),
        change_id: '123' as ChangeId,
        status: ChangeStatus.NEW,
      };
      element.mergeable = false;
      element.reload();
      assert.isFalse(conflictsStub.called);
    });
  });

  test('connected revisions', () => {
    const change: ParsedChangeInfo = {
      ...createParsedChange(),
      revisions: {
        e3c6d60783bfdec9ebae7dcfec4662360433449e: createRevision(1),
        '26e5e4c9c7ae31cbd876271cca281ce22b413997': createRevision(2),
        bf7884d695296ca0c91702ba3e2bc8df0f69a907: createRevision(7),
        b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3: createRevision(5),
        d6bcee67570859ccb684873a85cf50b1f0e96fda: createRevision(6),
        cc960918a7f90388f4a9e05753d0f7b90ad44546: createRevision(3),
        '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': createRevision(4),
      },
    };
    let latestPatchNum = 7 as PatchSetNumber;
    let relatedChanges: RelatedChangeAndCommitInfo[] = [
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
          ),
          parents: [
            {
              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
              subject: 'subject1',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
          ),
          parents: [
            {
              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
              subject: 'subject2',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
          ),
          parents: [
            {
              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
              subject: 'subject3',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            'b0ccb183494a8e340b8725a2dc553967d61e6dae'
          ),
          parents: [
            {
              commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' as CommitId,
              subject: 'subject4',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
          ),
          parents: [
            {
              commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' as CommitId,
              subject: 'subject5',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '613bc4f81741a559c6667ac08d71dcc3348f73ce'
          ),
          parents: [
            {
              commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' as CommitId,
              subject: 'subject6',
            },
          ],
        },
      },
    ];

    let connectedChanges = element._computeConnectedRevisions(
      change,
      latestPatchNum,
      relatedChanges
    );
    assert.deepEqual(connectedChanges, [
      '613bc4f81741a559c6667ac08d71dcc3348f73ce',
      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
      'b0ccb183494a8e340b8725a2dc553967d61e6dae',
      '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
      '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
      '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
    ]);

    latestPatchNum = 4 as PatchSetNumber;
    relatedChanges = [
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
          ),
          parents: [
            {
              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
          ),
          parents: [
            {
              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
          ),
          parents: [
            {
              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b'
          ),
          parents: [
            {
              commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
          ),
          parents: [
            {
              commit: 'af815dac54318826b7f1fa468acc76349ffc588e' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
      {
        ...createRelatedChangeAndCommitInfo(),
        commit: {
          ...createCommitInfoWithRequiredCommit(
            'af815dac54318826b7f1fa468acc76349ffc588e'
          ),
          parents: [
            {
              commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' as CommitId,
              subject: 'My parent commit',
            },
          ],
        },
      },
    ];

    connectedChanges = element._computeConnectedRevisions(
      change,
      latestPatchNum,
      relatedChanges
    );
    assert.deepEqual(connectedChanges, [
      'af815dac54318826b7f1fa468acc76349ffc588e',
      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
      'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
    ]);
  });

  suite('gr-related-changes-list plugin tests', () => {
    let element: GrRelatedChangesList;

    setup(async () => {
      resetPlugins();
      element = await fixture(
        html`<gr-related-changes-list></gr-related-changes-list>`
      );
    });

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

    test('endpoint params', async () => {
      element.change = {...createParsedChange(), labels: {}};
      interface RelatedChangesListGrEndpointDecorator
        extends GrEndpointDecorator {
        plugin: PluginApi;
        change: ParsedChangeInfo;
      }
      let hookEl: RelatedChangesListGrEndpointDecorator;
      let plugin: PluginApi;
      window.Gerrit.install(
        p => {
          plugin = p;
          plugin
            .hook('related-changes-section')
            .getLastAttached()
            .then(el => (hookEl = el as RelatedChangesListGrEndpointDecorator));
        },
        '0.1',
        'http://some/plugins/url1.js'
      );
      getPluginLoader().loadPlugins([]);
      await waitEventLoop();
      assert.strictEqual(hookEl!.plugin, plugin!);
      assert.strictEqual(hookEl!.change, element.change);
    });
  });
});
