const mustache = require( 'mustache' );
const fs = require( 'fs' );
const tableOfContentsTemplate = fs.readFileSync( 'includes/templates/TableOfContents.mustache', 'utf8' );
const tableOfContentsContentsTemplate = fs.readFileSync( 'includes/templates/TableOfContents__list.mustache', 'utf8' );
const tableOfContentsLineTemplate = fs.readFileSync( 'includes/templates/TableOfContents__line.mustache', 'utf8' );
const pinnableElementOpenTemplate = fs.readFileSync( 'includes/templates/PinnableElement/Open.mustache', 'utf8' );
const pinnableElementCloseTemplate = fs.readFileSync( 'includes/templates/PinnableElement/Close.mustache', 'utf8' );
const pinnableHeaderTemplate = fs.readFileSync( 'includes/templates/PinnableHeader.mustache', 'utf8' );
const initTableOfContents = require( '../../resources/skins.vector.js/tableOfContents.js' );

let /** @type {HTMLElement} */ container,
	/** @type {HTMLElement} */ fooSection,
	/** @type {HTMLElement} */ barSection,
	/** @type {HTMLElement} */ bazSection,
	/** @type {HTMLElement} */ quxSection,
	/** @type {HTMLElement} */ quuxSection;
const onHeadingClick = jest.fn();
const onHashChange = jest.fn();
const onToggleClick = jest.fn();
const onTogglePinned = jest.fn();

const SECTIONS = [
	{
		toclevel: 1,
		number: '1',
		line: 'foo',
		anchor: 'foo',
		linkAnchor: 'foo',
		'is-top-level-section': true,
		'is-parent-section': false,
		'array-sections': null
	}, {
		toclevel: 1,
		number: '2',
		line: 'bar',
		anchor: 'bar',
		linkAnchor: 'bar',
		'is-top-level-section': true,
		'is-parent-section': true,
		'vector-button-label': 'Toggle bar subsection',
		'array-sections': [ {
			toclevel: 2,
			number: '2.1',
			line: 'baz',
			anchor: 'baz',
			linkAnchor: 'baz',
			'is-top-level-section': false,
			'is-parent-section': true,
			'array-sections': [ {
				toclevel: 3,
				number: '2.1.1',
				line: 'qux',
				anchor: 'qux',
				linkAnchor: 'qux',
				'is-top-level-section': false,
				'is-parent-section': false,
				'array-sections': null
			} ]
		} ]
	}, {
		toclevel: 1,
		number: '3',
		line: 'quux',
		anchor: 'quux',
		linkAnchor: 'quux',
		'is-top-level-section': true,
		'is-parent-section': false,
		'array-sections': null
	}
];

/**
 * @param {Object} templateProps
 * @return {string}
 */
function render( templateProps = {} ) {
	const templateData = Object.assign( {
		'msg-vector-toc-beginning': 'Beginning',
		'vector-is-collapse-sections-enabled': false,
		'array-sections': SECTIONS,
		id: 'vector-toc',
		'data-pinnable-header': {
			'is-pinned': true,
			'data-feature-name': 'pinned',
			'data-pinnable-element-id': 'vector-toc',
			label: 'Contents',
			'label-tag-name': 'h2',
			'pin-label': 'move to sidebar',
			'unpin-label': 'hide'
		}
	}, templateProps );

	return mustache.render( tableOfContentsTemplate, templateData, {
		'PinnableElement/Open': pinnableElementOpenTemplate,
		'PinnableElement/Close': pinnableElementCloseTemplate,
		PinnableHeader: pinnableHeaderTemplate,
		TableOfContents__list: tableOfContentsContentsTemplate, // eslint-disable-line camelcase
		TableOfContents__line: tableOfContentsLineTemplate // eslint-disable-line camelcase
	} );
}

/**
 * @param {Object} templateProps
 * @return {module:TableOfContents~TableOfContents}
 */
function mount( templateProps = {} ) {
	document.body.innerHTML = render( templateProps );

	container = /** @type {HTMLElement} */ ( document.getElementById( 'vector-toc' ) );
	fooSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-foo' ) );
	barSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-bar' ) );
	bazSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-baz' ) );
	quxSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-qux' ) );
	quuxSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-quux' ) );

	return initTableOfContents( {
		container,
		onHeadingClick,
		onHashChange,
		onToggleClick,
		onTogglePinned
	} );
}

describe( 'Table of contents', () => {
	/**
	 * @type {module:TableOfContents~TableOfContents}
	 */
	let toc;

	beforeEach( () => {
		global.window.matchMedia = jest.fn( () => ( {} ) );
	} );

	afterEach( () => {
		if ( toc ) {
			toc.unmount();
			toc = undefined;
		}

		mw.util.getTargetFromFragment = undefined;
	} );

	describe( 'renders', () => {
		test( 'when `vector-is-collapse-sections-enabled` is false', () => {
			toc = mount();
			expect( document.body.innerHTML ).toMatchSnapshot();
			expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
		} );
		test( 'when `vector-is-collapse-sections-enabled` is true', () => {
			toc = mount( { 'vector-is-collapse-sections-enabled': true } );
			expect( document.body.innerHTML ).toMatchSnapshot();
			expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
		} );
		test( 'toggles for top level parent sections', () => {
			toc = mount();
			expect( fooSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
			expect( barSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 1 );
			expect( bazSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
			expect( quxSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
			expect( quuxSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
		} );
	} );

	describe( 'binds event listeners', () => {
		test( 'for onHeadingClick', () => {
			toc = mount();
			const heading = /** @type {HTMLElement} */ ( document.querySelector( `#toc-foo .${ toc.LINK_CLASS }` ) );
			heading.click();

			expect( onToggleClick ).not.toBeCalled();
			expect( onHashChange ).not.toBeCalled();
			expect( onHeadingClick ).toBeCalled();
		} );
		test( 'for onToggleClick', () => {
			toc = mount();
			const toggle = /** @type {HTMLElement} */ ( document.querySelector( `#toc-bar .${ toc.TOGGLE_CLASS }` ) );
			toggle.click();

			expect( onHeadingClick ).not.toBeCalled();
			expect( onHashChange ).not.toBeCalled();
			expect( onToggleClick ).toBeCalled();
		} );
		test( 'for onHashChange', () => {
			mw.util.getTargetFromFragment = jest.fn().mockImplementation( ( hash ) => hash === 'toc-foo' ? fooSection : null );
			mount();

			// Jest doesn't trigger a hashchange event when setting a hash location.
			location.hash = 'foo';
			window.dispatchEvent( new HashChangeEvent( 'hashchange', {
				oldURL: 'http://example.com',
				newURL: 'http://example.com#foo'
			} ) );

			expect( onHeadingClick ).not.toBeCalled();
			expect( onToggleClick ).not.toBeCalled();
			expect( onHashChange ).toBeCalled();
		} );
	} );

	describe( 'applies correct classes', () => {
		test( 'when changing active sections', () => {
			toc = mount( { 'vector-is-collapse-sections-enabled': true } );
			let activeSections;
			let activeTopSections;

			/**
			 * @param {string} id
			 * @param {HTMLElement} activeSection
			 * @param {HTMLElement} activeTopSection
			 */
			function testActiveClasses( id, activeSection, activeTopSection ) {
				toc.changeActiveSection( id );
				activeSections = container.querySelectorAll( `.${ toc.ACTIVE_SECTION_CLASS }` );
				activeTopSections = container.querySelectorAll( `.${ toc.ACTIVE_TOP_SECTION_CLASS }` );
				expect( activeSections.length ).toEqual( 1 );
				expect( activeTopSections.length ).toEqual( 1 );
				expect( activeSections[ 0 ] ).toEqual( activeSection );
				expect( activeTopSections[ 0 ] ).toEqual( activeTopSection );
			}

			testActiveClasses( 'toc-foo', fooSection, fooSection );
			testActiveClasses( 'toc-bar', barSection, barSection );
			testActiveClasses( 'toc-baz', bazSection, barSection );
			testActiveClasses( 'toc-qux', quxSection, barSection );
			testActiveClasses( 'toc-quux', quuxSection, quuxSection );
		} );

		test( 'when expanding sections', () => {
			toc = mount();
			toc.expandSection( 'toc-bar' );
			expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
		} );

		test( 'when toggling sections', () => {
			toc = mount();
			toc.toggleExpandSection( 'toc-bar' );
			expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
			toc.toggleExpandSection( 'toc-bar' );
			expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
		} );
	} );

	describe( 'applies the correct aria attributes', () => {
		test( 'when initialized', () => {
			toc = mount();
			const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${ toc.TOGGLE_CLASS }` ) );

			expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );
		} );

		test( 'when expanding sections', () => {
			toc = mount();
			const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${ toc.TOGGLE_CLASS }` ) );

			toc.expandSection( 'toc-bar' );
			expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );
		} );

		test( 'when toggling sections', () => {
			toc = mount();
			const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${ toc.TOGGLE_CLASS }` ) );

			toc.toggleExpandSection( 'toc-bar' );
			expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'false' );

			toc.toggleExpandSection( 'toc-bar' );
			expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );
		} );
	} );

	describe( 'when the hash fragment changes', () => {
		test( 'expands and activates corresponding section', () => {
			mw.util.getTargetFromFragment = jest.fn().mockImplementation( ( hash ) => hash === 'toc-qux' ? quxSection : null );
			toc = mount( { 'vector-is-collapse-sections-enabled': true } );
			expect(
				quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS )
			).toEqual( false );

			// Jest doesn't trigger a hashchange event when setting a hash location.
			location.hash = 'qux';
			window.dispatchEvent( new HashChangeEvent( 'hashchange', {
				oldURL: 'http://example.com',
				newURL: 'http://example.com#qux'
			} ) );

			const activeSections = container.querySelectorAll( `.${ toc.ACTIVE_SECTION_CLASS }` );
			const activeTopSections = container.querySelectorAll( `.${ toc.ACTIVE_TOP_SECTION_CLASS }` );
			expect( activeSections.length ).toEqual( 1 );
			expect( activeTopSections.length ).toEqual( 1 );
			expect(
				barSection.classList.contains( toc.ACTIVE_TOP_SECTION_CLASS )
			).toEqual( true );
			expect(
				barSection.classList.contains( toc.EXPANDED_SECTION_CLASS )
			).toEqual( true );
			expect(
				quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS )
			).toEqual( true );
		} );
	} );

	describe( 'reloadTableOfContents', () => {
		test( 're-renders toc when wikipage.tableOfContents hook is fired with empty sections', async () => {
			toc = mount();
			await toc.reloadTableOfContents( [] );

			expect( document.body.innerHTML ).toMatchSnapshot();
		} );

		test( 're-renders toc when wikipage.tableOfContents hook is fired with sections', async () => {
			jest.spyOn( mw.loader, 'using' ).mockImplementation( () => Promise.resolve() );
			mw.template.getCompiler = () => {};
			jest.spyOn( mw, 'message' ).mockImplementation( ( msg ) => {
				const msgFactory = ( /** @type {string} */ text ) => ( {
					parse: () => '',
					plain: () => '',
					escaped: () => '',
					exists: () => true,
					text: () => text
				} );
				switch ( msg ) {
					case 'vector-toc-beginning':
						return msgFactory( 'Beginning' );
					default:
						return msgFactory( '' );
				}

			} );
			jest.spyOn( mw.template, 'getCompiler' ).mockImplementation( () => ( {
				compile: () => ( {
					render: ( /** @type {Object} */ data ) => ( {
						html: () => render( data )
					} )
				} )
			} ) );

			toc = mount();

			const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${ toc.TOGGLE_CLASS }` ) );
			// Collapse section.
			toc.toggleExpandSection( 'toc-bar' );
			expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'false' );

			// wikipage.tableOfContents includes the nested sections in the top level
			// of the array.

			await toc.reloadTableOfContents( [
				// foo
				SECTIONS[ 0 ],
				// bar
				SECTIONS[ 1 ],
				// baz
				SECTIONS[ 1 ][ 'array-sections' ][ 0 ],
				// qux
				SECTIONS[ 1 ][ 'array-sections' ][ 0 ][ 'array-sections' ][ 0 ],
				// quux
				SECTIONS[ 2 ],
				// Add new section to see how the re-render performs.
				{
					toclevel: 1,
					number: '4',
					line: 'bat',
					anchor: 'bat',
					linkAnchor: 'bat',
					'is-top-level-section': true,
					'is-parent-section': false,
					'array-sections': null
				}
			] );

			const newToggleButton = /** @type {HTMLElement} */ ( document.querySelector( `#toc-bar .${ toc.TOGGLE_CLASS }` ) );
			expect( newToggleButton ).not.toBeNull();
			// Check that the sections render in their expanded form.
			expect( newToggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );

			// Verify newly rendered TOC html matches the expected html.
			expect( document.body.innerHTML ).toMatchSnapshot();
		} );
	} );
} );
