/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.portal.resource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URL;
import java.util.*;

import jakarta.servlet.ServletContext;

import org.exoplatform.component.test.web.WebAppImpl;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.test.mocks.servlet.MockServletContext;
import org.exoplatform.test.mocks.servlet.MockServletRequest;
import org.exoplatform.web.ControllerContext;
import org.exoplatform.web.application.javascript.JavascriptConfigParser;
import org.exoplatform.web.application.javascript.JavascriptConfigService;
import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.router.URIWriter;

import org.apache.commons.lang3.StringUtils;
import org.gatein.common.io.IOTools;
import org.gatein.portal.controller.resource.ResourceId;
import org.gatein.portal.controller.resource.ResourceScope;
import org.gatein.portal.controller.resource.script.FetchMode;
import org.gatein.portal.controller.resource.script.ScriptResource;
import org.json.JSONObject;

public class TestJavascriptConfigService extends AbstractWebResourceTest {
    private static final ControllerContext CONTROLLER_CONTEXT = new MockControllerContext();

    private JavascriptConfigService jsService;

    private static ServletContext mockServletContext;

    private static boolean isFirstStartup = true;

    @Override
    protected void setUp() throws Exception {
        final PortalContainer portalContainer = getContainer();
        jsService = (JavascriptConfigService) portalContainer.getComponentInstanceOfType(JavascriptConfigService.class);

        if (isFirstStartup) {
            Map<String, String> resources = new HashMap<String, String>(6);
            resources.put("/js/script1.js", "aaa;");
            resources.put("/js/script2.js", "bbb;");
            resources.put("/js/module1.js", "ccc;");
            resources.put("/js/module2.js", "ddd;");
            resources.put("/js/common.js", "kkk;");
            resources.put("/js/pluginTest.js", "iii;");
            resources.put("/js/exclude.js", "eee;");
            //trim all space character: tab, space, non-breaking space...
            resources.put("/js/normalize_test.js", " \n \t /* // */  //  /* \n  /* /*  */  \n  ggg; // /* */ \n");
            mockServletContext = new MockJSServletContext("mockwebapp", resources);
            jsService.registerContext(new WebAppImpl(mockServletContext, Thread.currentThread().getContextClassLoader()));

            URL url = portalContainer.getPortalClassLoader().getResource("mockwebapp/gatein-resources.xml");
            JavascriptConfigParser.processConfigResource(url.openStream(), jsService, mockServletContext);

            isFirstStartup = false;
        }
    }

    public void testGetScript() throws Exception {
        // no wrapper for SCRIPTS
        String script1 = "aaa;"
                + "\nif (typeof define === 'function' && define.amd && !require.specified('SHARED/script1')) {define('SHARED/script1');}";
        String script2 = "bbb;"
                + "\nif (typeof define === 'function' && define.amd && !require.specified('SHARED/script2')) {define('SHARED/script2');}";
        assertReader(script1, jsService.getScript(new ResourceId(ResourceScope.SHARED, "script1"), null));
        assertReader(script2, jsService.getScript(new ResourceId(ResourceScope.SHARED, "script2"), null));

        // wrapper for MODULE
        // test for Alias : module1 is used with 'm1' alias
        String module1 = "\ndefine('SHARED/module1', [], function() {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[];\neXo.define.deps=[];"
                + "\nreturn ccc;\n});";
        assertReader(module1, jsService.getScript(new ResourceId(ResourceScope.SHARED, "module1"), null));

        // module2 depends on module1
        // test for Alias : module1 is used with 'mod1' alias, module2 use default name for alias
        String module2 = "\ndefine('SHARED/module2', [\"SHARED/module1\"], function(mod1) {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[\"mod1\"];\neXo.define.deps=[mod1];"
                + "\nreturn ddd;\n});";
        assertReader(module2, jsService.getScript(new ResourceId(ResourceScope.SHARED, "module2"), null));
    }

    public void testCommonJS() throws Exception {
        String commonjs = "\ndefine('SHARED/commonjs', [\"require\",\"exports\",\"module\"], function(require,exports,module) {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[\"require\",\"exports\",\"module\"];"
                + "\neXo.define.deps=[eXo.require,exports,module];" + "\nreturn kkk;\n});";
        assertReader(commonjs, jsService.getScript(new ResourceId(ResourceScope.SHARED, "commonjs"), null));
    }

    public void testExcludeJS() throws Exception {
        String excludejs = "\ndefine('SHARED/excludejs', [], function() {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[];"
                + "\neXo.define.deps=[];" + "\nreturn eee;\n});";
        assertReader(excludejs, jsService.getScript(new ResourceId(ResourceScope.SHARED, "excludejs"), null));
    }

    public void testRequireJSPlugin() throws Exception {
        String pluginTest = "\ndefine('SHARED/pluginTest', [\"SHARED/text!/path/to/file.js\",\"SHARED/text!/path/to/file2.js\"],"
                + " function(text,text_1) {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\n"
                + "eXo.define.names=[\"text!/path/to/file.js\",\"text!/path/to/file2.js\"];"
                + "\neXo.define.deps=[text,text_1];" + "\nreturn iii;\n});";
        assertReader(pluginTest, jsService.getScript(new ResourceId(ResourceScope.SHARED, "pluginTest"), null));
    }

    public void testGroupingScript() throws Exception {
        String module1 = "\n//Begin SHARED/module1\n\ndefine('SHARED/module1', [], function() {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[];\neXo.define.deps=[];"
                + "\nreturn ccc;\n});\n//End SHARED/module1";
        String module2 = "\n//Begin SHARED/module2\n\ndefine('SHARED/module2', [\"SHARED/module1\"], function(mod1) {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[\"mod1\"];\neXo.define.deps=[mod1];"
                + "\nreturn ddd;\n});\n//End SHARED/module2";

        StringWriter buffer = new StringWriter();
        IOTools.copy(jsService.getScript(new ResourceId(ResourceScope.GROUP, "fooGroup"), null), buffer);

        // the order of module1, and module2 in group is not known
        String result = buffer.toString();
        result = result.replace(module1, "");
        result = result.replace(module2, "");
        assertTrue(result.trim().isEmpty());
    }

    public void testGetJSConfig() throws Exception {
        JSONObject config = jsService.getJSConfig();

        // All SCRIPTS and remote resource have to had dependencies declared in shim configuration
        JSONObject shim = config.getJSONObject("shim");
        assertNotNull(shim);

        JSONObject remoteDep = shim.getJSONObject("remote2");
        assertNotNull(remoteDep);
        assertEquals("{\"deps\":[\"remote1\"]}", remoteDep.toString());

        JSONObject scriptDep = shim.getJSONObject("SHARED/script2");
        assertNotNull(scriptDep);
        assertEquals("{\"deps\":[\"SHARED/script1\"]}", scriptDep.toString());

        // requireJS need a map of all resourceIDs and urls
        // requireJS add ".js" automatically so we truncate it from url
        JSONObject paths = config.getJSONObject("paths");
        assertNotNull(paths);
        // Return remote module/script url as it's declared in gatein-resources.xml
        assertEquals("http://js/remote1.js", paths.getString("remote1"));
        assertEquals("http://js/remote2.js", paths.getString("remote2"));

        // module1 and module2 are grouped
        assertTrue(paths.getString("SHARED/module1").contains("/mockwebapp/js/fooGroup.js"));
        assertTrue(paths.getString("SHARED/module2").contains("/mockwebapp/js/fooGroup.js"));

        // navController url for scripts
        assertTrue(paths.getString("SHARED/script1").contains("/mockwebapp/js/script1.js"));
        assertTrue(paths.getString("SHARED/script2").contains("/mockwebapp/js/script2.js"));
    }

    public void testGenerateURL() throws Exception {
        ResourceId remote1 = new ResourceId(ResourceScope.SHARED, "remote1");
        String remoteURL = jsService.generateUrl(remote1);
        // Return remote module/script url as it's declared in gatein-resources.xml
        assertEquals("http://js/remote1.js", remoteURL);

        ResourceId module1 = new ResourceId(ResourceScope.SHARED, "module1");
        remoteURL = jsService.generateUrl(module1);
        assertEquals("/mockwebapp/js/module1.js?hash=-55414319&scope=SHARED&minify=true", remoteURL);
    }

    public void testGenerateURLWithLocale() throws Exception {
      ScriptResource resource = jsService.getResource(new ResourceId(ResourceScope.SHARED, "text"));
      resource.addSupportedLocale(Locale.ENGLISH);
      resource.addSupportedLocale(Locale.FRENCH);

      JSONObject config = jsService.getJSConfig();
      assertNotNull(config);
      assertNotNull(config.getJSONObject("paths"));
      assertNotNull(config.getJSONObject("paths").getString("SHARED/text"));
      assertEquals("/mockwebapp/js/text.js?hash=1278397679&scope=SHARED&minify=true", config.getJSONObject("paths").getString("SHARED/text"));

      config = jsService.getJSConfig();
      assertNotNull(config);
      assertNotNull(config.getJSONObject("paths"));
      assertNotNull(config.getJSONObject("paths").getString("SHARED/text"));
      assertTrue(config.getJSONObject("paths").getString("SHARED/text").contains("/mockwebapp/js/text.js"));
    }

    public void testNormalize() throws Exception {
        String normalizedJS = "\ndefine('SHARED/normalize_test', [], function() {"
                + "\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;\neXo.define.names=[];\neXo.define.deps=[];"
                + "\nreturn ggg; // /* */ \n\n});";
        assertReader(normalizedJS, jsService.getScript(new ResourceId(ResourceScope.SHARED, "normalize_test"), null));
    }

    public void testResolveIDs() {
        Map<ResourceId, FetchMode> tmp = new HashMap<ResourceId, FetchMode>();
        tmp.put(new ResourceId(ResourceScope.SHARED, "script2"), null);

        Map<ScriptResource, FetchMode> ids = jsService.resolveIds(tmp);
        // Only resolve dependencies for SCRIPTS
        assertEquals(2, ids.size());
        List<ScriptResource> result = new ArrayList<ScriptResource>(ids.keySet());
        assertEquals("script1", result.get(0).getId().getName());
        assertEquals("script2", result.get(1).getId().getName());

        tmp.clear();
        // module2 depends on module1 but we don't resolve dependencies for it
        tmp.put(new ResourceId(ResourceScope.SHARED, "module2"), null);
        ids = jsService.resolveIds(tmp);
        assertEquals(1, ids.size());
        assertEquals("module2", ids.keySet().iterator().next().getId().getName());
    }

    /*
     * public void testCommonJScripts() { assertEquals(5, jsService.getCommonJScripts().size());
     * assertTrue(jsService.isModuleLoaded("js.test1")); assertTrue(jsService.isModuleLoaded("js.test2"));
     * assertTrue(jsService.isModuleLoaded("js.test3")); assertTrue(jsService.isModuleLoaded("js.test4"));
     * assertTrue(jsService.isModuleLoaded("js.test7"));
     *
     * assertFalse(jsService.isModuleLoaded("js.test5"));
     *
     * // InputStream script = jsService.getScript(new ResourceId(ResourceScope.SHARED, "common"), "js.test1");
     * assertNotNull(script); }
     */

    private void assertReader(String expected, Reader actual) throws Exception {
        StringWriter buffer = new StringWriter();
        IOTools.copy(actual, buffer, 1);
        assertEquals(expected, buffer.toString());
    }

    /*
     * public void testPortalJScript() throws IOException { Collection<Javascript> site = jsService.getPortalJScripts("site1");
     * assertEquals(1, site.size()); Iterator<Javascript> iterator = site.iterator();
     * assertEquals(mockServletContext.getContextPath() + "/js/test5.js", iterator.next().getPath());
     *
     * // InputStream script = jsService.getScript(new ResourceId(ResourceScope.PORTAL, "site1")); assertNotNull(script);
     * assertEquals("", read(script));
     *
     * site = jsService.getPortalJScripts("site2"); assertEquals(2, site.size()); iterator = site.iterator();
     * assertEquals(mockServletContext.getContextPath() + "/js/test6.js", iterator.next().getPath());
     * assertEquals(mockServletContext.getContextPath() + "/js/test5.js", iterator.next().getPath());
     *
     * // script = jsService.getScript(new ResourceId(ResourceScope.PORTAL, "site2")); assertNotNull(script); assertEquals("",
     * read(script));
     *
     * // assertNull(jsService.getPortalJScripts("classic")); assertNull(jsService.getScript(new
     * ResourceId(ResourceScope.PORTAL, "classic")));
     *
     * // jsService.removePortalJScripts("site1"); assertNull(jsService.getPortalJScripts("site1"));
     * assertNull(jsService.getScript(new ResourceId(ResourceScope.PORTAL, "site1")));
     *
     * // Javascript portalJScript = Javascript.create(new ResourceId(ResourceScope.PORTAL, "/portal"), "portal1",
     * "/mockwebapp", "/portal", Integer.MAX_VALUE); jsService.addPortalJScript(portalJScript); String jScript =
     * jsService.getJScript(portalJScript.getPath()); assertNull(jScript); assertEquals("", read(script));
     * resResolver.addResource(portalJScript.getPath(), "bar1"); jScript = jsService.getJScript(portalJScript.getPath());
     * assertEquals("bar1", jScript); }
     */

    private static class MockControllerContext extends ControllerContext {
        public MockControllerContext() {
            super(null, null, new MockServletRequest(null, null), null, null);
        }

        @Override
        public void renderURL(Map<QualifiedName, String> parameters, URIWriter uriWriter) throws IOException {
            String lang = parameters.get(QualifiedName.create("gtn", "lang"));
            lang = StringUtils.isBlank(lang) ? "" : "-" + lang;
            uriWriter.appendSegment("mock_url_of_" + parameters.get(QualifiedName.create("gtn", "resource")) + lang + ".js");
        }
    }

    private static class MockJSServletContext extends MockServletContext {
        private Map<String, String> resources;

        public MockJSServletContext(String contextName, Map<String, String> resources) {
            super(contextName);
            this.resources = resources;
        }

        public String getContextPath() {
            return "/" + getServletContextName();
        }

        @Override
        public InputStream getResourceAsStream(String s) {
            String input = resources.get(s);
            if (input != null) {
                return new ByteArrayInputStream(input.getBytes());
            } else {
                return null;
            }
        }
    }
}
