/*
 *  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 bugs

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

final class CustomMetaClassTest {

    @BeforeEach
    void setUp() {
        ExpandoMetaClass.disableGlobally()

        def reg = GroovySystem.metaClassRegistry
        reg.removeMetaClass(String)
        reg.removeMetaClass(this.class)
        reg.removeMetaClass(this.metaClass.class)
    }

    @Test
    void testReplaceMetaClass() {
        /*
         * Constructing first instance before metaclass replacement
         * is made.
         */
        def firstInstance = "first"
        assert "first" == firstInstance.toString()

        MetaClassRegistry registry = GroovySystem.metaClassRegistry
        def stored = registry.getMetaClass(String.class)
        assert stored instanceof MetaClassImpl
        def myMetaClass = new MyDelegatingMetaClass(String.class)
        registry.removeMetaClass String.class
        registry.setMetaClass(String.class, myMetaClass)

        /*
         * Constructing second instance after metaclass replacement
         * is made.
         */
        def secondInstance = "second"

        /*
         * Since we are replacing a metaclass at the class level
         * we are changing the behavior of the first and second
         * instances of the string.
         */
        assert "changed first" == firstInstance.toString()
        assert "changed second" == secondInstance.toString()

        registry.removeMetaClass String.class
        stored = registry.getMetaClass(String.class)
        assert stored instanceof MetaClassImpl
    }

    @Test
    void testNormalCreated() {
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == metaClass.class
        assert MetaClassImpl == metaClass.delegate.class
    }

    @Test
    void testEmcCreated() {
        GroovySystem.metaClassRegistry.removeMetaClass metaClass.theClass
        ExpandoMetaClass.enableGlobally()
        metaClass = GroovySystem.metaClassRegistry.getMetaClass(CustomMetaClassTest)
        assert metaClass instanceof groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass
        assert ExpandoMetaClass == metaClass.delegate.class
        ExpandoMetaClass.disableGlobally()

        GroovySystem.metaClassRegistry.removeMetaClass metaClass.theClass
        metaClass = null
        assert getMetaClass() instanceof org.codehaus.groovy.runtime.HandleMetaClass
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == getMetaClass().delegate.class
    }

    @Test
    void testStaticMetaClass() {
        // Custom metaclass created
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == metaClass.class
        // delegated to MCImpl
        assert MetaClassImpl == metaClass.delegate.class

        MetaClass handle = CustomMetaClassTest.metaClass

        // It should still be custom
        assert org.codehaus.groovy.runtime.HandleMetaClass == handle.class
        // delegated to CustomMetaClassTestMetaClass
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == handle.delegate.class

        // object should still hold reference to old one
        assert MetaClassImpl == metaClass.delegate.class
        // let's give it a chance to reinitialize
        metaClass = null
        // should still be default one
        assert org.codehaus.groovy.runtime.HandleMetaClass == getMetaClass().class
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == getMetaClass().delegate.class

        handle.toString = {
            -> "I am modified"
        }
        // let's give it a chance to reinitialize
        metaClass = null

        assert  "I am modified" == toString()

        assert "I am modified" == metaClass.invokeMethod(this, "toString", null)

        handle.static.toString = {
            -> "I am modified static"
        }

        assert "I am modified static" == getClass().toString()

        GroovySystem.metaClassRegistry.removeMetaClass metaClass.theClass
        metaClass = null
        assert getMetaClass() instanceof org.codehaus.groovy.runtime.HandleMetaClass
        assert groovy.runtime.metaclass.bugs.CustomMetaClassTestMetaClass == getMetaClass().delegate.class
    }

    static class MyDelegatingMetaClass extends groovy.lang.DelegatingMetaClass {
        MyDelegatingMetaClass(final Class a_class) {
            super(a_class);
            initialize()
        }

        public Object invokeMethod(Object a_object, String a_methodName, Object[] a_arguments) {
            return "changed ${super.invokeMethod(a_object, a_methodName, a_arguments)}"
        }
    }
}
