/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.tests;

import org.junit.Test;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

/**
 * Unit tests for <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=374918">Bug 374918 - String
 * primitive prototype wrongly resolved when used with many top scopes</a>
 *
 * @author Marc Guillemot
 */
public class PrimitiveTypeScopeResolutionTest {
    /** */
    @Test
    public void functionCall() {
        String str2 =
                "function f() {\n"
                        + "String.prototype.foo = function() { return 'from 2' }; \n"
                        + "var s2 = 's2';\n"
                        + "var s2Foo = s2.foo();\n"
                        + "if (s2Foo != 'from 2') throw 's2 got: ' + s2Foo;\n" // fails
                        + "}";

        String str1 = "String.prototype.foo = function() { return 'from 1'};" + "scope2.f()";
        testWithTwoScopes(str1, str2);
    }

    /** */
    @Test
    public void propertyAccess() {
        String str2 =
                "function f() { String.prototype.foo = 'from 2'; \n"
                        + "var s2 = 's2';\n"
                        + "var s2Foo = s2.foo;\n"
                        + "if (s2Foo != 'from 2') throw 's2 got: ' + s2Foo;\n" // fails
                        + "}";

        String str1 = "String.prototype.foo = 'from 1'; scope2.f()";
        testWithTwoScopes(str1, str2);
    }

    /** */
    @Test
    public void elementAccess() {
        String str2 =
                "function f() { String.prototype.foo = 'from 2'; \n"
                        + "var s2 = 's2';\n"
                        + "var s2Foo = s2['foo'];\n"
                        + "if (s2Foo != 'from 2') throw 's2 got: ' + s2Foo;\n" // fails
                        + "}";

        String str1 = "String.prototype.foo = 'from 1'; scope2.f()";
        testWithTwoScopes(str1, str2);
    }

    private void testWithTwoScopes(final String scriptScope1, final String scriptScope2) {
        Utils.runWithAllModes(
                cx -> {
                    final Scriptable scope1 =
                            cx.initStandardObjects(new MySimpleScriptableObject("scope1"));
                    final Scriptable scope2 =
                            cx.initStandardObjects(new MySimpleScriptableObject("scope2"));
                    cx.evaluateString(scope2, scriptScope2, "source2", 1, null);

                    scope1.put("scope2", scope1, scope2);

                    return cx.evaluateString(scope1, scriptScope1, "source1", 1, null);
                });
    }

    /** Simple utility allowing to better see the concerned scope while debugging */
    static class MySimpleScriptableObject extends ScriptableObject {
        private static final long serialVersionUID = 1L;
        private String label_;

        MySimpleScriptableObject(String label) {
            label_ = label;
        }

        @Override
        public String getClassName() {
            return "MySimpleScriptableObject";
        }

        @Override
        public String toString() {
            return label_;
        }
    }

    public static class MyObject extends ScriptableObject {
        private static final long serialVersionUID = 1L;

        @Override
        public String getClassName() {
            return "MyObject";
        }

        public Object readPropFoo(final Scriptable s) {
            return ScriptableObject.getProperty(s, "foo");
        }
    }

    /** Test that FunctionObject use the right top scope to convert a primitive to an object */
    @Test
    public void functionObjectPrimitiveToObject() throws Exception {
        final String scriptScope2 =
                "function f() {\n"
                        + "String.prototype.foo = 'from 2'; \n"
                        + "var s2 = 's2';\n"
                        + "var s2Foo = s2.foo;\n"
                        + "var s2FooReadByFunction = myObject.readPropFoo(s2);\n"
                        + "if (s2Foo != s2FooReadByFunction)\n"
                        + "throw 's2 got: ' + s2FooReadByFunction;\n"
                        + "}";

        // define object with custom method
        final MyObject myObject = new MyObject();
        final String[] functionNames = {"readPropFoo"};
        myObject.defineFunctionProperties(functionNames, MyObject.class, ScriptableObject.EMPTY);

        final String scriptScope1 = "String.prototype.foo = 'from 1'; scope2.f()";

        Utils.runWithAllModes(
                cx -> {
                    final Scriptable scope1 =
                            cx.initStandardObjects(new MySimpleScriptableObject("scope1"));
                    final Scriptable scope2 =
                            cx.initStandardObjects(new MySimpleScriptableObject("scope2"));

                    scope2.put("myObject", scope2, myObject);
                    cx.evaluateString(scope2, scriptScope2, "source2", 1, null);

                    scope1.put("scope2", scope1, scope2);

                    return cx.evaluateString(scope1, scriptScope1, "source1", 1, null);
                });
    }
}
