import React from 'react';
import {
  cleanup,
  fireEvent,
  render,
  RenderResult,
  waitFor,
  waitForElementToBeRemoved,
} from '@testing-library/react';
import startCase from 'lodash/startCase';

import {
  constructProjectURL,
  dataFixtures,
  hostDefaults,
  urlFixtures,
} from '../data';
import {
  defaultMongoNavContext,
  MongoNavContextProps,
  MongoNavProvider,
} from '../MongoNavContext';
import { ActiveNavElement, CurrentProjectInterface, Mode } from '../types';

import ProjectNav from './ProjectNav';
import { ProjectNavProps } from './ProjectNav.types';

const productToTestId: Record<string, string> = {
  atlas: 'project-nav-data-services',
  cloudManager: 'project-nav-cloud-manager',
  charts: 'project-nav-charts',
  realm: 'project-nav-app-services',
};

// data
const { currentOrganization, currentProject, projects } = dataFixtures;
const {
  projectNav: { alerts, activityFeed, invite },
} = urlFixtures;

// this avoids having to explicitly type orgNav with nullable fields
// and then extend it to allow string indexes
const linkNamesToUrls: Record<string, string | undefined> = {
  alerts,
  activityFeed,
  invite,
};

const linkNamesToTestIds: Record<string, string> = {
  alerts: 'project-nav-alerts',
  activityFeed: 'project-nav-activity-feed',
  invite: 'project-nav-invite',
};

describe('packages/mongo-nav/src/project-nav', () => {
  let onProjectChange: jest.Mock;

  let fetchMock: jest.Mock;
  const originalFetch: typeof window.fetch = window.fetch;

  let fetchData: Array<object>;

  beforeEach(() => {
    onProjectChange = jest.fn();
    fetchMock = jest.fn();
    fetchData = [];
    fetchMock.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(fetchData),
    });
    window.fetch = fetchMock;
  });

  afterEach(() => {
    window.fetch = originalFetch;
    jest.restoreAllMocks();
    cleanup();
  });

  function renderComponent(
    props?: Partial<ProjectNavProps>,
    withContext?: Partial<MongoNavContextProps>,
  ): RenderResult {
    const context: MongoNavContextProps = {
      ...defaultMongoNavContext,
      currentOrg: currentOrganization,
      currentProject: currentProject,
      activeNav: ActiveNavElement.OrgNavOrgSettings,
      activeProduct: 'cloud',
      ...withContext,
    };

    return render(
      <MongoNavProvider context={context}>
        <ProjectNav
          data={projects}
          constructProjectURL={constructProjectURL}
          urls={urlFixtures}
          environment="commercial"
          hosts={hostDefaults()}
          onProjectChange={onProjectChange}
          mode={Mode.Production}
          {...props}
        />
      </MongoNavProvider>,
    );
  }

  describe('when rendered with default props', () => {
    let queryByTestId: RenderResult['queryByTestId'];
    let getByTestId: RenderResult['getByTestId'];
    beforeEach(async () => {
      fetchData = [];
      ({ queryByTestId, getByTestId } = renderComponent(
        {},
        { currentProject },
      ));
    });

    Object.keys(productToTestId).forEach(product => {
      const isVisible = product !== 'cloudManager';
      it(`${
        isVisible ? 'displays' : 'does not display'
      } the ${product} in the nav`, () => {
        const foundProduct = queryByTestId(productToTestId[product]);

        if (isVisible) {
          expect(foundProduct).toBeVisible();
        } else {
          expect(foundProduct).toBeNull();
        }
      });
    });

    Object.keys(linkNamesToUrls).forEach(linkName => {
      it(`displays the ${startCase(linkName)} iconButton`, () => {
        const navLink = queryByTestId(linkNamesToTestIds[linkName]);

        expect(navLink).toBeVisible();

        expect((navLink as HTMLAnchorElement).href).toEqual(
          linkNamesToUrls[linkName],
        );
      });
    });

    it(`does not display the project status badge in the nav`, () => {
      const statusBadge = queryByTestId('project-nav-project-status-badge');
      expect(statusBadge).not.toBeInTheDocument();
    });

    it('atlas tab shows the correct link', () => {
      expect(
        getByTestId(productToTestId['atlas'])
          .closest('a')
          ?.getAttribute('href'),
      ).toEqual('https://cloud.mongodb.com/v2/fakeProjectId1#');
    });

    it('does not show alerts badge', () => {
      expect(queryByTestId('project-nav-alerts-badge')).toBeNull();
    });

    it('has a `product` data attribute on the tab', () => {
      // This is to ensure there's no layout shift when hovering tabs
      expect(getByTestId(productToTestId['atlas'])).toHaveAttribute(
        'data-product',
        'Data Services',
      );
    });
  });

  describe('when the current organization uses Cloud Manager', () => {
    const cloudManagerProject = projects[1];
    let queryByTestId: RenderResult['queryByTestId'];
    beforeEach(() => {
      ({ queryByTestId } = renderComponent(
        {},
        { currentProject: cloudManagerProject as CurrentProjectInterface },
      ));
    });

    Object.keys(productToTestId).forEach(product => {
      const isVisible = product === 'cloudManager';
      it(`${
        isVisible ? 'displays' : 'does not display'
      } the ${product} in the nav`, () => {
        const foundProduct = queryByTestId(productToTestId[product]);

        if (isVisible) {
          expect(foundProduct).toBeVisible();
        } else {
          expect(foundProduct).toBeNull();
        }
      });
    });

    Object.keys(linkNamesToUrls).forEach(linkName => {
      it(`displays the ${startCase(linkName)} iconButton`, () => {
        const navLink = queryByTestId(linkNamesToTestIds[linkName]);
        expect(navLink).toBeVisible();
        expect((navLink as HTMLAnchorElement).href).toEqual(
          linkNamesToUrls[linkName],
        );
      });
    });
  });

  describe('when the environment is "government"', () => {
    test('Atlas is the only product tab displayed', () => {
      const { getByTestId, queryByTestId } = renderComponent({
        environment: 'government',
      });
      expect(getByTestId(productToTestId['atlas'])).toBeVisible();
      expect(queryByTestId('project-nav-charts')).toBeNull();
      expect(queryByTestId('project-nav-realm')).toBeNull();
    });
  });

  describe('when useCNRegionsOnly is "true"', () => {
    let getByTestId: RenderResult['getByTestId'];
    let queryByTestId: RenderResult['queryByTestId'];
    beforeEach(async () => {
      const useCNRegionsOnlyProject = {
        ...currentProject,
        useCNRegionsOnly: true,
      };
      fetchData = [{}, {}];
      ({ getByTestId, queryByTestId } = renderComponent(
        {},
        { currentProject: useCNRegionsOnlyProject as CurrentProjectInterface },
      ));
    });

    it('atlas tab shows the correct link', () => {
      expect(getByTestId(productToTestId['atlas'])).toBeVisible();
      expect(
        getByTestId(productToTestId['atlas'])
          .closest('a')
          ?.getAttribute('href'),
      ).toEqual('https://cloud.mongodb.com/v2/fakeProjectId1#');
    });

    it('App Services tab shows no link', () => {
      expect(getByTestId(productToTestId['realm'])).toBeVisible();
      expect(
        getByTestId(productToTestId['realm']).getAttribute('href'),
      ).toBeNull();
    });

    it('hovering over App Services tab shows a tooltip', async () => {
      fireEvent.mouseEnter(getByTestId(productToTestId['realm']));
      await waitFor(() => getByTestId('project-nav-tooltip-app-services'), {
        timeout: 500,
      });

      expect(
        getByTestId('project-nav-tooltip-app-services'),
      ).toBeInTheDocument();

      fireEvent.mouseLeave(getByTestId(productToTestId['realm']));
      await waitForElementToBeRemoved(
        getByTestId('project-nav-tooltip-app-services'),
        { timeout: 500 },
      );

      expect(
        queryByTestId('project-nav-tooltip-app-services'),
      ).not.toBeInTheDocument();
    });

    it('charts tab shows no link', () => {
      expect(getByTestId(productToTestId['charts'])).toBeVisible();
      expect(
        getByTestId(productToTestId['charts']).getAttribute('href'),
      ).toBeNull();
    });

    it('hovering over charts tab shows a tooltip', async () => {
      fireEvent.mouseEnter(getByTestId(productToTestId['charts']));
      await waitFor(() => getByTestId('project-nav-tooltip-charts'), {
        timeout: 500,
      });

      expect(getByTestId('project-nav-tooltip-charts')).toBeInTheDocument();

      fireEvent.mouseLeave(getByTestId(productToTestId['charts']));
      await waitForElementToBeRemoved(
        getByTestId('project-nav-tooltip-charts'),
        { timeout: 500 },
      );

      expect(
        queryByTestId('project-nav-tooltip-charts'),
      ).not.toBeInTheDocument();
    });
  });

  describe('alerts polling behavior', () => {
    describe('when it should poll', () => {
      let getByTestId: RenderResult['getByTestId'];
      beforeEach(() => {
        fetchData = [{}, {}];
        ({ getByTestId } = renderComponent({}, { currentProject }));
      });

      it('calls fetch with correct arguments', () => {
        expect(fetchMock).toHaveBeenCalledWith(
          'https://cloud.mongodb.com/user/shared/alerts/project/fakeProjectId1',
          { credentials: 'include', method: 'GET', mode: 'cors' },
        );
      });

      it('displays expected alerts badge', () => {
        expect(getByTestId('project-nav-alerts-badge').innerHTML).toContain(
          '2',
        );
      });
    });

    describe('when in dev mode', () => {
      let getByTestId: RenderResult['getByTestId'];
      beforeEach(() => {
        fetchData = [{}, {}];
        ({ getByTestId } = renderComponent(
          { mode: Mode.Dev },
          { currentProject },
        ));
      });

      it('does not call fetch', () => {
        expect(fetchMock).not.toHaveBeenCalled();
      });

      it('displays value from currentProject', () => {
        expect(getByTestId('project-nav-alerts-badge').innerHTML).toContain(
          '1',
        );
      });
    });
  });

  describe('when rendered without a current project', () => {
    let getByTestId: RenderResult['getByTestId'];
    beforeEach(
      () =>
        ({ getByTestId } = renderComponent({}, { currentProject: undefined })),
    );

    test('atlas tab shows the correct link', () => {
      expect(
        getByTestId(productToTestId['atlas'])
          .closest('a')
          ?.getAttribute('href'),
      ).toEqual('https://cloud.mongodb.com');
    });
  });

  describe('when admin is set to true', () => {
    it(`displays the project status badge in the nav`, () => {
      const { queryByTestId } = renderComponent(
        { admin: true },
        { currentProject },
      );
      const statusBadge = queryByTestId('project-nav-project-status-badge');
      expect(statusBadge).toBeInTheDocument();
      expect(statusBadge!.textContent).toEqual('ACTIVE');
    });

    it('displays the provided displayName in the project status badge', () => {
      const { queryByTestId } = renderComponent(
        {
          admin: true,
        },
        {
          currentProject: {
            ...currentProject,
            status: 'DEAD',
            statusDisplayName: 'INACTIVE',
          } as CurrentProjectInterface,
        },
      );
      const statusBadge = queryByTestId('project-nav-project-status-badge');
      expect(statusBadge).toBeInTheDocument();
      expect(statusBadge!.textContent).toEqual('INACTIVE');
    });

    it('displays the project status badge when no displayName is provided', () => {
      const { queryByTestId } = renderComponent(
        {
          admin: true,
        },
        {
          currentProject: {
            ...currentProject,
            statusDisplayName: undefined,
          } as CurrentProjectInterface,
        },
      );
      const statusBadge = queryByTestId('project-nav-project-status-badge');
      expect(statusBadge).toBeInTheDocument();
      expect(statusBadge!.textContent).toEqual(currentProject!.status);
    });
  });

  describe('accessibility props are properly set on menu', () => {
    let getByTestId: RenderResult['getByTestId'];
    beforeEach(() => ({ getByTestId } = renderComponent()));

    test('"aria-expanded" is set to false when menu is closed', () => {
      expect(
        getByTestId('project-nav-project-menu').getAttribute('aria-expanded'),
      ).toBe('false');
    });

    test('"aria-expanded" is set to true when menu is open', () => {
      const menu = getByTestId('project-nav-project-menu');
      fireEvent.click(menu);
      waitFor(() => {
        expect(menu.getAttribute('aria-expanded')).toBe('true');
      });
    });
  });
});
