/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.testdomain.conf;

import java.util.ArrayList;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import jakarta.inject.Inject;

import org.apache.wicket.Component;
import org.apache.wicket.IPageFactory;
import org.apache.wicket.Page;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.core.request.handler.IPageProvider;
import org.apache.wicket.markup.head.ResourceAggregator;
import org.apache.wicket.markup.head.filter.JavaScriptFilteredIntoFooterHeaderResponse;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.settings.DebugSettings.ClassOutputStrategy;
import org.apache.wicket.util.tester.WicketTester;
import org.junit.jupiter.api.function.ThrowingConsumer;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.testdomain.util.dto.BookDto;
import org.apache.causeway.testdomain.util.dto.IBook;
import org.apache.causeway.viewer.wicket.model.causeway.WicketApplicationInitializer;
import org.apache.causeway.viewer.wicket.model.models.PageType;
import org.apache.causeway.viewer.wicket.model.util.PageParameterUtils;
import org.apache.causeway.viewer.wicket.ui.app.registry.ComponentFactoryRegistry;
import org.apache.causeway.viewer.wicket.ui.app.registry.HasComponentFactoryRegistry;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.FieldFrame;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.RegularFrame;
import org.apache.causeway.viewer.wicket.ui.pages.PageClassRegistry;
import org.apache.causeway.viewer.wicket.ui.pages.obj.DomainObjectPage;
import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer;
import org.apache.causeway.viewer.wicket.viewer.wicketapp.CausewayWicketAjaxRequestListenerUtil;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;

import de.agilecoders.wicket.core.Bootstrap;
import de.agilecoders.wicket.core.settings.BootstrapSettings;
import de.agilecoders.wicket.core.settings.IBootstrapSettings;

@Configuration
@Import({
    CausewayModuleViewerWicketViewer.class,
    Configuration_usingWicket.WicketViewerOutputMarkupContainerClassNameEnable.class
})
public class Configuration_usingWicket {

    @Configuration
    public class WicketViewerOutputMarkupContainerClassNameEnable
    implements WicketApplicationInitializer {

        @Override
        public void init(final WebApplication webApplication) {
            webApplication.getDebugSettings()
                .setComponentPathAttributeName("wicket-tester-path")
                .setOutputMarkupContainerClassNameStrategy(ClassOutputStrategy.HTML_COMMENT);
        }
    }

    public static class DomainObjectPageTester
    extends WicketTester
    implements HasMetaModelContext {

        // -- HOMEPAGE (TEST APP)

        public static final String OPEN_SAMPLE_ACTION = "theme:domainObjectContainer:domainObject:rows:1"
                + ":rowContents:1"
                + ":col:objectHeaderPanel:objectActions:additionalLinkList:additionalLinkItem:1"
                + ":actionLink";

        public static final String OPEN_SAMPLE_ACTION_TITLE = OPEN_SAMPLE_ACTION
                + ":additionalLinkTitle";

        // -- BOOK PAGE

        /** strange asymmetry with JPA, to investigate another day */
        public static final String BOOK_DELETE_ACTION_JDO = "theme:domainObjectContainer:domainObject:rows:2"
                + ":rowContents:1"
                + ":col:rows:1:rowContents:1:col:tabGroups:1"
                + ":panel:tabPanel"
                + ":rows:1:rowContents:1"
                + ":col:fieldSets:1:memberGroup"
                + ":panelHeading:associatedActionLinksPanel"
                + ":additionalLinkList:additionalLinkItem:0:actionLink";

        /** strange asymmetry with JDO, to investigate another day */
        public static final String BOOK_DELETE_ACTION_JPA = "theme:domainObjectContainer:domainObject:rows:2"
                + ":rowContents:1"
                + ":col:rows:1:rowContents:1:col:tabGroups:1"
                + ":1:rowContents:1"
                + ":col:fieldSets:1:memberGroup"
                + ":panelHeading:associatedActionLinksPanel"
                + ":additionalLinkList:additionalLinkItem:0:actionLink";

        // -- GENERIC STANDALONE COLLECTION

        public static final String STANDALONE_COLLECTION = "theme:standaloneCollection:standaloneCollection";
        public static final String STANDALONE_COLLECTION_LABEL = STANDALONE_COLLECTION
                + ":actionName";

        // -- INVENTORY JAXB VM

        public static final String FAVORITE_BOOK_SCALAR = "theme:domainObjectContainer:domainObject:rows:2"
                + ":rowContents:1"
                + ":col:fieldSets:1"
                + ":memberGroup:properties:2"
                + ":property:scalarTypeContainer:scalarIfRegular";

        public static final String FAVORITE_BOOK_SCALAR_NAME = FAVORITE_BOOK_SCALAR
                + ":scalarNameBeforeValue";

        public static final String FAVORITE_BOOK_ENTITY_LINK = FAVORITE_BOOK_SCALAR
                + ":container-fieldFrame:scalarValueInlinePromptLink:container-scalarValue:objectLink"
                + ":objectIconAndTitle:objectLinkWrapper:objectLink";

        public static final String FAVORITE_BOOK_ENTITY_LINK_TITLE = FAVORITE_BOOK_ENTITY_LINK
                + ":objectTitle";

        @RequiredArgsConstructor
        public static enum SimulatedProperties implements SimulatedProperty {
            INVENTORY_NAME("theme:domainObjectContainer:domainObject:rows:2"
                    + ":rowContents:1"
                    + ":col:fieldSets:1"
                    + ":memberGroup:properties:1"
                    + ":property"),
            JDO_BOOK_ISBN("theme:domainObjectContainer:domainObject:rows:2"
                    + ":rowContents:1:col:rows:1:rowContents:1"
                    + ":col:tabGroups:1:panel:tabPanel:rows:1:rowContents:1:col"
                    + ":fieldSets:1"
                    + ":memberGroup:properties:3"
                    + ":property"),
            JPA_BOOK_ISBN("theme:domainObjectContainer:domainObject:rows:2"
                    + ":rowContents:1:col:rows:1:rowContents:1"
                    + ":col:tabGroups:1:1"
                    + ":rowContents:1:col"
                    + ":fieldSets:1"
                    + ":memberGroup:properties:4"
                    + ":property"
                    );
            @Getter @Accessors(fluent=true) final String id;
        }

        public static interface SimulatedProperty {
            String id();
            default String editLink() {
                return id() + ":scalarTypeContainer:scalarIfRegular:container-fieldFrame"
                        + ":scalarValueInlinePromptLink";
            }
            default String editInlineForm() {
                return id() + ":scalarTypeContainer:scalarIfRegularInlinePromptForm:inputForm";
            }
            default String scalarField() {
                return "property:attributeNameAndValue:scalarTypeContainer:scalarIfRegular:"
                    + RegularFrame.FIELD.getContainerId() + ":"
                    + FieldFrame.SCALAR_VALUE_CONTAINER.getContainerId() + ":scalarValue";
            }
            default String editInlineFormTextField() {
                return id() + ":" + scalarField();
            }
            default String editInlinePromptForm() {
                return editInlineForm();
                //return id() + ":scalarTypeContainer:scalarIfRegularInlinePromptForm:inputForm";
            }
            default String editInlinePromptFormOk() {
                return editInlinePromptForm() + ":okButton";
            }
        }

        // --

        @Getter
        private final MetaModelContext metaModelContext;
        private final Function<BookDto, IBook> bookFactory;

        public DomainObjectPageTester(
                final MetaModelContext metaModelContext,
                final Function<BookDto, IBook> bookFactory) {
            super(newWicketApplication(metaModelContext));
            this.metaModelContext = metaModelContext;
            metaModelContext.injectServicesInto(this);
            this.bookFactory = bookFactory;

        }

        public PageParameters createPageParameters(final Object entityOrVm) {
            final ManagedObject domainObject = getObjectManager().adapt(entityOrVm);
            return PageParameterUtils.createPageParametersForObject(domainObject);
        }

        public void assertPageTitle(final String expectedLabel) {
            assertLabel("pageTitle", expectedLabel);
        }

        public void assertHeaderBrandText(final String expectedLabel) {
            assertLabel("header:applicationName:brandText", expectedLabel);
        }

        @SneakyThrows
        public void assertPropertyValue(final String path, final ThrowingConsumer<ManagedObject> checker) {
            Component component = getComponentFromLastRenderedPage(path);
            ManagedObject adapter = (ManagedObject)component.getDefaultModelObject();
            checker.accept(adapter);
        }

        public void assertFavoriteBookIs(final BookDto bookDto) {
            assertLabel(FAVORITE_BOOK_SCALAR_NAME, "Favorite Book");
            assertComponent(FAVORITE_BOOK_ENTITY_LINK, BookmarkablePageLink.class);

            var expectedLinkTitle = bookFactory.apply(bookDto).title();
            assertLabel(FAVORITE_BOOK_ENTITY_LINK_TITLE, expectedLinkTitle);
        }

        public void assertInventoryNameIs(final String expectedName) {
            assertPropertyValue(SimulatedProperties.INVENTORY_NAME.id(), adapter->{
                assertEquals(expectedName, adapter.getPojo());
            });
        }

        public void dumpComponentTree(final Predicate<Component> filter) {
            getLastRenderedPage().visitChildren((component, visit) -> {
                if(filter.test(component)) {

                    var inversePath = new ArrayList<String>();
                    var comp = component;

                    while(comp!=null) {
                        inversePath.add(comp.getId());
                        comp = comp.getParent();
                    }

                    var path = Can.ofCollection(inversePath)
                    .reverse()
                    .stream()
                    .skip(1L)
                    .collect(Collectors.joining(":"));

                    System.err.printf("comp[%s]: %s -> %s: %s%n",
                            path, component, component.getClass().getSimpleName(),
                            component.getDefaultModelObjectAsString());
                }
            });
        }

        /**
         * Renders the {@link DomainObjectPage}.
         * @see #startPage(IPageProvider)
         */
        public DomainObjectPage startDomainObjectPage(final PageParameters pageParameters) {
            var domainObjectPage = DomainObjectPage.forPageParameters(pageParameters);
            var startedPage = startPage(domainObjectPage);
            assertRenderedPage(DomainObjectPage.class);
            return startedPage;
        }

    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public static class WicketTesterFactory {
        private final MetaModelContext commonContext;
        public DomainObjectPageTester createTester(final Function<BookDto, IBook> bookFactory) {
            return new DomainObjectPageTester(commonContext, bookFactory);
        }
    }

    @Bean @Inject
    public WicketTesterFactory wicketTesterFactory(final MetaModelContext commonContext) {
        return new WicketTesterFactory(commonContext);
    }

    // -- HELPER -- APPLICATION (WICKET)

    private static WebApplication newWicketApplication(final MetaModelContext commonContext) {
        var wicketApplication = new WicketApplication_forTesting(commonContext);
        ThreadContext.setApplication(wicketApplication);
        return wicketApplication;
    }

    @RequiredArgsConstructor
    private static class PageFactory_forTesting implements IPageFactory {

        private final IPageFactory delegate;

        @Override
        public <C extends IRequestablePage> C newPage(final Class<C> pageClass, final PageParameters parameters) {
            if(DomainObjectPage.class.equals(pageClass)) {
                return _Casts.uncheckedCast(DomainObjectPage.forPageParameters(parameters));
            }
            return delegate.newPage(pageClass, parameters);
        }

        @Override
        public <C extends IRequestablePage> C newPage(final Class<C> pageClass) {
            if(DomainObjectPage.class.equals(pageClass)) {
                throw _Exceptions.illegalArgument("cannot instantiate DomainObjectPage without PageParameters");
            }
            return delegate.newPage(pageClass);
        }

        @Override
        public <C extends IRequestablePage> boolean isBookmarkable(final Class<C> pageClass) {
            if(DomainObjectPage.class.equals(pageClass)) {
                return true;
            }
            return delegate.isBookmarkable(pageClass);
        }
    }

    @RequiredArgsConstructor
    static class WicketApplication_forTesting
    extends WebApplication
    implements
        HasComponentFactoryRegistry{
        private static final long serialVersionUID = 1L;

        @Override
        protected void init() {
            super.init();
            getCspSettings().blocking().disabled(); // since Wicket 9, CSP is enabled by default [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP]
            final IBootstrapSettings settings = new BootstrapSettings();
            settings.setDeferJavascript(false);
            Bootstrap.install(this, settings);
            getHeaderResponseDecorators().add(response ->
                new ResourceAggregator(new JavaScriptFilteredIntoFooterHeaderResponse(response, "footerJS")));
            //XXX set to false for less strict testing
            getDebugSettings().setComponentUseCheck(false);
        }

        @Getter
        private final MetaModelContext metaModelContext;

        @Getter(lazy=true)
        private final ComponentFactoryRegistry componentFactoryRegistry =
                metaModelContext.lookupServiceElseFail(ComponentFactoryRegistry.class);

        @Getter(lazy=true)
        private final PageClassRegistry pageClassRegistry =
                metaModelContext.lookupServiceElseFail(PageClassRegistry.class);

        @Override
        public Class<? extends Page> getHomePage() {
            return getPageClassRegistry().getPageClass(PageType.HOME);
        }

        @Override
        protected IPageFactory newPageFactory() {
            return new PageFactory_forTesting(super.newPageFactory());
        }

        @Override
        protected void internalInit() {
            super.internalInit();
            // intercept AJAX requests and reload view-models so any detached entities are re-fetched
            CausewayWicketAjaxRequestListenerUtil.setRootRequestMapper(this, metaModelContext);
        }

    }

}
