import { getByTestId, fireEvent } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import DiffRow from '~/diffs/components/diff_row.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import diffsModule from '~/diffs/store/modules';
import { findInteropAttributes } from '../find_interop_attributes';
import { getDiffFileMock } from '../mock_data/diff_file';

const showCommentForm = jest.fn();
const enterdragging = jest.fn();
const stopdragging = jest.fn();
const setHighlightedRow = jest.fn();
let wrapper;

describe('DiffRow', () => {
  const testLines = [
    {
      left: { old_line: 1, discussions: [] },
      right: { new_line: 1, discussions: [] },
      hasDiscussionsLeft: true,
      hasDiscussionsRight: true,
    },
    {
      left: {},
      right: {},
      isMatchLineLeft: true,
      isMatchLineRight: true,
    },
    {},
    {
      left: { old_line: 1, discussions: [] },
      right: { new_line: 1, discussions: [] },
    },
  ];

  const createWrapper = ({ props, state = {}, actions, isLoggedIn = true }) => {
    Vue.use(Vuex);

    const diffs = diffsModule();
    diffs.state = { ...diffs.state, ...state };
    diffs.actions = { ...diffs.actions, ...actions };

    const getters = { isLoggedIn: () => isLoggedIn };

    const store = new Vuex.Store({
      modules: { diffs },
      getters,
    });

    window.gon = { current_user_id: isLoggedIn ? 1 : 0 };
    const coverageFileData = state.coverageFiles?.files ? state.coverageFiles.files : {};

    const propsData = {
      fileHash: 'abc',
      filePath: 'abc',
      line: {},
      index: 0,
      isHighlighted: false,
      fileLineCoverage: (file, line) => {
        const hits = coverageFileData[file]?.[line];
        if (hits) {
          return { text: `Test coverage: ${hits} hits`, class: 'coverage' };
        } else if (hits === 0) {
          return { text: 'No test coverage', class: 'no-coverage' };
        }

        return {};
      },
      ...props,
    };

    const provide = {
      glFeatures: { dragCommentSelection: true },
    };

    return shallowMount(DiffRow, {
      propsData,
      store,
      provide,
      listeners: {
        enterdragging,
        stopdragging,
        setHighlightedRow,
        showCommentForm,
      },
    });
  };

  afterEach(() => {
    wrapper.destroy();
    wrapper = null;

    window.gon = {};
    showCommentForm.mockReset();
    enterdragging.mockReset();
    stopdragging.mockReset();
    setHighlightedRow.mockReset();

    Object.values(DiffRow).forEach(({ cache }) => {
      if (cache) {
        cache.clear();
      }
    });
  });

  const getCommentButton = (side) => wrapper.find(`[data-testid="${side}-comment-button"]`);

  describe.each`
    side
    ${'left'}
    ${'right'}
  `('$side side', ({ side }) => {
    it(`renders empty cells if ${side} is unavailable`, () => {
      wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
      expect(wrapper.find(`[data-testid="${side}-line-number"]`).exists()).toBe(false);
      expect(wrapper.find(`[data-testid="${side}-empty-cell"]`).exists()).toBe(true);
    });

    describe('comment button', () => {
      let line;

      beforeEach(() => {
        // https://eslint.org/docs/rules/prefer-destructuring#when-not-to-use-it
        // eslint-disable-next-line prefer-destructuring
        line = testLines[3];
      });

      it('renders', () => {
        wrapper = createWrapper({ props: { line, inline: false } });
        expect(getCommentButton(side).exists()).toBe(true);
      });

      it('responds to click and keyboard events', async () => {
        wrapper = createWrapper({
          props: { line, inline: false },
        });
        const commentButton = getCommentButton(side);

        await commentButton.trigger('click');
        await commentButton.trigger('keydown.enter');
        await commentButton.trigger('keydown.space');

        expect(showCommentForm).toHaveBeenCalledTimes(3);
      });

      it('ignores click and keyboard events when comments are disabled', async () => {
        line[side].commentsDisabled = true;
        wrapper = createWrapper({
          props: { line, inline: false },
        });
        const commentButton = getCommentButton(side);

        await commentButton.trigger('click');
        await commentButton.trigger('keydown.enter');
        await commentButton.trigger('keydown.space');

        expect(showCommentForm).not.toHaveBeenCalled();
      });
    });

    it('renders avatars', () => {
      wrapper = createWrapper({ props: { line: testLines[0], inline: false } });

      expect(wrapper.find(`[data-testid="${side}-discussions"]`).exists()).toBe(true);
    });
  });

  it('renders left line numbers', () => {
    wrapper = createWrapper({ props: { line: testLines[0] } });
    const lineNumber = testLines[0].left.old_line;
    expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
  });

  it('renders right line numbers', () => {
    wrapper = createWrapper({ props: { line: testLines[0] } });
    const lineNumber = testLines[0].right.new_line;
    expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
  });

  describe('drag operations', () => {
    let line;

    beforeEach(() => {
      line = { ...testLines[0] };
    });

    it.each`
      side
      ${'left'}
      ${'right'}
    `('emits `enterdragging` onDragEnter $side side', ({ side }) => {
      wrapper = createWrapper({ props: { line } });
      fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));

      expect(enterdragging).toHaveBeenCalledWith({ ...line[side], index: 0 });
    });

    it.each`
      side
      ${'left'}
      ${'right'}
    `('emits `stopdragging` onDrop $side side', ({ side }) => {
      wrapper = createWrapper({ props: { line } });
      fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));

      expect(stopdragging).toHaveBeenCalled();
    });
  });

  describe('sets coverage title and class', () => {
    const diffFileMockData = getDiffFileMock();
    const thisLine = diffFileMockData.parallel_diff_lines[2];
    const rightLine = diffFileMockData.parallel_diff_lines[2].right;

    const mockDiffContent = {
      diffFile: diffFileMockData,
      shouldRenderDraftRow: jest.fn(),
      hasParallelDraftLeft: jest.fn(),
      hasParallelDraftRight: jest.fn(),
      draftsForLine: jest.fn().mockReturnValue([]),
    };

    const applyMap = mapParallel(mockDiffContent);
    const props = {
      line: applyMap(thisLine),
      fileHash: diffFileMockData.file_hash,
      filePath: diffFileMockData.file_path,
      contextLinesPath: 'contextLinesPath',
      isHighlighted: false,
    };
    const name = diffFileMockData.file_path;
    const line = rightLine.new_line;

    it('for lines with coverage', () => {
      const coverageFiles = { files: { [name]: { [line]: 5 } } };
      wrapper = createWrapper({ props, state: { coverageFiles } });
      const coverage = wrapper.find('.line-coverage.right-side');

      expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
      expect(coverage.classes('coverage')).toBe(true);
    });

    it('for lines without coverage', () => {
      const coverageFiles = { files: { [name]: { [line]: 0 } } };
      wrapper = createWrapper({ props, state: { coverageFiles } });
      const coverage = wrapper.find('.line-coverage.right-side');

      expect(coverage.attributes('title')).toContain('No test coverage');
      expect(coverage.classes('no-coverage')).toBe(true);
    });

    it('for unknown lines', () => {
      const coverageFiles = {};
      wrapper = createWrapper({ props, state: { coverageFiles } });
      const coverage = wrapper.find('.line-coverage.right-side');

      expect(coverage.attributes('title')).toBeUndefined();
      expect(coverage.classes('coverage')).toBe(false);
      expect(coverage.classes('no-coverage')).toBe(false);
    });
  });

  describe('interoperability', () => {
    it.each`
      desc                                 | line                                                   | inline   | leftSide                                                  | rightSide
      ${'with inline and new_line'}        | ${{ left: { old_line: 3, new_line: 5, type: 'new' } }} | ${true}  | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }} | ${null}
      ${'with inline and no new_line'}     | ${{ left: { old_line: 3, type: 'old' } }}              | ${true}  | ${{ type: 'old', line: '3', oldLine: '3' }}               | ${null}
      ${'with parallel and no right side'} | ${{ left: { old_line: 3, new_line: 5 } }}              | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }}               | ${null}
      ${'with parallel and no left side'}  | ${{ right: { old_line: 3, new_line: 5 } }}             | ${false} | ${null}                                                   | ${{ type: 'new', line: '5', newLine: '5' }}
      ${'with parallel and right side'}    | ${{ left: { old_line: 3 }, right: { new_line: 5 } }}   | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }}               | ${{ type: 'new', line: '5', newLine: '5' }}
    `('$desc, sets interop data attributes', ({ line, inline, leftSide, rightSide }) => {
      wrapper = createWrapper({ props: { line, inline } });

      expect(findInteropAttributes(wrapper, '[data-testid="left-side"]')).toEqual(leftSide);
      expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
    });
  });
});

describe('coverage state memoization', () => {
  it('updates when coverage is loaded', () => {
    const lineWithoutCoverage = {};
    const lineWithCoverage = {
      text: 'Test coverage: 5 hits',
      class: 'coverage',
    };

    const unchangedProps = {
      inline: true,
      filePath: 'file/path',
      line: { left: { new_line: 3 } },
    };

    const noCoverageProps = {
      fileLineCoverage: () => lineWithoutCoverage,
      coverageLoaded: false,
      ...unchangedProps,
    };
    const coverageProps = {
      fileLineCoverage: () => lineWithCoverage,
      coverageLoaded: true,
      ...unchangedProps,
    };

    // this caches no coverage for the line
    expect(DiffRow.coverageStateLeft(noCoverageProps)).toStrictEqual(lineWithoutCoverage);

    // this retrieves coverage for the line because it has been recached
    expect(DiffRow.coverageStateLeft(coverageProps)).toStrictEqual(lineWithCoverage);
  });
});