import { Router as RemixRouter } from '@remix-run/router/dist/router';
import { screen, waitFor, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

import { CREATE_BLUEPRINT, EDIT_BLUEPRINT } from '../../../../../constants';
import { CreateBlueprintRequest } from '../../../../../store/imageBuilderApi';
import { mockBlueprintIds } from '../../../../fixtures/blueprints';
import {
  expectedAllPackageRecommendations,
  expectedPackages,
  expectedPackagesWithoutRecommendations,
  expectedSinglePackageRecommendation,
  packagesCreateBlueprintRequest,
} from '../../../../fixtures/editMode';
import {
  clickBack,
  clickNext,
  clickReviewAndFinish,
  verifyCancelButton,
} from '../../wizardTestUtils';
import { selectCustomRepo } from '../../wizardTestUtils';
import {
  blueprintRequest,
  clickRegisterLater,
  enterBlueprintName,
  interceptBlueprintRequest,
  interceptEditBlueprintRequest,
  openAndDismissSaveAndBuildModal,
  renderCreateMode,
  renderEditMode,
} from '../../wizardTestUtils';

const router: RemixRouter | undefined = undefined;

const selectGuestImageTarget = async () => {
  const user = userEvent.setup();
  const guestImageCheckBox = await screen.findByRole('checkbox', {
    name: /virtualization guest image checkbox/i,
  });
  await waitFor(() => user.click(guestImageCheckBox));
};

const goToPackagesStep = async () => {
  await selectGuestImageTarget();
  await clickNext(); // Registration
  await clickRegisterLater();
  await clickNext(); // OpenSCAP
  await clickNext(); // File System
  await clickNext(); // Snapshots
  await clickNext(); // Custom repositories
  await clickNext(); // Additional packages
};

const goToReviewStep = async () => {
  await clickNext(); // Users
  await clickNext(); // Timezone
  await clickNext(); // First Boot
  await clickNext(); // Details
  await enterBlueprintName();
  await clickNext(); // Review
};

const typeIntoSearchBox = async (searchTerm: string) => {
  const user = userEvent.setup();
  const searchbox = await screen.findByRole('textbox', {
    name: /search packages/i,
  });
  await waitFor(() => user.type(searchbox, searchTerm));
};

const clearSearchInput = async () => {
  const user = userEvent.setup();
  const clearSearchBtn = await screen.findByRole('button', {
    name: /clear-package-search/i,
  });
  await waitFor(() => user.click(clearSearchBtn));
};

const getAllCheckboxes = async () => {
  const pkgTable = await screen.findByTestId('packages-table');
  await screen.findAllByTestId('package-row');

  const checkboxes = await within(pkgTable).findAllByRole('checkbox', {
    name: /select row/i,
  });

  return checkboxes;
};

const getRows = async () => {
  const packagesTable = await screen.findByTestId('packages-table');
  return await within(packagesTable).findAllByTestId('package-row');
};

const comparePackageSearchResults = async () => {
  const availablePackages = await getRows();
  await waitFor(() => expect(availablePackages).toHaveLength(6));

  expect(availablePackages[0]).toHaveTextContent('test');
  expect(availablePackages[1]).toHaveTextContent('test-sources');
  expect(availablePackages[2]).toHaveTextContent('testPkg');
  expect(availablePackages[3]).toHaveTextContent('testPkg-sources');
  expect(availablePackages[4]).toHaveTextContent('lib-test');
  expect(availablePackages[5]).toHaveTextContent('lib-test-sources');
};

const clickFirstPackageCheckbox = async () => {
  const user = userEvent.setup();
  const row0Checkbox = await screen.findByRole('checkbox', {
    name: /select row 0/i,
  });
  await waitFor(async () => user.click(row0Checkbox));
};

const toggleSelected = async () => {
  const user = userEvent.setup();
  const selected = await screen.findByRole('button', { name: /selected/i });
  await waitFor(async () => user.click(selected));
};

const openIncludedPackagesPopover = async () => {
  const user = userEvent.setup();
  const popoverBtn = await screen.findByRole('button', {
    name: /About included packages/i,
  });
  await waitFor(() => user.click(popoverBtn));
};

const checkRecommendationsEmptyState = async () => {
  await screen.findByRole('button', {
    name: /Recommended Red Hat packages/,
  });

  await screen.findByText('Select packages to generate recommendations.');
};

const addSingleRecommendation = async () => {
  const user = userEvent.setup();
  const addPackageButtons = await screen.findAllByText(/add package/i);
  await waitFor(() => user.click(addPackageButtons[0]));
};

const addAllRecommendations = async () => {
  const user = userEvent.setup();
  const addAllBtn = await screen.findByText(/add all packages/i);
  await waitFor(async () => user.click(addAllBtn));
};

const deselectRecommendation = async () => {
  const user = userEvent.setup();
  const row1Checkbox = await screen.findByRole('checkbox', {
    name: /select row 1/i,
  });
  await waitFor(async () => user.click(row1Checkbox));
};

const clickRevisitButton = async () => {
  const user = userEvent.setup();
  const expandable = await screen.findByTestId('content-expandable');
  const revisitButton = await within(expandable).findByTestId(
    'revisit-custom-repositories'
  );
  await waitFor(() => user.click(revisitButton));
  await waitFor(() =>
    expect(
      screen.queryByRole('button', { name: /Create blueprint/ })
    ).not.toBeInTheDocument()
  );
};

describe('Step Packages', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  const user = userEvent.setup();

  test('clicking Next loads Users', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await clickNext();
    await screen.findByRole('heading', {
      name: 'Users',
    });
  });

  test('clicking Back loads Custom repositories', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await clickBack();
    await screen.findByRole('heading', {
      name: 'Custom repositories',
    });
  });

  test('clicking Cancel loads landing page', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await verifyCancelButton(router);
  });

  test('clicking Review and finish leads to Details', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await clickReviewAndFinish();
    await screen.findByRole('heading', {
      name: 'Details',
    });
  });

  test('should display search bar and toggle buttons', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('test');
    await screen.findByRole('button', {
      name: /Available/,
    });
    await screen.findByRole('button', {
      name: /Selected/,
    });
  });

  test('should display default state', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await screen.findByText(
      'Search above to add additionalpackages to your image.'
    );
  });

  test('should display an exact match if found regardless of too many results', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('testPkg-123');
    await screen.findByTestId('exact-match-row');
    await screen.findByRole('heading', {
      name: /too many results to display/i,
    });
  });

  test('search results should be sorted with most relevant results first', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await selectCustomRepo();
    await typeIntoSearchBox('test');
    await comparePackageSearchResults();
  });

  test('selected packages are sorted the same way as available packages', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await selectCustomRepo();
    await typeIntoSearchBox('test');

    // select all packages
    const checkboxes = await getAllCheckboxes();
    for (const checkbox in checkboxes) {
      user.click(checkboxes[checkbox]);
    }

    await toggleSelected();
    await comparePackageSearchResults();
  });

  test('selected packages persist throughout steps', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('test');

    const checkboxes = await getAllCheckboxes();
    let firstPkgCheckbox = checkboxes[0] as HTMLInputElement;

    expect(firstPkgCheckbox.checked).toEqual(false);
    user.click(firstPkgCheckbox);
    await waitFor(() => expect(firstPkgCheckbox.checked).toEqual(true));
    await clickNext();
    await clickBack();
    firstPkgCheckbox = checkboxes[0] as HTMLInputElement;
    expect(firstPkgCheckbox.checked).toEqual(true);
  });

  test('should display empty available state on failed search', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('asdf');
    await screen.findByText('No results found');
  });

  test('should display too many results state for more than 100 results', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('te');
    await screen.findByText('Too many results to display');
  });

  test('should display too short', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('t');
    await screen.findByText('The search value is too short');
  });

  test('should display relevant results in selected first', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await selectCustomRepo();
    await typeIntoSearchBox('test');

    const checkboxes = await getAllCheckboxes();

    user.click(checkboxes[0]);
    user.click(checkboxes[1]);

    await clearSearchInput();
    await typeIntoSearchBox('mock');
    await screen.findByText(/mockPkg/);

    user.click(checkboxes[0]);
    user.click(checkboxes[1]);

    await toggleSelected();
    await clearSearchInput();
    await typeIntoSearchBox('test');

    const availablePackages = await getRows();
    expect(availablePackages[0]).toHaveTextContent('test');
    expect(availablePackages[1]).toHaveTextContent('test-sources');
  });

  test('should display recommendations', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await checkRecommendationsEmptyState();
    await typeIntoSearchBox('test');
    await clickFirstPackageCheckbox();

    await screen.findByText('recommendedPackage1');
    await screen.findByText('recommendedPackage2');
    await screen.findByText('recommendedPackage3');
  });

  test('allow to add recommendations to selected', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await checkRecommendationsEmptyState();
    await typeIntoSearchBox('test');
    await clickFirstPackageCheckbox();
    await addSingleRecommendation();
    await toggleSelected();

    const pkgTable = await screen.findByTestId('packages-table');
    await within(pkgTable).findByText('recommendedPackage1');
  });

  test('revisit step button on Review works', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('test');
    await clickFirstPackageCheckbox();
    await goToReviewStep();
    await clickRevisitButton();
    await screen.findByRole('heading', { name: /Custom repositories/ });
  });

  describe('Pagination', () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    test('itemcount correct after search', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await selectCustomRepo();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select

      // the pagination in the top right
      const top = await screen.findByTestId('packages-pagination-top');
      expect(top).toHaveTextContent('of 6');
      const bottom = await screen.findByTestId('packages-pagination-bottom');
      expect(bottom).toHaveTextContent('of 6');
    });

    test('itemcount correct after toggling selected', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select
      await toggleSelected();

      // the pagination in the top right
      const top = await screen.findByTestId('packages-pagination-top');
      expect(top).toHaveTextContent('of 1');
      const bottom = await screen.findByTestId('packages-pagination-bottom');
      expect(bottom).toHaveTextContent('of 1');
    });

    test('itemcount correct after clearing search input', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select
      await clearSearchInput();

      // the pagination in the top right
      const top = await screen.findByTestId('packages-pagination-top');
      expect(top).toHaveTextContent('of 0');
      const bottom = await screen.findByTestId('packages-pagination-bottom');
      expect(bottom).toHaveTextContent('of 0');
    });
  });

  describe('Package groups', () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    test('included packages popover', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('@grouper'); // search for '@grouper' package group
      await clickFirstPackageCheckbox(); // select
      await openIncludedPackagesPopover();

      const table = await screen.findByTestId('group-included-packages-table');
      const rows = await within(table).findAllByRole('row');
      expect(rows).toHaveLength(2);

      const firstRowCells = await within(rows[0]).findAllByRole('cell');
      expect(firstRowCells[0]).toHaveTextContent('fish1');
      const secondRowCells = await within(rows[1]).findAllByRole('cell');
      await waitFor(() => expect(secondRowCells[0]).toHaveTextContent('fish2'));
    });
  });
});

describe('Packages request generated correctly', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  test('with custom packages', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('test'); // search for 'test' package
    await clickFirstPackageCheckbox(); // select
    await goToReviewStep();
    // informational modal pops up in the first test only as it's tied
    // to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage
    await openAndDismissSaveAndBuildModal();
    const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

    const expectedRequest: CreateBlueprintRequest = {
      ...blueprintRequest,
      customizations: {
        packages: expectedPackages,
      },
    };

    expect(receivedRequest).toEqual(expectedRequest);
  });

  test('deselecting a package removes it from the request', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('test'); // search for 'test' package
    await clickFirstPackageCheckbox(); // select
    await clickFirstPackageCheckbox(); // deselect
    await goToReviewStep();
    const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

    const expectedRequest = blueprintRequest;

    expect(receivedRequest).toEqual(expectedRequest);
  });

  test('with custom groups', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('@grouper'); // search for '@grouper' package group
    await clickFirstPackageCheckbox(); // select
    await goToReviewStep();

    const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

    const expectedRequest: CreateBlueprintRequest = {
      ...blueprintRequest,
      customizations: {
        packages: ['@grouper'],
      },
    };
    expect(receivedRequest).toEqual(expectedRequest);
  });

  test('deselecting a group removes it from the request', async () => {
    await renderCreateMode();
    await goToPackagesStep();
    await typeIntoSearchBox('@grouper'); // search for '@grouper' package group
    await clickFirstPackageCheckbox(); // select
    await toggleSelected();
    await clickFirstPackageCheckbox(); // deselect
    await goToReviewStep();

    const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
    const expectedRequest = blueprintRequest;
    expect(receivedRequest).toEqual(expectedRequest);
  });

  describe('Package recommendations', () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    test('selecting single recommendation adds it to the request', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select
      await addSingleRecommendation();
      await goToReviewStep();
      const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

      const expectedRequest: CreateBlueprintRequest = {
        ...blueprintRequest,
        customizations: {
          packages: expectedSinglePackageRecommendation,
        },
      };

      expect(receivedRequest).toEqual(expectedRequest);
    });

    test('clicking "Add all packages" adds all recommendations to the request', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select
      await addAllRecommendations();
      await goToReviewStep();
      const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

      const expectedRequest: CreateBlueprintRequest = {
        ...blueprintRequest,
        customizations: {
          packages: expectedAllPackageRecommendations,
        },
      };

      expect(receivedRequest).toEqual(expectedRequest);
    });

    test('deselecting a package recommendation removes it from the request', async () => {
      await renderCreateMode();
      await goToPackagesStep();
      await typeIntoSearchBox('test'); // search for 'test' package
      await clickFirstPackageCheckbox(); // select
      await addSingleRecommendation();
      await toggleSelected();
      await deselectRecommendation();
      await goToReviewStep();
      const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

      const expectedRequest: CreateBlueprintRequest = {
        ...blueprintRequest,
        customizations: {
          packages: expectedPackagesWithoutRecommendations,
        },
      };

      await waitFor(() => {
        expect(receivedRequest).toEqual(expectedRequest);
      });
    });
  });
});

describe('Packages edit mode', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  test('edit mode works', async () => {
    const id = mockBlueprintIds['packages'];
    await renderEditMode(id);

    // starts on review step
    const receivedRequest = await interceptEditBlueprintRequest(
      `${EDIT_BLUEPRINT}/${id}`
    );
    const expectedRequest = packagesCreateBlueprintRequest;
    expect(receivedRequest).toEqual(expectedRequest);
  });
});
