import fetchMock from 'fetch-mock-jest';

import userEvent from '@testing-library/user-event';
import MockAdapter from 'axios-mock-adapter/types';
import { QueryClient } from '@tanstack/react-query';
import {
  initializeMocks,
  render,
  screen,
  waitFor,
  within,
} from '@src/testUtils';
import { mockContentSearchConfig } from '@src/search-manager/data/api.mock';
import { type ToastActionData } from '@src/generic/toast-context';
import { libraryBlockChangesUrl } from '@src/course-unit/data/api';
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
import { CourseLibraries } from './CourseLibraries';
import {
  mockGetEntityLinks,
  mockGetEntityLinksSummaryByDownstreamContext,
  mockFetchIndexDocuments,
  mockUseLibBlockMetadata,
} from './data/api.mocks';

mockContentSearchConfig.applyMock();
mockGetEntityLinks.applyMock();
mockGetEntityLinksSummaryByDownstreamContext.applyMock();
mockUseLibBlockMetadata.applyMock();

const searchParamsGetMock = jest.fn();
let axiosMock: MockAdapter;
let mockShowToast: (message: string, action?: ToastActionData) => void;
let queryClient: QueryClient;

jest.mock('../studio-home/hooks', () => ({
  useStudioHome: () => ({
    isLoadingPage: false,
    isFailedLoadingPage: false,
    librariesV2Enabled: true,
  }),
}));

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
  useSearchParams: () => [{
    get: searchParamsGetMock,
    getAll: () => [],
  }],
}));

describe('<CourseLibraries />', () => {
  beforeEach(() => {
    initializeMocks();
    fetchMock.mockReset();
    mockFetchIndexDocuments.applyMock();
    localStorage.clear();
    searchParamsGetMock.mockReturnValue('all');
  });

  const renderCourseLibrariesPage = async (courseKey?: string) => {
    const courseId = courseKey || mockGetEntityLinks.courseKey;
    render(
      <CourseAuthoringProvider courseId={courseId}>
        <CourseLibraries />
      </CourseAuthoringProvider>,
    );
  };

  it('shows the spinner before the query is complete', async () => {
    // This mock will never return data (it loads forever):
    await renderCourseLibrariesPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading);
    const spinner = await screen.findByRole('status');
    expect(spinner.textContent).toEqual('Loading...');
  });

  it('shows empty state when no links are present', async () => {
    await renderCourseLibrariesPage(mockGetEntityLinks.courseKeyEmpty);
    const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
    expect(emptyMsg).toBeInTheDocument();
  });

  it('shows alert when out of sync components are present', async () => {
    const user = userEvent.setup();
    await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
    const allTab = await screen.findByRole('tab', { name: 'Libraries' });
    const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' });
    // review tab should be open by default as outOfSyncCount is greater than 0
    expect(reviewTab).toHaveAttribute('aria-selected', 'true');

    await user.click(allTab);
    const alert = await screen.findByRole('alert');
    expect(await within(alert).findByText(
      '7 library components are out of sync. Review updates to accept or ignore changes',
    )).toBeInTheDocument();
    expect(allTab).toHaveAttribute('aria-selected', 'true');

    const reviewBtn = await screen.findByRole('button', { name: 'Review' });
    await user.click(reviewBtn);

    expect(allTab).toHaveAttribute('aria-selected', 'false');
    expect(await screen.findByRole('tab', { name: 'Review Content Updates 7' })).toHaveAttribute('aria-selected', 'true');
    expect(alert).not.toBeInTheDocument();
  });

  it('hide alert on dismiss', async () => {
    const user = userEvent.setup();
    await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
    const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' });
    // review tab should be open by default as outOfSyncCount is greater than 0
    expect(reviewTab).toHaveAttribute('aria-selected', 'true');
    const allTab = await screen.findByRole('tab', { name: 'Libraries' });
    await user.click(allTab);
    expect(allTab).toHaveAttribute('aria-selected', 'true');

    const alert = await screen.findByRole('alert');
    expect(await within(alert).findByText(
      '7 library components are out of sync. Review updates to accept or ignore changes',
    )).toBeInTheDocument();
    const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
    await user.click(dismissBtn);
    expect(allTab).toHaveAttribute('aria-selected', 'true');
    await waitFor(() => expect(alert).not.toBeInTheDocument());
    // review updates button
    const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
    await user.click(reviewActionBtn);
    expect(await screen.findByRole('tab', { name: 'Review Content Updates 7' })).toHaveAttribute('aria-selected', 'true');
  });

  it('show alert if max lastPublishedDate is greated than the local storage value', async () => {
    const user = userEvent.setup();
    const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
    localStorage.setItem(
      `outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
      String(lastPublishedDate.getTime() - 1000),
    );

    await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
    const allTab = await screen.findByRole('tab', { name: 'Libraries' });
    const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' });
    // review tab should be open by default as outOfSyncCount is greater than 0
    expect(reviewTab).toHaveAttribute('aria-selected', 'true');

    await user.click(allTab);
    const alert = await screen.findByRole('alert');
    expect(await within(alert).findByText(
      '7 library components are out of sync. Review updates to accept or ignore changes',
    )).toBeInTheDocument();
  });

  it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
    const user = userEvent.setup();
    const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
    localStorage.setItem(
      `outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
      String(lastPublishedDate.getTime() + 1000),
    );

    await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
    const allTab = await screen.findByRole('tab', { name: 'Libraries' });
    const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' });
    // review tab should be open by default as outOfSyncCount is greater than 0
    expect(reviewTab).toHaveAttribute('aria-selected', 'true');
    await user.click(allTab);
    expect(allTab).toHaveAttribute('aria-selected', 'true');

    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
  });
});

describe('<CourseLibraries ReviewTab />', () => {
  beforeEach(() => {
    const mocks = initializeMocks();
    axiosMock = mocks.axiosMock;
    mockShowToast = mocks.mockShowToast;
    fetchMock.mockReset();
    mockFetchIndexDocuments.applyMock();
    localStorage.clear();
    searchParamsGetMock.mockReturnValue('review');
    queryClient = mocks.queryClient;
  });

  const renderCourseLibrariesReviewPage = async (courseKey?: string) => {
    const courseId = courseKey || mockGetEntityLinks.courseKey;
    render(
      <CourseAuthoringProvider courseId={courseId}>
        <CourseLibraries />
      </CourseAuthoringProvider>,
    );
  };

  it('shows the spinner before the query is complete', async () => {
    // This mock will never return data (it loads forever):
    await renderCourseLibrariesReviewPage(mockGetEntityLinks.courseKeyLoading);
    const spinner = await screen.findByRole('status');
    expect(spinner.textContent).toEqual('Loading...');
  });

  it('shows empty state when no readyToSync links are present', async () => {
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate);
    const emptyMsg = await screen.findByText('All components are up to date');
    expect(emptyMsg).toBeInTheDocument();
  });

  it('shows all readyToSync links', async () => {
    await renderCourseLibrariesReviewPage();
    const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
    expect(updateBtns.length).toEqual(7);
    const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
    expect(ignoreBtns.length).toEqual(7);
  });

  test.each([
    {
      label: 'update changes works with components',
      itemIndex: 0,
      expectedToastMsg: 'Success! "Dropdown" is updated',
    },
    {
      label: 'update changes works with containers',
      itemIndex: 5,
      expectedToastMsg: 'Success! "Unit 1" is updated',
    },
  ])('$label', async ({ itemIndex, expectedToastMsg }) => {
    const user = userEvent.setup();
    const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
    const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey;
    axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
    const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
    expect(updateBtns.length).toEqual(7);
    await user.click(updateBtns[itemIndex]);
    await waitFor(() => {
      expect(axiosMock.history.post.length).toEqual(1);
    });
    expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
    expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg);
    expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] });
  });

  test.each([
    {
      label: 'update changes works in preview modal with components',
      itemIndex: 0,
      expectedToastMsg: 'Success! "Dropdown" is updated',
    },
    {
      label: 'update changes works in preview modal with containers',
      itemIndex: 5,
      expectedToastMsg: 'Success! "Unit 1" is updated',
    },
  ])('$label', async ({ itemIndex, expectedToastMsg }) => {
    const user = userEvent.setup();
    const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
    const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey;
    axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
    const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
    expect(previewBtns.length).toEqual(7);
    await user.click(previewBtns[itemIndex]);
    const dialog = await screen.findByRole('dialog');
    const confirmBtn = await within(dialog).findByRole('button', { name: 'Accept changes' });
    await user.click(confirmBtn);
    await waitFor(() => {
      expect(axiosMock.history.post.length).toEqual(1);
    });
    expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
    expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg);
    expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] });
  });

  test.each([
    {
      label: 'ignore change works with components',
      itemIndex: 0,
      expectedToastMsg: '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.',
    },
    {
      label: 'ignore change works with containers',
      itemIndex: 5,
      expectedToastMsg: '"Unit 1" will remain out of sync with library content. You will be notified when this component is updated again.',
    },
  ])('$label', async ({ itemIndex, expectedToastMsg }) => {
    const user = userEvent.setup();
    const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
    const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey;
    axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
    const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
    expect(ignoreBtns.length).toEqual(7);
    // Show confirmation modal on clicking ignore.
    await user.click(ignoreBtns[itemIndex]);
    const dialog = await screen.findByRole('dialog', { name: 'Ignore these changes?' });
    expect(dialog).toBeInTheDocument();
    const confirmBtn = await within(dialog).findByRole('button', { name: 'Ignore' });
    await user.click(confirmBtn);
    await waitFor(() => {
      expect(axiosMock.history.delete.length).toEqual(1);
    });
    expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
    expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg);
    expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] });
  });

  test.each([
    {
      label: 'ignore change works with components',
      itemIndex: 0,
      expectedToastMsg: '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.',
    },
    {
      label: 'ignore change works with containers',
      itemIndex: 5,
      expectedToastMsg: '"Unit 1" will remain out of sync with library content. You will be notified when this component is updated again.',
    },
  ])('$label', async ({ itemIndex, expectedToastMsg }) => {
    const user = userEvent.setup();
    const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
    const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey;
    axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
    const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
    expect(previewBtns.length).toEqual(7);
    await user.click(previewBtns[itemIndex]);
    const previewDialog = await screen.findByRole('dialog');
    const ignoreBtn = await within(previewDialog).findByRole('button', { name: 'Ignore changes' });
    await user.click(ignoreBtn);
    // Show confirmation modal on clicking ignore.
    const dialog = await screen.findByRole('dialog', { name: 'Ignore these changes?' });
    expect(dialog).toBeInTheDocument();
    const confirmBtn = await within(dialog).findByRole('button', { name: 'Ignore' });
    await user.click(confirmBtn);
    await waitFor(() => {
      expect(axiosMock.history.delete.length).toEqual(1);
    });
    expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
    expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg);
    expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] });
  });

  it('should show sync modal with local changes', async () => {
    const itemIndex = 3;
    const user = userEvent.setup();
    await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
    const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
    expect(previewBtns.length).toEqual(7);
    await user.click(previewBtns[itemIndex]);

    expect(screen.getByText('This library content has local edits.')).toBeInTheDocument();
    expect(screen.getByRole('tab', { name: /course content/i })).toBeInTheDocument();
    expect(screen.getByRole('tab', { name: /published library content/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /update to published library content/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /keep course content/i })).toBeInTheDocument();
  });
});
