/*
 *  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 groovy.jmx.builder

import groovy.jmx.GroovyMBean
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import javax.management.MBeanServerConnection
import javax.management.ObjectName

import static groovy.test.GroovyAssert.shouldFail

final class JmxBeanExportFactoryTest {

    private JmxBuilder builder
    private MBeanServerConnection server

    @BeforeEach
    void setUp() {
        builder = new JmxBuilder()
        try {
            server = builder.getMBeanServer()
        } catch (e) {
            Assumptions.abort(e.getMessage())
        }
    }

    @Test
    void testBeanExportReplacePolicy() {
        def object = new MockManagedObject()
        // defaults to regpolicy = replace
        def beans = builder.export {
            bean(object)
            bean(object)
        }
        assert beans
        assert beans.size() == 1
    }

    @Test
    void testBeanExportIgnorePolicy() {
        def object = new MockManagedObject()
        def beans = builder.export(regPolicy: 'ignore') {
            bean(object)
            bean(object)
        }
        assert beans
        assert beans.size() == 1
    }

    @Test
    void testBeanExportErrorPolicy() {
        def object = new MockManagedObject()
        shouldFail {
            def beans = builder.export(regPolicy: 'error') {
                bean(object)
                bean(object)
            }
            assert beans
            assert beans.size() == 1
        }
    }

    @Test
    void testImplicitBeanGeneration() {
        def object = new MockManagedObject()
        def objName = "jmx.builder:type=ExportedObject,name=${object.class.canonicalName}@${object.hashCode()}"

        builder.export {
            bean(object)
        }

        GroovyMBean bean = new GroovyMBean(server, new ObjectName(objName))

        assert server.isRegistered(new ObjectName(objName))

        assert bean.info().getConstructors().size() == 3

        assert bean
        assert bean.name().toString() == objName

        assert bean.info().getAttribute('Something').name == 'Something'
        assert bean.info().getAttribute('Something').type == 'java.lang.String'
        assert bean.info().getAttribute('Something').descriptor.getFieldValue('name') == 'Something'
        assert bean.info().getAttribute('Something').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Something').descriptor.getFieldValue('writable')

        assert bean.info().getOperation('doSomething').name == 'doSomething'
        assert bean.info().getOperation('doSomething').signature.size() == 0
        assert bean.info().getOperation('doSomethingElse').name == 'doSomethingElse'
        assert bean.info().getOperation('doSomethingElse').signature.size() == 2
        assert bean.info().getOperation('doSomethingElse').signature[0].type == 'int'
        assert bean.info().getOperation('doSomethingElse').signature[1].type == 'java.lang.String'
    }

    @Test
    void testImplicitWithObjectName() {
        def objName = 'jmx.builder:type=ImplicitDescriptor'
        def object = new MockManagedObject()

        def beans = builder.export {
            bean([target: object, name: objName])
        }

        assert beans

        def bean = beans[0]
        assert bean

        assert bean.info().getConstructors().size() == 3

        assert bean
        assert bean.name().toString() == objName

        assert bean.info().getAttribute('Something')
        assert bean.info().getAttribute('SomethingElse')
        assert bean.info().getOperation('doSomething')
        assert bean.info().getOperation('doSomething').signature.size() == 0
        assert bean.info().getOperation('doSomethingElse')
        assert bean.info().getOperation('doSomethingElse').signature.size() == 2
    }

    @Test
    void testImplicitEmbeddedBeanExport() {
        def object = new BaseEmbeddedClass()
        def objName = "jmx.builder:type=ExportedObject,name=${object.class.canonicalName}@${object.hashCode()}"
        def beans = builder.export {
            bean(object)
        }
        assert beans
        def bean = beans[0]

        assert bean.name().toString() == objName

        assert bean.info().getAttribute('Id').name == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('name') == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Id').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Location').name == 'Location'

        assert bean.info().getAttribute('Name').name == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('name') == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Name').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Available').name == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('name') == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Available').descriptor.getFieldValue('writable')

        assert bean.info().getOperation('doNothing')
        assert bean.info().getOperation('doTwoThings')
        assert bean.info().getOperation('doThreeThings')
        assert bean.info().getOperation('doThreeThings').signature.size() == 3

    }

    @Test
    void testExplicitAttributesDeclaration() {
        def objName = 'jmx.builder:type=ExplicitDescriptor'
        def beans
        def gbean

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, attributes: '*')
        }

        assert beans
        gbean = beans[0]

        assert gbean
        assert gbean.info().getAttribute('Something')
        assert gbean.info().getAttribute('SomethingElse')
        assert gbean.info().getOperations().size() == 3

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, attributes: ['Something', 'SomethingElse'])
        }
        gbean = beans[0]
        assert gbean.info().getAttribute('Something')
        assert gbean.info().getAttribute('SomethingElse')
        assert gbean.info().getOperations().size() == 2

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, attributes: ['Something'])
        }
        gbean = beans[0]
        assert gbean.info().getAttribute('Something')
        assert !gbean.info().getAttribute('SomethingElse')
        assert gbean.info().getOperations().size() == 1

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, attributes: [
                    'Something': [desc: 'Something description', readable: true, writeable: false],
                    'SomethingElse': [readable: true, writable: true, defaultValue: 'Hello']
            ])
        }

        gbean = beans[0]
        assert gbean
        assert gbean.info().getAttribute('Something')
        assert gbean.info().getAttribute('Something').description == 'Something description'
        assert gbean.info().getAttribute('Something').descriptor.getFieldValue('readable')
        assert !gbean.info().getAttribute('Something').descriptor.getFieldValue('writable')
        assert gbean.info().getAttribute('SomethingElse')
        assert gbean.info().getAttribute('SomethingElse').descriptor.getFieldValue('readable')
        assert gbean.info().getAttribute('SomethingElse').descriptor.getFieldValue('writable')

        assert gbean.info().getAttribute('SomethingElse').descriptor.getFieldValue('default').equals('Hello')
        assert gbean.info().getOperations().size() == 3
    }

    @Test
    void testExplicitEmbeddedAttribDeclaration() {
        def object = new EmbeddedAllAttribsOnly()
        def beans = builder.export {
            bean(object)
        }
        assert beans
        def bean = beans[0]

        assert bean.info().getAttribute('Id').name == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('name') == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Id').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Location').name == 'Location'

        assert bean.info().getAttribute('Name').name == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('name') == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Name').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Location').name == 'Location'
        assert bean.info().getAttribute('Location').descriptor.getFieldValue('name') == 'Location'
        assert bean.info().getAttribute('Location').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Location').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Available').name == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('name') == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Available').descriptor.getFieldValue('writable')
    }

    @Test
    void testExplicitConstructorDeclaration() {
        def objName = 'jmx.builder:type=ExplicitDescriptor'
        def beans
        GroovyMBean gbean

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, ctors: '*')
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getConstructors().size() == 3

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, ctors: ['MockManagedObject': []])
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getConstructors().size() == 1
        assert gbean.info().getConstructors()[0].signature.size() == 0

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, ctors: ['MockManagedObject': ['String', 'int']])
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getConstructors().size() == 1
        assert gbean.info().getConstructors()[0].signature.size() == 2
        assert gbean.info().getConstructors()[0].signature[0].type == 'java.lang.String'
        assert gbean.info().getConstructors()[0].signature[1].type == 'int'

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, ctors: [
                    'MockManagedObject': [
                            desc: 'Class creator',
                            params: [
                                    'String': [name: 'initialTarget', desc: 'The constructor target'],
                                    'int': [name: 'quantity']
                            ]
                    ]
            ])
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getConstructors().size() == 1
        assert gbean.info().getConstructors()[0].signature.size() == 2
        assert gbean.info().getConstructors()[0].signature[0].type == 'java.lang.String'
        assert gbean.info().getConstructors()[0].signature[0].name == 'initialTarget'
        assert gbean.info().getConstructors()[0].signature[0].description == 'The constructor target'
        assert gbean.info().getConstructors()[0].signature[1].type == 'int'
    }

    @Test
    void testExplicitOperationDeclaration() {
        def objName = 'jmx.builder:type=ExplicitDescriptor'
        def beans
        GroovyMBean gbean

        beans = builder.export {
            bean(target: new MockManagedObject(), name: objName, ops: '*')
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getOperation('doSomething')
        assert gbean.info().getOperation('doSomethingElse')
        assert gbean.info().getOperation('dontDoThis')
        assert gbean.info().getAttributes().size() == 0
        assert gbean.info().getConstructors().size() == 0

        beans = builder.export {
            bean([target: new MockManagedObject(), name: objName,
                    ops: ['doSomething', 'dontDoThis']
            ])
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getOperation('doSomething')
        assert !gbean.info().getOperation('doSomethingElse')
        assert gbean.info().getOperation('dontDoThis')
        assert gbean.info().getAttributes().size() == 0
        assert gbean.info().getConstructors().size() == 0

        beans = builder.export {
            bean([target: new MockManagedObject(), name: objName,
                    ops: [
                            'doSomething': '*',
                            'doSomethingElse': ['int', 'String']
                    ]
            ])
        }
        gbean = beans[0]
        assert gbean
        assert gbean.info().getOperation('doSomething')
        assert gbean.info().getOperation('doSomethingElse')
        assert !gbean.info().getOperation('dontDoThis')
        assert gbean.info().getAttributes().size() == 0
        assert gbean.info().getConstructors().size() == 0

        beans = builder.export {
            bean([target: new MockManagedObject(), name: objName,
                    ops: [
                            'doSomething': [params: []],

                            'doSomethingElse': [
                                    desc: 'SomethingElse will be done',
                                    params: ['int', 'java.lang.String']
                            ]
                    ]
            ])
        }

        gbean = beans[0]
        assert gbean
        assert gbean.info().getOperation('doSomething')
        assert gbean.info().getOperation('doSomething').signature.size() == 0

        assert gbean.info().getOperation('doSomethingElse')
        assert gbean.info().getOperation('doSomethingElse').description == 'SomethingElse will be done'
        assert gbean.info().getOperation('doSomethingElse').signature.size() == 2
        assert gbean.info().getOperation('doSomethingElse').signature[0].type == 'int'
        assert gbean.info().getOperation('doSomethingElse').signature[1].type == 'java.lang.String'

        assert !gbean.info().getOperation('dontDoThis')
        assert gbean.info().getAttributes().size() == 0
        assert gbean.info().getConstructors().size() == 0

    }

    @Test
    void testExplicitEmbeddedOperations() {
        def object = new EmbeddedAllOps()
        def beans = builder.export {
            bean(object)
        }
        assert beans
        def bean = beans[0]

        assert bean.info().getOperation('doNothing')
        assert bean.info().getOperation('doNothing').signature.size() == 0
        assert bean.info().getOperation('doTwoThings')
        assert bean.info().getOperation('doTwoThings').signature.size() == 2
        assert bean.info().getOperation('doThreeThings')
        assert bean.info().getOperation('doThreeThings').signature.size() == 3
        assert bean.info().getOperation('doThreeThings').signature[0].type == 'java.lang.Object'
        assert bean.info().getOperation('doThreeThings').signature[1].type == 'boolean'
        assert bean.info().getOperation('doThreeThings').signature[2].type == 'int'
    }

    @Test
    void testAttributeChangeListener() {
        def objName = 'jmx.builder:type=ExplicitDescriptor'
        def object = new MockManagedObject()
        def testFlag = '0'

        MockManagedObject.metaClass.'dynaMethod' = {
            testFlag = 'dyna'
        }
        def beans = builder.export {
            bean(target: object, name: objName,
                    attributes: [
                            'Something': [desc: 'Something description', defaultValue: 'Hello', writable: true,
                                    onChange: {event ->
                                        testFlag = 1
                                    }
                            ],

                            'SomethingElse': [
                                    writable: true,
                                    onChange: object.&dynaMethod
                            ]
                    ]
            )
        }

        assert beans
        beans[0].Something = 'A Great Day'
        assert testFlag == 1
        testFlag = '0'
        beans[0].Something = 'A Car'
        assert testFlag == 1

        beans[0].SomethingElse = 3
        assert testFlag == 'dyna'
    }

    @Test
    void testOperationCallListener() {
        def object = new MockManagedObject()
        def testFlag1 = '0'
        def testFlag2 = 1
        def testFlag3 = '2'
        def testFlag4 = 4
        def beans = builder.export {
            bean(
                    target: object, name: 'jmx.builder:type=ExplicitObject',
                    operations: [
                            doSomethingElse: [params: ['int', 'String'],
                                    onCall: {event ->
                                        testFlag1 = '1'
                                    }
                            ],
                            doSomething: [
                                    onCall: {event ->
                                        testFlag2 = '2'
                                    }
                            ],
                            dontDoThis: [
                                    params: ['Object'],
                                    onCall: {->
                                        testFlag3 = 3 + 5
                                    }
                            ],
                            doWork: [
                                    params: ['int', 'String'],
                                    onCall: {->
                                        testFlag4 = 4
                                    }
                            ]
                    ]
            )
        }

        assert beans
        beans[0].doSomethingElse(40, 'foo')
        assert testFlag1 == '1'
        beans[0].doSomething()
        assert testFlag2 == '2'
        beans[0].dontDoThis('work')
        assert testFlag3 == 8
    }

    @Test
    void testExportMBeanObject() {
        def object = new MockSimpleObject(id: '0001', priority: 99)
        def object2 = new MockSimpleObject(id: '0003', priority: 100)

        assert object.id == '0001'
        assert object.priority == 99

        def gbeans = builder.export {
            bean(object)
            bean(target: object2, name: 'jmx.builder:type=MBeanObject')
        }
        assert gbeans
        assert gbeans[0].Id == '0001'
        gbeans[0].Id = '0008'
        shouldFail {
            assert gbeans[0].Id == '0001'
        }

        assert gbeans[1].Id == '0003'
        assert gbeans[1].Priority == 100
    }

    @Test
    void testBeansExport() {
        def object1 = new MockManagedObject()
        def object2 = new BaseEmbeddedClass()

        def mbeans = builder.export {
            beans([object1, object2])
        }
        assert mbeans

        def bean = mbeans[0]
        assert bean.info().getConstructors().size() == 3

        assert bean
        assert bean.info().getAttribute('Something').name == 'Something'
        assert bean.info().getAttribute('Something').type == 'java.lang.String'
        assert bean.info().getAttribute('Something').descriptor.getFieldValue('name') == 'Something'
        assert bean.info().getAttribute('Something').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Something').descriptor.getFieldValue('writable')

        assert bean.info().getOperation('doSomething').name == 'doSomething'
        assert bean.info().getOperation('doSomething').signature.size() == 0
        assert bean.info().getOperation('doSomethingElse').name == 'doSomethingElse'
        assert bean.info().getOperation('doSomethingElse').signature.size() == 2
        assert bean.info().getOperation('doSomethingElse').signature[0].type == 'int'
        assert bean.info().getOperation('doSomethingElse').signature[1].type == 'java.lang.String'

        bean = mbeans[1]
        assert bean.info().getAttribute('Id').name == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('name') == 'Id'
        assert bean.info().getAttribute('Id').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Id').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Location').name == 'Location'

        assert bean.info().getAttribute('Name').name == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('name') == 'Name'
        assert bean.info().getAttribute('Name').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Name').descriptor.getFieldValue('writable')

        assert bean.info().getAttribute('Available').name == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('name') == 'Available'
        assert bean.info().getAttribute('Available').descriptor.getFieldValue('readable')
        assert !bean.info().getAttribute('Available').descriptor.getFieldValue('writable')

        assert bean.info().getOperation('doNothing')
        assert bean.info().getOperation('doTwoThings')
        assert bean.info().getOperation('doThreeThings')
        assert bean.info().getOperation('doThreeThings').signature.size() == 3
    }
}
