/* eslint-disable unicorn/no-null */

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

import {
  mockWindowHistory,
  mockWindowLocation,
  restoreWindowHistory,
  restoreWindowLocation,
  WindowHistoryMock
} from './mockWindowLocation';

/**
 * Taken from https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs
 *
 *          userinfo       host      port
 *          ┌──┴───┐ ┌──────┴──────┐ ┌┴┐
 *  https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top
 *  └─┬─┘   └─────────────┬────────────┘└───────┬───────┘ └────────────┬────────────┘ └┬┘
 *  scheme          authority                  path                  query           fragment
 *
 */
const urlNetworking =
  'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top';

const urlComputing = 'https://john.doe@www.example.com:123/forum/answers/?tag=computing';

const urlMathematics = 'https://john.doe@www.example.com:123/forum/answers/?tag=mathematics';

const urlCryptography = 'https://john.doe@www.example.com:123/forum/answers/?tag=cryptography';

const urlWikipedia = 'https://en.wikipedia.org/wiki/Main_Page';

beforeEach(() => {
  mockWindowLocation(urlNetworking);
  mockWindowHistory();
});

afterEach(() => {
  restoreWindowLocation();
  restoreWindowHistory();
});

describe('window.location', () => {
  test('mock and restore window.location', () => {
    expect(window.location.href).toBe(urlNetworking);
    expect(document.location.href).toBe('http://localhost:3000/'); // :-/
    expect(document.URL).toBe('http://localhost:3000/'); // :-/
    expect(document.documentURI).toBe('http://localhost:3000/'); // :-/
    restoreWindowLocation();
    expect(window.location.href).toBe('http://localhost:3000/');
    expect(document.location.href).toBe('http://localhost:3000/');
    expect(document.URL).toBe('http://localhost:3000/');
    expect(document.documentURI).toBe('http://localhost:3000/');
  });

  test('getters', () => {
    expect(window.location.toString()).toBe(urlNetworking);
    expect(window.location.href).toBe(urlNetworking);
    expect(window.location.origin).toBe('https://www.example.com:123');
    expect(window.location.protocol).toBe('https:');
    expect(window.location.host).toBe('www.example.com:123');
    expect(window.location.hostname).toBe('www.example.com');
    expect(window.location.port).toBe('123');
    expect(window.location.pathname).toBe('/forum/questions/');
    expect(window.location.search).toBe('?tag=networking&order=newest');
    expect(window.location.hash).toBe('#top');
  });

  test('set href', () => {
    expect(window.location.href).toBe(urlNetworking);

    window.location.href = urlWikipedia;

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      { state: null, url: urlWikipedia }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(urlWikipedia);
  });

  test('set protocol', () => {
    expect(window.location.protocol).toBe('https:');

    window.location.protocol = 'http:';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'
    );
  });

  test('set host', () => {
    expect(window.location.host).toBe('www.example.com:123');

    window.location.host = 'en.wikipedia.org';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top'
    );
  });

  test('set hostname', () => {
    expect(window.location.hostname).toBe('www.example.com');

    window.location.hostname = 'en.wikipedia.org';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top'
    );
  });

  test('set port', () => {
    expect(window.location.port).toBe('123');

    window.location.port = '1234';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@www.example.com:1234/forum/questions/?tag=networking&order=newest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@www.example.com:1234/forum/questions/?tag=networking&order=newest#top'
    );
  });

  test('set pathname', () => {
    expect(window.location.pathname).toBe('/forum/questions/');

    window.location.pathname = '/forum/answers/';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@www.example.com:123/forum/answers/?tag=networking&order=newest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@www.example.com:123/forum/answers/?tag=networking&order=newest#top'
    );
  });

  test('set search', () => {
    expect(window.location.search).toBe('?tag=networking&order=newest');

    window.location.search = '?tag=networking&order=oldest';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=oldest#top'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=oldest#top'
    );
  });

  test('set hash', () => {
    expect(window.location.hash).toBe('#top');

    window.location.hash = '#bottom';

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      {
        state: null,
        url: 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#bottom'
      }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(
      'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#bottom'
    );
  });

  test('assign()', () => {
    window.location.assign(urlComputing);

    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      { state: null, url: urlComputing }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(urlComputing);
  });

  test('replace()', () => {
    window.location.replace(urlComputing);

    expect(window.history.length).toBe(1);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlComputing }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(urlComputing);
  });

  test('reload()', () => {
    window.location.reload();

    expect(window.history.length).toBe(1);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking }
    ]);
    expect(window.history.state).toBeNull();
    expect(window.location.href).toBe(urlNetworking);
  });
});

describe('window.history', () => {
  test('mock and restore window.history', () => {
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking }
    ]);

    restoreWindowHistory();

    // Back to JSDOM window.history
    expect((window.history as WindowHistoryMock).sessionHistory).toBeUndefined();
  });

  test('go()', () => {
    expect(window.history.length).toBe(1);

    window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing');
    window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics');
    window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography');

    expect(window.history.length).toBe(4);
    expect(window.location.href).toBe(urlCryptography);

    window.history.go(-1);
    expect(window.location.href).toBe(urlMathematics);

    // Reload current page
    window.history.go(0);
    expect(window.location.href).toBe(urlMathematics);

    window.history.go(-2);
    expect(window.location.href).toBe(urlNetworking);

    // Do nothing
    window.history.go(-1);
    expect(window.location.href).toBe(urlNetworking);

    window.history.go(+1);
    expect(window.location.href).toBe(urlComputing);

    window.history.go(+2);
    expect(window.location.href).toBe(urlCryptography);

    // Do nothing
    window.history.go(+1);
    expect(window.location.href).toBe(urlCryptography);
  });

  test('back()', () => {
    window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing');
    window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics');
    window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography');

    expect(window.location.href).toBe(urlCryptography);

    window.history.back();
    expect(window.location.href).toBe(urlMathematics);

    window.history.back();
    expect(window.location.href).toBe(urlComputing);

    window.history.back();
    expect(window.location.href).toBe(urlNetworking);

    // Do nothing
    window.history.back();
    expect(window.location.href).toBe(urlNetworking);
  });

  test('forward()', () => {
    window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing');
    window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics');
    window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography');

    expect(window.location.href).toBe(urlCryptography);

    window.history.back();
    window.history.back();
    window.history.back();
    expect(window.location.href).toBe(urlNetworking);

    window.history.forward();
    expect(window.location.href).toBe(urlComputing);

    window.history.forward();
    expect(window.location.href).toBe(urlMathematics);

    window.history.forward();
    expect(window.location.href).toBe(urlCryptography);

    // Do nothing
    window.history.forward();
    expect(window.location.href).toBe(urlCryptography);
  });

  test('pushState()', () => {
    expect(window.history.length).toBe(1);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking }
    ]);
    expect(window.history.state).toBeNull();

    window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing');
    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      { state: 'computing', url: urlComputing }
    ]);
    expect(window.history.state).toBe('computing');
    expect(window.location.href).toBe(urlComputing);

    window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics');
    expect(window.history.length).toBe(3);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking },
      { state: 'computing', url: urlComputing },
      { state: 'mathematics', url: urlMathematics }
    ]);
    expect(window.history.state).toBe('mathematics');
    expect(window.location.href).toBe(urlMathematics);
  });

  test('replaceState()', () => {
    expect(window.history.length).toBe(1);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: null, url: urlNetworking }
    ]);
    expect(window.history.state).toBeNull();

    window.history.replaceState('computing', 'unused', '/forum/answers/?tag=computing');
    expect(window.history.length).toBe(1);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: 'computing', url: urlComputing }
    ]);
    expect(window.history.state).toBe('computing');
    expect(window.location.href).toBe(urlComputing);

    window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics');
    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: 'computing', url: urlComputing },
      { state: 'mathematics', url: urlMathematics }
    ]);
    expect(window.history.state).toBe('mathematics');
    expect(window.location.href).toBe(urlMathematics);

    window.history.replaceState('cryptography', 'unused', '/forum/answers/?tag=cryptography');
    expect(window.history.length).toBe(2);
    expect((window.history as WindowHistoryMock).sessionHistory).toEqual([
      { state: 'computing', url: urlComputing },
      { state: 'cryptography', url: urlCryptography }
    ]);
    expect(window.history.state).toBe('cryptography');
    expect(window.location.href).toBe(urlCryptography);
  });

  test('pushState() and replaceState() throw when pushing an URL with another origin', () => {
    expect(() => {
      window.history.pushState({}, 'unused', urlWikipedia);
    }).toThrow(
      "Failed to execute 'pushState' on 'History': A history state object with URL 'https://en.wikipedia.org/wiki/Main_Page' cannot be created in a document with origin 'https://www.example.com:123' and URL 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'."
    );

    expect(() => {
      window.history.replaceState({}, 'unused', urlWikipedia);
    }).toThrow(
      "Failed to execute 'replaceState' on 'History': A history state object with URL 'https://en.wikipedia.org/wiki/Main_Page' cannot be created in a document with origin 'https://www.example.com:123' and URL 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'."
    );
  });

  test('popstate event - see window-history-playground.html', () => {
    const popstateSpy = vi.fn();
    function popstateListener({ state }: PopStateEvent) {
      popstateSpy(state);
    }
    window.addEventListener('popstate', popstateListener);

    expect(popstateSpy).toHaveBeenCalledTimes(0);

    window.history.pushState('page=1', 'page=1', '?page=1');
    window.history.pushState('page=2', 'page=2', '?page=2');
    window.history.pushState('page=3', 'page=3', '?page=3');
    window.history.replaceState('page=3-replace', 'page=3-replace', '?page=3-replace');

    // popstate page=2
    window.history.back();
    expect(popstateSpy).toHaveBeenCalledTimes(1);
    expect(popstateSpy).toHaveBeenNthCalledWith(1, 'page=2');

    // popstate page=1
    window.history.back();
    expect(popstateSpy).toHaveBeenCalledTimes(2);
    expect(popstateSpy).toHaveBeenNthCalledWith(2, 'page=1');

    // popstate null (i.e. root page)
    window.history.back();
    expect(popstateSpy).toHaveBeenCalledTimes(3);
    expect(popstateSpy).toHaveBeenNthCalledWith(3, null);

    // popstate page=2
    window.history.go(2);
    expect(popstateSpy).toHaveBeenCalledTimes(4);
    expect(popstateSpy).toHaveBeenNthCalledWith(4, 'page=2');

    // popstate page=1
    window.history.go(-1);
    expect(popstateSpy).toHaveBeenCalledTimes(5);
    expect(popstateSpy).toHaveBeenNthCalledWith(5, 'page=1');

    // load page=1
    window.history.go();
    expect(popstateSpy).toHaveBeenCalledTimes(5);

    window.removeEventListener('popstate', popstateListener);
  });
});