'use strict';

const { nextTick, VueWrapper } = require( 'vue' );
const { flushPromises } = require( '@vue/test-utils' );
const { getSpecialBlock } = require( './SpecialBlock.setup.js' );
const useBlockStore = require( '../../../resources/src/mediawiki.special.block/stores/block.js' );

/**
 * Mock postWithEditToken to return an actual Deferred object,
 * so that jQuery promise chain methods (e.g. always()) will execute in the test.
 *
 * @param {Object} [config] Configuration to override the defaults.
 * @param {Object} [postResponse] Response to return from the postWithEditToken call.
 * @return {VueWrapper}
 */
const withSubmission = ( config, postResponse ) => {
	const jQuery = jest.requireActual( '../../../resources/lib/jquery/jquery.js' );
	mw.Api.prototype.postWithEditToken = jest.fn( () => jQuery.Deferred().resolve( postResponse ).promise() );
	HTMLFormElement.prototype.checkValidity = jest.fn().mockReturnValue( true );
	return getSpecialBlock( config );
};

describe( 'SpecialBlock', () => {
	let wrapper;

	afterEach( () => wrapper.unmount() );

	it( 'should show no banner and no "Add block" button on page load', async () => {
		wrapper = getSpecialBlock();
		expect( wrapper.find( '.cdx-message__content' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block-submit' ).exists() ).toBeFalsy();
	} );

	it( 'should show no banner and block form after selecting a valid target with no active blocks', async () => {
		wrapper = getSpecialBlock( { blockId: null } );
		const hookSpy = jest.spyOn( mw.hook(), 'fire' );
		expect( wrapper.find( '.cdx-message__content' ).exists() ).toBeFalsy();
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeTruthy();
		expect( hookSpy ).toHaveBeenCalledWith( true, 'ExampleUser', null );
		expect( wrapper.find( '.mw-block-submit' ).text() ).toStrictEqual( 'block-submit' );
	} );

	it( 'should show a banner and "Add block" button based on if user is already blocked', () => {
		expect( wrapper.find( '.mw-block-messages .cdx-message--error' ).exists() ).toBeFalsy();
		wrapper = getSpecialBlock( {
			blockAlreadyBlocked: true,
			blockTargetUser: 'ExampleUser',
			blockTargetExists: true,
			blockPreErrors: [ 'ExampleUser is already blocked.' ]
		} );
		// Server-generated message, hence why it's in English.
		expect( wrapper.find( '.mw-block-messages .cdx-message--error' ).text() )
			.toStrictEqual( 'ExampleUser is already blocked.' );
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeTruthy();
	} );

	it( 'should submit an API request to add a new block for a user (Multiblocks OFF)', async () => {
		wrapper = withSubmission( { blockId: null, blockEnableMultiblocks: false }, { block: { user: 'ExampleUser' } } );
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		await flushPromises();
		expect( wrapper.find( '.mw-block__block-form h2' ).text() ).toStrictEqual( 'block-create' );
		await wrapper.find( '.cdx-radio__input[value=datetime]' ).setValue( true );
		await wrapper.find( '[name=wpExpiry-other]' ).setValue( '2999-01-23T12:34' );
		await wrapper.find( '[name=wpReason-other]' ).setValue( 'This is a test' );
		const spy = jest.spyOn( mw.Api.prototype, 'postWithEditToken' );
		const submitButton = wrapper.find( '.mw-block-submit' );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeFalsy();
		await submitButton.trigger( 'click' );
		expect( spy ).toHaveBeenCalledWith( {
			action: 'block',
			user: 'ExampleUser',
			expiry: '2999-01-23T12:34:00Z',
			reason: 'This is a test',
			nocreate: 1,
			allowusertalk: 1,
			autoblock: 1,
			errorformat: 'html',
			errorlang: 'en',
			errorsuselocal: true,
			uselang: 'en',
			format: 'json',
			formatversion: 2
		} );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block-success .cdx-message__content' ).text() ).toContain( 'block-added-message' );
	} );

	it( 'should submit an API request to block the user', async () => {
		wrapper = withSubmission( undefined, { block: { user: 'ExampleUser' } } );
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		await flushPromises();
		await wrapper.find( '.cdx-radio__input[value=datetime]' ).setValue( true );
		await wrapper.find( '[name=wpExpiry-other]' ).setValue( '2999-01-23T12:34' );
		await wrapper.find( '[name=wpReason-other]' ).setValue( 'This is a test' );
		const spy = jest.spyOn( mw.Api.prototype, 'postWithEditToken' );
		const hookSpy = jest.spyOn( mw.hook(), 'fire' );
		const submitButton = wrapper.find( '.mw-block-submit' );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeFalsy();
		await submitButton.trigger( 'click' );
		expect( spy ).toHaveBeenCalledWith( {
			action: 'block',
			user: 'ExampleUser',
			newblock: 1,
			expiry: '2999-01-23T12:34:00Z',
			reason: 'This is a test',
			nocreate: 1,
			allowusertalk: 1,
			autoblock: 1,
			errorformat: 'html',
			errorlang: 'en',
			errorsuselocal: true,
			uselang: 'en',
			format: 'json',
			formatversion: 2
		} );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeTruthy();
		// {user:'ExampleUser'} is from the fixtures in SpecialBlock.setup.js
		expect( hookSpy ).toHaveBeenCalledWith( { user: 'ExampleUser' } );
	} );

	it( 'should add an error state to invalid fields on submission', async () => {
		wrapper = withSubmission();
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		await flushPromises();
		await wrapper.find( '.cdx-radio__input[value=datetime]' ).setValue( true );
		// Add invalid date
		await wrapper.find( '[name=wpExpiry-other]' ).setValue( '0000-01-23T12:34:56' );
		await wrapper.find( '.mw-block-submit' ).trigger( 'click' );
		await nextTick();
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block-expiry-field__datetime .cdx-text-input' ).attributes().class )
			.toContain( 'cdx-text-input--status-error' );
		expect( wrapper.find( '.mw-block-expiry-field__datetime .cdx-message--error' ).exists() )
			.toBeTruthy();
	} );

	it( 'should require confirmation for the hide-user option', async () => {
		wrapper = getSpecialBlock( { blockHideUser: true } );
		const store = useBlockStore();
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		// Assert 'hide username' is not yet visible.
		expect( wrapper.find( '.mw-block-hideuser input' ).exists() ).toBeFalsy();
		expect( store.hideUserVisible ).toBeFalsy();
		expect( store.confirmationMessage ).toStrictEqual( '' );
		// Set the expiry to 'infinite' to enable the hide-user option.
		store.expiry = 'infinite';
		mw.util.isInfinity = jest.fn().mockReturnValue( true );
		await nextTick();
		// Assert 'hide username' is now clickable.
		expect( wrapper.find( '.mw-block-hideuser input' ).attributes().disabled ).toBeUndefined();
		expect( store.hideUserVisible ).toBeTruthy();
		expect( store.hideUser ).toBeFalsy();
		await wrapper.find( '.mw-block-hideuser input' ).trigger( 'click' );
		expect( store.hideUser ).toBeTruthy();
		// Assert confirmation is required.
		expect( store.confirmationMessage ).toStrictEqual( 'ipb-confirmhideuser' );
		expect( store.confirmationNeeded ).toBeTruthy();
		expect( store.formSubmitted ).toBeFalsy();
		expect( wrapper.vm.confirmationOpen ).toBeFalsy();
		expect( document.body.querySelector( '.mw-block-confirm' ) ).toBeFalsy();
		await wrapper.find( '.mw-block-submit' ).trigger( 'click' );
		expect( store.formSubmitted ).toBeTruthy();
		expect( wrapper.vm.confirmationOpen ).toBeTruthy();
		expect( document.body.querySelector( '.mw-block-confirm' ) ).toBeTruthy();
		mw.util.isInfinity = jest.fn().mockReturnValue( false );
	} );

	it( 'should require confirmation for self-blocking', async () => {
		wrapper = getSpecialBlock( {
			wgUserName: 'ExampleUser'
		} );
		const store = useBlockStore();
		expect( wrapper.find( '.cdx-message--error' ).exists() ).toBeFalsy();
		expect( store.confirmationNeeded ).toBeFalsy();
		expect( store.confirmationMessage ).toStrictEqual( '' );
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		store.expiry = '3 days';
		expect( store.confirmationNeeded ).toBeTruthy();
		expect( store.confirmationMessage ).toStrictEqual( 'ipb-blockingself' );
		expect( store.formSubmitted ).toBeFalsy();
		expect( wrapper.vm.confirmationOpen ).toBeFalsy();
		expect( document.body.querySelector( '.mw-block-confirm' ) ).toBeFalsy();
		await wrapper.find( '.mw-block-submit' ).trigger( 'click' );
		await nextTick();
		expect( store.formSubmitted ).toBeTruthy();
		expect( wrapper.vm.confirmationOpen ).toBeTruthy();
		expect( document.body.querySelector( '.mw-block-confirm' ) ).toBeTruthy();
	} );

	it( 'should reset form refs after blocking', async () => {
		wrapper = withSubmission(
			{ blockTargetUser: 'ActiveBlockedUser', blockTargetExists: true },
			{ block: { user: 'ActiveBlockedUser' } }
		);
		const store = useBlockStore();
		await flushPromises();
		expect( wrapper.find( '[data-test=edit-block-button]' ).exists() ).toBeTruthy();
		await wrapper.find( '[data-test=edit-block-button]' ).trigger( 'click' );
		expect( store.reason ).toStrictEqual( 'Spamming talk page' );
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block__block-form h2' ).text() ).toStrictEqual( 'block-update' );
		expect( wrapper.find( '.mw-block-submit' ).text() ).toStrictEqual( 'block-submit' );
		await wrapper.find( '[name=wpReason-other]' ).setValue( 'This is a test' );
		expect( store.reason ).toStrictEqual( 'This is a test' );
		await wrapper.find( '.mw-block-submit' ).trigger( 'click' );
		await flushPromises();
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block-success .cdx-message__content' ).text() ).toContain( 'block-updated-message' );
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeFalsy();
		expect( store.reason ).toStrictEqual( '' );
		expect( store.blockId ).toStrictEqual( null );
	} );

	it( 'should reset the form after changing the target while editing a block (T389056)', async () => {
		wrapper = getSpecialBlock( {
			blockTargetUser: 'ActiveBlockedUser',
			blockTargetExists: true,
			blockAlreadyBlocked: true
		} );
		const store = useBlockStore();
		await flushPromises();
		await wrapper.find( '[data-test=edit-block-button]' ).trigger( 'click' );
		expect( store.blockId ).toStrictEqual( 1110 );
		expect( store.formVisible ).toBeTruthy();
		await wrapper.find( '[name=wpTarget]' ).setValue( 'ExampleUser' );
		await wrapper.find( '[name=wpTarget]' ).trigger( 'change' );
		expect( store.targetUser ).toStrictEqual( 'ExampleUser' );
		expect( store.blockId ).toBeNull();
	} );

	it( 'should use pre-set values when creating a new block', async () => {
		wrapper = getSpecialBlock( {
			blockTargetUser: 'ExampleUser',
			blockTargetExists: true,
			blockTypePreset: 'partial',
			blockPageRestrictions: 'Foo\nBar',
			blockExpiryPreset: '99 hours'
		} );
		const store = useBlockStore();
		await flushPromises();
		expect( store.type ).toStrictEqual( 'partial' );
		expect( store.pages ).toStrictEqual( [ 'Foo', 'Bar' ] );
		expect( store.expiry ).toStrictEqual( '99 hours' );
		expect( wrapper.find( '[name=expiryType]:checked' ).element.value ).toStrictEqual( 'custom-duration' );
		expect( wrapper.find( 'input[type=number]' ).element.value ).toStrictEqual( '99' );
		expect( wrapper.find( '.mw-block-pages' ).text() ).toStrictEqual( 'FooBar' );
	} );

	it( 'should show an "Add block" button in the page', async () => {
		wrapper = getSpecialBlock( { blockTargetUser: 'ExampleUser', blockTargetExists: true } );
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeTruthy();
	} );

	it( 'should not show an "Add block" button (multiblocks OFF)', async () => {
		wrapper = withSubmission( {
			blockTargetUser: 'ActiveBlockedUser',
			blockTargetExists: true,
			blockEnableMultiblocks: false,
			blockAlreadyBlocked: true
		}, { block: { user: 'ActiveBlockedUser' } } );
		const store = useBlockStore();
		await flushPromises();
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeFalsy();
		// Edit a block.
		expect( wrapper.find( '[data-test=edit-block-button]' ).exists() ).toBeTruthy();
		await wrapper.find( '[data-test=edit-block-button]' ).trigger( 'click' );
		// Cancel the edit.
		await wrapper.find( '[data-test="cancel-edit-button"]' ).trigger( 'click' );
		// "Add block" button still shouldn't be shown.
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeFalsy();
		expect( store.alreadyBlocked ).toBeTruthy();
	} );

	it( 'should reset the form to the initial state for subsequent blocks (T384822)', async () => {
		wrapper = withSubmission(
			{ blockTargetUser: 'ActiveBlockedUser', blockTargetExists: true },
			{ block: { user: 'ActiveBlockedUser' } }
		);
		const store = useBlockStore();
		await flushPromises();
		// Edit a block.
		expect( wrapper.find( '[data-test=edit-block-button]' ).exists() ).toBeTruthy();
		await wrapper.find( '[data-test=edit-block-button]' ).trigger( 'click' );
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block__block-form h2' ).text() ).toStrictEqual( 'block-update' );
		expect( wrapper.find( '.mw-block-submit' ).text() ).toStrictEqual( 'block-submit' );
		expect( store.formDirty ).toBeFalsy();
		await wrapper.find( '[name=wpReason-other]' ).setValue( 'This is a test' );
		expect( store.reason ).toStrictEqual( 'This is a test' );
		expect( store.formDirty ).toBeTruthy();
		// Submit.
		await wrapper.find( '.mw-block-submit' ).trigger( 'click' );
		await flushPromises();
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeFalsy();
		expect( store.reason ).toStrictEqual( '' );
		expect( store.blockId ).toBeNull();
		// Add a new block.
		await wrapper.find( '.mw-block__create-button' ).trigger( 'click' );
		expect( wrapper.find( '.mw-block__block-form' ).exists() ).toBeTruthy();
		expect( store.blockId ).toBeNull();
		expect( store.reason ).toStrictEqual( '' );
		expect( store.formDirty ).toBeFalsy();
		expect( wrapper.find( '[name=wpReason-other]' ).element.value ).toStrictEqual( '' );
	} );

	it( 'should reset the form to the initial state for new blocks when a block is edited, canceled, and then create block', async () => {
		wrapper = withSubmission(
			{ blockTargetUser: 'ActiveBlockedUser', blockTargetExists: true },
			{ block: { user: 'ActiveBlockedUser' } }
		);
		const hookSpy = jest.spyOn( mw.hook(), 'fire' );
		const store = useBlockStore();
		await flushPromises();
		// Edit a block.
		await wrapper.find( '[data-test=edit-block-button]' ).trigger( 'click' );
		expect( hookSpy ).toHaveBeenCalledWith( true, 'ActiveBlockedUser', 1110 );
		await wrapper.find( '[name=wpReason-other]' ).setValue( 'This is a test' );
		expect( store.reason ).toStrictEqual( 'This is a test' );
		// Cancel the edit.
		await wrapper.find( '[data-test="cancel-edit-button"]' ).trigger( 'click' );
		expect( hookSpy ).toHaveBeenCalledWith( false, 'ActiveBlockedUser', null );
		// Create a new block.
		await wrapper.find( '.mw-block__create-button' ).trigger( 'click' );
		expect( store.blockId ).toBeNull();
		expect( store.reason ).toStrictEqual( '' );
		expect( wrapper.find( '[name=wpReason-other]' ).element.value ).toStrictEqual( '' );
	} );

	it( 'should show no block logs and no "Add block" button when the page is loaded with an invalid target', async () => {
		wrapper = getSpecialBlock( {
			blockTargetUser: 'NonexistentUser',
			blockTargetExists: false,
			blockId: null
		} );

		await flushPromises();
		expect( wrapper.find( '.mw-block-log__type-active' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block-log__type-recent' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeFalsy();
	} );

	it( 'should show an error when given an invalid block ID', async () => {
		wrapper = getSpecialBlock( { blockId: 12345 }, [ {
			params: {
				list: 'blocks',
				bkids: 12345
			},
			response: { blocks: [] }
		} ] );
		const store = useBlockStore();
		await flushPromises();
		expect( wrapper.find( '.cdx-message--error' ).text() ).toBe( 'block-invalid-id' );
		expect( store.blockId ).toBeNull();
	} );

	it( 'should show no block logs and no "Add block" button when the target is changed from a valid to an invalid target', async () => {
		wrapper = withSubmission(
			{ blockTargetUser: 'ActiveBlockedUser', blockTargetExists: true },
			{ block: { user: 'ActiveBlockedUser' } }
		);

		await flushPromises();
		expect( wrapper.find( '.mw-block-log__type-active' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block-log__type-recent' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeTruthy();

		wrapper.find( '#mw-bi-target' ).setValue( 'NonexistentUser' );
		await flushPromises();
		expect( wrapper.find( '.mw-block-log__type-active' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block-log__type-recent' ).exists() ).toBeFalsy();
		expect( wrapper.find( '.mw-block__create-button' ).exists() ).toBeFalsy();
	} );

	it( 'should emit "submit" event when Enter key is pressed', async () => {
		wrapper = withSubmission(
			{ blockTargetUser: 'ActiveBlockedUser', blockTargetExists: true },
			{ block: { user: 'ActiveBlockedUser' } }
		);
		await flushPromises();
		// Create a new block.
		await wrapper.find( '.mw-block__create-button' ).trigger( 'click' );
		// Set expiry date
		await wrapper.find( '.cdx-radio__input[value=datetime]' ).setValue( true );
		await wrapper.find( '[name=wpExpiry-other]' ).setValue( '2999-01-23T12:34' );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeFalsy();
		// Simulate a press enter
		await wrapper.find( '[name="wpReason-other"]' ).trigger( 'keypress', { key: 'Enter' } );
		expect( wrapper.find( '.mw-block-success' ).exists() ).toBeTruthy();
	} );

	it( 'should show "Active blocks" and "Active range blocks" on the given IP', async () => {
		mw.util.isIPAddress = jest.fn().mockReturnValue( true );
		wrapper = getSpecialBlock( { blockTargetUser: '1.2.3.20', blockTargetExists: true } );
		await flushPromises();
		expect( wrapper.find( '.mw-block-log__type-active' ).exists() ).toBeTruthy();
		expect( wrapper.findAll( '.mw-block-log__type-active tbody tr' ) ).toHaveLength( 1 );
		expect( wrapper.find( '.mw-block-log__type-active-ranges' ).exists() ).toBeTruthy();
		expect( wrapper.findAll( '.mw-block-log__type-active-ranges tbody tr' ) ).toHaveLength( 2 );
	} );

	it( 'should show an empty "Active range blocks" for an IP with no range blocks', async () => {
		mw.util.isIPAddress = jest.fn().mockReturnValue( true );
		wrapper = getSpecialBlock( { blockTargetUser: '192.168.0.1', blockTargetExists: true } );
		await flushPromises();
		expect( wrapper.find( '.mw-block-log__type-active' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block-log__type-active tbody' ).text() )
			.toStrictEqual( 'block-user-no-active-blocks' );
		expect( wrapper.find( '.mw-block-log__type-active-ranges' ).exists() ).toBeTruthy();
		expect( wrapper.find( '.mw-block-log__type-active-ranges tbody' ).text() )
			.toStrictEqual( 'block-user-no-active-range-blocks' );
	} );

	it( 'should show "add block" form for an IP with range blocks and no active blocks', async () => {
		mw.util.isIPAddress = jest.fn().mockReturnValue( true );
		wrapper = getSpecialBlock( { blockTargetUser: '1.2.3.20', blockTargetExists: true, blockEnableMultblocks: false } );
		await flushPromises();
		// since there are no blocks, the form is instanly available and add-block is not visible
		expect( wrapper.find( '.mw-block-submit' ).exists() ).toBeTruthy();
	} );
} );
