/**
 * @jest-environment jest-fixed-jsdom
 */

describe( 'createCheckoutPlaceOrderApi', () => {
	let $form;
	let $termsCheckbox;
	let $termsRow;
	let capturedApi;
	// Set the number of invalid `.form-row` elements that are hidden (e.g. the
	// collapsed "Ship to a different address?" shipping fields). These must never
	// block submission, so `validate()` should only count visible invalid fields.
	let setHiddenInvalidCount;

	beforeEach( () => {
		capturedApi = null;
		let hiddenInvalidCount = 0;
		setHiddenInvalidCount = ( count ) => {
			hiddenInvalidCount = count;
		};

		// used to track whether terms checkbox is checked
		let termsChecked = false;

		const termsRowClasses = new Set();
		const formInvalidElements = new Set();

		$termsRow = {
			addClass: jest.fn( ( cls ) => {
				cls.split( ' ' ).forEach( ( c ) => formInvalidElements.add( 'terms-row' ) );
				cls.split( ' ' ).forEach( ( c ) => termsRowClasses.add( c ) );
				return $termsRow;
			} ),
			removeClass: jest.fn( ( cls ) => {
				cls.split( ' ' ).forEach( ( c ) => termsRowClasses.delete( c ) );
				if ( cls.includes( 'woocommerce-invalid' ) ) {
					formInvalidElements.delete( 'terms-row' );
				}
				return $termsRow;
			} ),
			hasClass: jest.fn( ( cls ) => termsRowClasses.has( cls ) ),
			length: 1,
			offset: jest.fn( () => ( { top: 100 } ) ),
		};

		$termsCheckbox = {
			length: 1,
			is: jest.fn( ( selector ) => {
				if ( selector === ':checked' ) {
					return termsChecked;
				}
				return false;
			} ),
			closest: jest.fn( () => $termsRow ),
			trigger: jest.fn(),
		};

		// a helper to set the checkbox's state.
		$termsCheckbox.setChecked = ( checked ) => {
			termsChecked = checked;
		};

		$form = {
			length: 1,
			find: jest.fn( ( selector ) => {
				if ( selector === 'input[name="terms"]:visible' ) {
					return $termsCheckbox;
				}
				if ( selector === '.input-text, select, input:checkbox' ) {
					return { trigger: jest.fn() };
				}
				if ( selector === '.woocommerce-invalid:visible' ) {
					// Visible invalid fields only (e.g. the terms row). Hidden
					// invalid fields are deliberately excluded.
					return {
						length: formInvalidElements.size,
						first: jest.fn( () => ( {
							length: formInvalidElements.size > 0 ? 1 : 0,
							offset: jest.fn( () => ( { top: 100 } ) ),
						} ) ),
					};
				}
				if ( selector === '.woocommerce-invalid' ) {
					// Unfiltered query (includes hidden fields). The implementation
					// must NOT use this to gate submission; counting hidden invalid
					// fields here is the regression these tests guard against.
					const total = formInvalidElements.size + hiddenInvalidCount;
					return {
						length: total,
						first: jest.fn( () => ( {
							length: total > 0 ? 1 : 0,
							offset: jest.fn( () => ( { top: 100 } ) ),
						} ) ),
					};
				}
				if ( selector === '.validate-required:visible' ) {
					return { each: jest.fn() };
				}
				if ( selector === 'input[name="payment_method"]:checked' ) {
					return { val: jest.fn( () => 'test-gateway' ) };
				}
				return { length: 0, trigger: jest.fn() };
			} ),
			trigger: jest.fn(),
		};

		// Add methods to $form for checkout.js initialization
		$form.on = jest.fn( () => $form );
		$form.attr = jest.fn( () => $form );

		// Default mock for unhandled selectors - provides all common jQuery methods
		const createDefaultMock = () => {
			const mock = {
				length: 0,
				on: jest.fn( () => mock ),
				off: jest.fn( () => mock ),
				attr: jest.fn( () => mock ),
				find: jest.fn( () => createDefaultMock() ),
				first: jest.fn( () => createDefaultMock() ),
				filter: jest.fn( () => createDefaultMock() ),
				eq: jest.fn( () => createDefaultMock() ),
				trigger: jest.fn( () => mock ),
				val: jest.fn(),
				prop: jest.fn( () => mock ),
				each: jest.fn( () => mock ),
				data: jest.fn(),
				serialize: jest.fn( () => '' ),
				addClass: jest.fn( () => mock ),
				removeClass: jest.fn( () => mock ),
				hasClass: jest.fn( () => false ),
				is: jest.fn( () => false ),
				get: jest.fn( () => [] ),
				text: jest.fn( () => '' ),
				html: jest.fn( () => '' ),
				closest: jest.fn( () => createDefaultMock() ),
				parent: jest.fn( () => createDefaultMock() ),
				parents: jest.fn( () => createDefaultMock() ),
				siblings: jest.fn( () => createDefaultMock() ),
				children: jest.fn( () => createDefaultMock() ),
				append: jest.fn( () => mock ),
				prepend: jest.fn( () => mock ),
				remove: jest.fn( () => mock ),
				empty: jest.fn( () => mock ),
				show: jest.fn( () => mock ),
				hide: jest.fn( () => mock ),
				css: jest.fn( () => mock ),
				slideUp: jest.fn( () => mock ),
				slideDown: jest.fn( () => mock ),
				fadeIn: jest.fn( () => mock ),
				fadeOut: jest.fn( () => mock ),
				offset: jest.fn( () => ( { top: 0, left: 0 } ) ),
				width: jest.fn( () => 0 ),
				height: jest.fn( () => 0 ),
				outerWidth: jest.fn( () => 0 ),
				outerHeight: jest.fn( () => 0 ),
				scrollTop: jest.fn( () => 0 ),
				focus: jest.fn( () => mock ),
				blur: jest.fn( () => mock ),
				block: jest.fn( () => mock ),
				unblock: jest.fn( () => mock ),
			};
			return mock;
		};

		// Simple event system for document.body to enable event-based API capture
		const bodyEventHandlers = {};
		const mockBody = {
			on: jest.fn( ( event, handler ) => {
				if ( ! bodyEventHandlers[ event ] ) {
					bodyEventHandlers[ event ] = [];
				}
				bodyEventHandlers[ event ].push( handler );
				return mockBody;
			} ),
			trigger: jest.fn( ( event, args ) => {
				const handlers = bodyEventHandlers[ event ] || [];
				handlers.forEach( ( handler ) => handler( {}, ...( args || [] ) ) );
				return mockBody;
			} ),
			hasClass: jest.fn( () => false ),
		};

		// Mock jQuery - needs to handle document ready pattern: jQuery(function($) { ... })
		const jQueryMock = jest.fn( ( selectorOrCallback ) => {
			// Handle document ready: jQuery(function($) { ... })
			if ( typeof selectorOrCallback === 'function' ) {
				// Execute immediately with jQuery mock as argument
				selectorOrCallback( jQueryMock );
				return jQueryMock;
			}
			if ( selectorOrCallback === 'form.checkout' ) {
				return $form;
			}
			if ( selectorOrCallback === '#order_review' ) {
				return { length: 0, on: jest.fn(), attr: jest.fn(), find: jest.fn( () => ( { length: 0, val: jest.fn() } ) ) };
			}
			if ( selectorOrCallback === 'html, body' ) {
				return { animate: jest.fn() };
			}
			if ( selectorOrCallback === document.body ) {
				return mockBody;
			}
			// Return a default mock for any other selector
			return createDefaultMock();
		} );
		jQueryMock.blockUI = { defaults: { overlayCSS: {} } };

		global.window.jQuery = jQueryMock;
		global.window.$ = jQueryMock;
		global.jQuery = jQueryMock;
		global.$ = jQueryMock;

		global.window.wc_checkout_params = {
			gateways_with_custom_place_order_button: [ 'test-gateway' ],
		};

		global.window.wc = {
			customPlaceOrderButton: {
				__getForm: jest.fn( () => $form ),
				__maybeShow: jest.fn( ( gatewayId, api ) => {
					capturedApi = api;
				} ),
				__maybeHideDefaultButtonOnInit: jest.fn(),
				__cleanup: jest.fn(),
			},
		};

		// requiring checkout.js - this will execute the jQuery wrapper
		jest.resetModules();
		require( '../checkout' );

		// Trigger the event to capture the API via __maybeShow
		// This simulates a gateway registering after page load
		mockBody.trigger( 'wc_custom_place_order_button_registered', [ 'test-gateway' ] );
	} );

	afterEach( () => {
		jest.clearAllMocks();
	} );

	describe( 'Terms checkbox validation', () => {
		test( 'should return hasError: true when terms checkbox is not checked', async () => {
			$termsCheckbox.setChecked( false );

			const result = await capturedApi.validate();

			expect( result.hasError ).toBe( true );
			expect( $termsRow.addClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );
		} );

		test( 'should return hasError: false when terms checkbox is checked', async () => {
			$termsCheckbox.setChecked( true );

			const result = await capturedApi.validate();

			expect( result.hasError ).toBe( false );
		} );

		test( 'should clear stale invalid state before re-validating terms', async () => {
			// First validation: terms not checked
			$termsCheckbox.setChecked( false );
			await capturedApi.validate();

			expect( $termsRow.addClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );

			// clearing the mock history so the expectations are clearer.
			$termsRow.removeClass.mockClear();
			$termsRow.addClass.mockClear();

			// Second validation: marking the terms as checked
			$termsCheckbox.setChecked( true );
			const result = await capturedApi.validate();

			// Should have cleared the invalid state first
			expect( $termsRow.removeClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );
			// Should NOT have re-added the invalid class
			expect( $termsRow.addClass ).not.toHaveBeenCalledWith( 'woocommerce-invalid' );
			// Should pass validation
			expect( result.hasError ).toBe( false );
		} );

		test( 'should allow submission after checking terms following a failed validation', async () => {
			// First attempt: terms not checked - should fail
			$termsCheckbox.setChecked( false );
			const firstResult = await capturedApi.validate();
			expect( firstResult.hasError ).toBe( true );

			// pretending the user checked the terms checkbox
			$termsCheckbox.setChecked( true );

			// Second attempt: should pass on first try (not require double-click)
			const secondResult = await capturedApi.validate();
			expect( secondResult.hasError ).toBe( false );
		} );
	} );

	describe( 'Hidden field validation', () => {
		test( 'should ignore invalid fields that are hidden (e.g. collapsed shipping address)', async () => {
			// A shippable cart renders the "Ship to a different address?" block,
			// whose required shipping fields are present but hidden when the option
			// is unchecked. Field-level validation flags them as invalid regardless
			// of visibility, so validate() must only count *visible* invalid fields
			// or the order is never submitted even when the visible form is valid.
			$termsCheckbox.setChecked( true );
			setHiddenInvalidCount( 5 );

			const result = await capturedApi.validate();

			expect( result.hasError ).toBe( false );
		} );

		test( 'should only count visible invalid fields', async () => {
			$termsCheckbox.setChecked( true );
			setHiddenInvalidCount( 5 );

			await capturedApi.validate();

			expect( $form.find ).toHaveBeenCalledWith(
				'.woocommerce-invalid:visible'
			);
			expect( $form.find ).not.toHaveBeenCalledWith( '.woocommerce-invalid' );
		} );
	} );
} );
