// The loader listens to raw DOM anchors, so the fixtures below are plain <a>
// elements on purpose — next/link would hide exactly what is under test.
/* eslint-disable @next/next/no-html-link-for-pages */
import React from 'react';
import {render, screen, fireEvent, act} from '@testing-library/react';
import NavigationLoader, {
  NAVIGATION_START_EVENT,
  NAVIGATION_TIMEOUT_MS,
  startNavigationLoader,
} from '../NavigationLoader';

let mockPathname = '/';
jest.mock('next/navigation', () => ({
  usePathname: () => mockPathname,
}));

type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  [dataAttribute: `data-${string}`]: string;
};

// jsdom cannot navigate; the anchors would log "Not implemented: navigation".
// The capture-phase listener under test runs before this bubble handler.
const swallowNavigation = (event: React.MouseEvent<HTMLAnchorElement>) => event.preventDefault();

const Fixture = ({linkProps = {}}: {linkProps?: LinkProps}) => (
  <>
    <NavigationLoader />
    <a href="/fr/evenements" onClick={swallowNavigation} {...linkProps}>
      Agenda
    </a>
    <button type="button">Bouton</button>
  </>
);

const renderFixture = (linkProps: LinkProps = {}) => render(<Fixture linkProps={linkProps} />);

const loader = () => screen.queryByTestId('navigation-loader');
const clickLink = () => fireEvent.click(screen.getByText('Agenda'));

describe('NavigationLoader', () => {
  beforeEach(() => {
    mockPathname = '/';
    window.history.replaceState({}, '', '/');
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
    window.history.replaceState({}, '', '/');
  });

  it('shouldShowLoaderWhenInternalLinkIsClicked', () => {
    renderFixture();
    expect(loader()).not.toBeInTheDocument();

    clickLink();

    expect(loader()).toBeInTheDocument();
  });

  it('shouldGrowProgressWhileNavigationIsPending', () => {
    renderFixture();
    clickLink();
    expect(loader()).toHaveStyle({width: '30%'});

    act(() => {
      jest.advanceTimersByTime(300);
    });
    expect(loader()).toHaveStyle({width: '60%'});

    act(() => {
      jest.advanceTimersByTime(600);
    });
    expect(loader()).toHaveStyle({width: '85%'});

    act(() => {
      jest.advanceTimersByTime(3000);
    });
    expect(loader()).toHaveStyle({width: '85%'});
  });

  it('shouldHideLoaderWhenPathnameChanges', () => {
    const {rerender} = renderFixture();
    clickLink();
    expect(loader()).toBeInTheDocument();

    mockPathname = '/fr/evenements';
    rerender(<Fixture />);
    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreClickOutsideAnyLink', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Bouton'));

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreDefaultPreventedClick', () => {
    // window is earlier than document in the capture path, so the loader sees
    // an already-prevented event.
    const preventEverything = (event: Event) => event.preventDefault();
    window.addEventListener('click', preventEverything, true);
    renderFixture();

    try {
      clickLink();
    } finally {
      window.removeEventListener('click', preventEverything, true);
    }

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreMiddleButtonClick', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Agenda'), {button: 1});

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreCtrlClick', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Agenda'), {ctrlKey: true});

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreMetaClick', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Agenda'), {metaKey: true});

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreShiftClick', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Agenda'), {shiftKey: true});

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreAltClick', () => {
    renderFixture();

    fireEvent.click(screen.getByText('Agenda'), {altKey: true});

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreBlankTargetLink', () => {
    renderFixture({target: '_blank', rel: 'noreferrer'});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldStartOnSelfTargetLink', () => {
    renderFixture({target: '_self'});

    clickLink();

    expect(loader()).toBeInTheDocument();
  });

  it('shouldIgnoreDownloadLink', () => {
    renderFixture({href: '/fr/programme.pdf', download: ''});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreLinkMarkedWithDataNoLoader', () => {
    renderFixture({'data-no-loader': ''});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreExternalLink', () => {
    renderFixture({href: 'https://www.bretagne.bzh/agenda'});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreMailtoLink', () => {
    renderFixture({href: 'mailto:contact@tamm-kreiz.bzh'});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreTelLink', () => {
    renderFixture({href: 'tel:+33296000000'});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreJavascriptLink', () => {
    // Built outside JSX: React warns when a javascript: URL is passed as a prop.
    render(<NavigationLoader />);
    const anchor = document.createElement('a');
    anchor.setAttribute('href', 'javascript:void(0)');
    anchor.textContent = 'Action';
    anchor.addEventListener('click', (event) => event.preventDefault());
    document.body.appendChild(anchor);

    fireEvent.click(anchor);

    expect(loader()).not.toBeInTheDocument();
    anchor.remove();
  });

  it('shouldIgnoreHashOnlyLink', () => {
    renderFixture({href: '#programme'});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreLinkToCurrentUrl', () => {
    renderFixture({href: window.location.pathname});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldIgnoreLinkToCurrentPathWithHash', () => {
    renderFixture({href: `${window.location.pathname}#programme`});

    clickLink();

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldStartOnSamePathWithDifferentQuery', () => {
    renderFixture({href: `${window.location.pathname}?page=2`});

    clickLink();

    expect(loader()).toBeInTheDocument();
  });

  it('shouldHideLoaderAfterTimeoutWhenNavigationNeverCompletes', () => {
    renderFixture();
    clickLink();

    act(() => {
      jest.advanceTimersByTime(NAVIGATION_TIMEOUT_MS - 1);
    });
    expect(loader()).toBeInTheDocument();

    act(() => {
      jest.advanceTimersByTime(1);
    });
    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldShowLoaderWhenNavigationStartEventIsDispatched', () => {
    expect(NAVIGATION_START_EVENT).toBe('tk:navigation-start');
    renderFixture();

    act(() => {
      window.dispatchEvent(new Event(NAVIGATION_START_EVENT));
    });

    expect(loader()).toBeInTheDocument();
  });

  it('shouldShowLoaderWhenStartNavigationLoaderIsCalled', () => {
    renderFixture();

    act(() => {
      startNavigationLoader();
    });

    expect(loader()).toBeInTheDocument();
  });

  it('shouldRemoveListenersOnUnmount', () => {
    const removeDocumentListener = jest.spyOn(document, 'removeEventListener');
    const removeWindowListener = jest.spyOn(window, 'removeEventListener');
    const {unmount} = renderFixture();

    unmount();

    expect(removeDocumentListener).toHaveBeenCalledWith('click', expect.any(Function), true);
    expect(removeWindowListener).toHaveBeenCalledWith(NAVIGATION_START_EVENT, expect.any(Function));
    removeDocumentListener.mockRestore();
    removeWindowListener.mockRestore();
  });

  it('shouldClearPendingTimersOnUnmount', () => {
    const {unmount} = renderFixture();
    clickLink();
    expect(jest.getTimerCount()).toBeGreaterThan(0);

    unmount();

    expect(jest.getTimerCount()).toBe(0);
  });

  it('shouldCompleteProgressBeforeHidingOnPathnameChange', () => {
    const {rerender} = renderFixture();
    clickLink();

    mockPathname = '/fr/evenements';
    rerender(<Fixture />);

    expect(loader()).toHaveStyle({width: '100%'});
  });

  it('shouldStayHiddenWhenPathnameChangesWithoutAnyClick', () => {
    const {rerender} = renderFixture();

    mockPathname = '/fr/evenements';
    rerender(<Fixture />);

    expect(loader()).not.toBeInTheDocument();
  });

  it('shouldHideLoaderWhenOnlyTheQueryStringChanges', () => {
    // usePathname() stays the same on a query-only navigation (pagination links),
    // so the loader also watches the browser URL.
    renderFixture({href: '/?page=2'});
    clickLink();
    expect(loader()).toBeInTheDocument();

    window.history.pushState({}, '', '/?page=2');
    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(loader()).not.toBeInTheDocument();
  });
});
