/*
 * 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.deltaspike.cdise.tck;


import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.context.SessionScoped;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;

import org.apache.deltaspike.cdise.api.CdiContainer;
import org.apache.deltaspike.cdise.api.CdiContainerLoader;
import org.apache.deltaspike.cdise.api.ContextControl;
import org.apache.deltaspike.cdise.tck.beans.Car;
import org.apache.deltaspike.cdise.tck.beans.CarRepair;
import org.apache.deltaspike.cdise.tck.beans.TestUser;
import org.apache.deltaspike.test.control.LockedImplementation;
import org.apache.deltaspike.test.control.LockedVersionRange;
import org.apache.deltaspike.test.control.VersionControlRule;
import org.apache.deltaspike.test.utils.Implementation;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;

/**
 * TCK test for the {@link org.apache.deltaspike.cdise.api.CdiContainer}
 */
public class ContainerCtrlTckTest
{
    private static final Logger log = Logger.getLogger(ContainerCtrlTckTest.class.getName());
    private static final int NUM_THREADS = 10;

    @Rule
    public VersionControlRule versionControlRule = new VersionControlRule();

    @Test
    public void testContainerBoot()
    {
        CdiContainer cc = CdiContainerLoader.getCdiContainer();
        Assert.assertNotNull(cc);

        cc.boot();
        cc.getContextControl().startContexts();

        BeanManager bm = cc.getBeanManager();
        Assert.assertNotNull(bm);
        
        Set<Bean<?>> beans = bm.getBeans(CarRepair.class);
        Bean<?> bean = bm.resolve(beans);
        
        CarRepair carRepair = (CarRepair) bm.getReference(bean, CarRepair.class, bm.createCreationalContext(bean));
        Assert.assertNotNull(carRepair);

        Assert.assertNotNull(carRepair.getCar());
        Assert.assertNotNull(carRepair.getCar().getUser());

        cc.shutdown();
    }

    @Test
    public void testParallelThreadExecution() throws Exception
    {
        final CdiContainer cc = CdiContainerLoader.getCdiContainer();
        Assert.assertNotNull(cc);

        cc.boot();
        cc.getContextControl().startContexts();

        final BeanManager bm = cc.getBeanManager();
        Assert.assertNotNull(bm);

        final AtomicInteger numErrors = new AtomicInteger(0);
        final ContextControl contextControl = cc.getContextControl();

        Runnable runnable = new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    contextControl.startContext(SessionScoped.class);
                    contextControl.startContext(RequestScoped.class);


                    Set<Bean<?>> beans = bm.getBeans(CarRepair.class);
                    Bean<?> bean = bm.resolve(beans);

                    CarRepair carRepair = (CarRepair)
                            bm.getReference(bean, CarRepair.class, bm.createCreationalContext(bean));
                    Assert.assertNotNull(carRepair);

                    for (int i = 0; i < 100000; i++)
                    {
                        // we need the threads doing something ;)
                        Assert.assertNotNull(carRepair.getCar());
                        Assert.assertNotNull(carRepair.getCar().getUser());
                        Assert.assertNull(carRepair.getCar().getUser().getName());
                    }
                    contextControl.stopContext(RequestScoped.class);
                    contextControl.stopContext(SessionScoped.class);
                }
                catch (Throwable e)
                {
                    log.log(Level.SEVERE, "An exception happened on a new worker thread", e);
                    numErrors.incrementAndGet();
                }
            }
        };


        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0 ; i < NUM_THREADS; i++)
        {
            threads[i] = new Thread(runnable);
        }

        for (int i = 0 ; i < NUM_THREADS; i++)
        {
            threads[i].start();
        }

        for (int i = 0 ; i < NUM_THREADS; i++)
        {
            threads[i].join();
        }

        Assert.assertEquals("An error happened while executing parallel threads", 0, numErrors.get());


        cc.shutdown();
    }

    /**
     * Stops and starts: application-, session- and request-scope.
     * <p/>
     * application-scoped instance has a ref to
     * request-scoped instance which has a ref to
     * session-scoped instance.
     * <p/>
     * If the deepest ref has the expected value, all levels in between were resetted correctly.
     */
    @Test
    public void testRestartContexts()
    {
        CdiContainer cdiContainer = CdiContainerLoader.getCdiContainer();
        Assert.assertNotNull(cdiContainer);

        cdiContainer.boot();
        cdiContainer.getContextControl().startContexts();

        BeanManager beanManager = cdiContainer.getBeanManager();
        Assert.assertNotNull(beanManager);

        Set<Bean<?>> beans = beanManager.getBeans(CarRepair.class);
        Bean<?> bean = beanManager.resolve(beans);

        CarRepair carRepair = (CarRepair)
            beanManager.getReference(bean, CarRepair.class, beanManager.createCreationalContext(bean));

        Assert.assertNotNull(carRepair);

        Car car = carRepair.getCar();

        Assert.assertNotNull(car);
        Assert.assertNotNull(car.getUser());


        carRepair.getCar().getUser().setName("tester");
        Assert.assertEquals("tester", car.getUser().getName());

        Assert.assertFalse(CarRepair.isPreDestroyCalled());
        Assert.assertFalse(Car.isPreDestroyCalled());
        Assert.assertFalse(TestUser.isPreDestroyCalled());

        cdiContainer.getContextControl().stopContexts();

        Assert.assertTrue(CarRepair.isPreDestroyCalled());
        Assert.assertTrue(Car.isPreDestroyCalled());
        Assert.assertTrue(TestUser.isPreDestroyCalled());

        try
        {
            car.getUser();

            // accessing the car should have triggered a ContextNotActiveException
            Assert.fail();
        }
        catch (ContextNotActiveException e)
        {
            //do nothing - exception expected
        }

        cdiContainer.getContextControl().startContexts();

        carRepair = (CarRepair)
            beanManager.getReference(bean, CarRepair.class, beanManager.createCreationalContext(bean));

        Assert.assertNotNull(carRepair.getCar());
        Assert.assertNotNull(carRepair.getCar().getUser());
        Assert.assertNull(carRepair.getCar().getUser().getName());

        cdiContainer.shutdown();
    }

    @LockedImplementation(versions = {
            @LockedVersionRange(implementation = Implementation.WELD11, versionRange = "[1.1.14,1.2)"),
            @LockedVersionRange(implementation = Implementation.WELD20, versionRange = "[2.0.1.Final,2.1)")
        })
    @Test
    public void testShutdownWithInactiveContexts()
    {
        CdiContainer cdiContainer = CdiContainerLoader.getCdiContainer();
        Assert.assertNotNull(cdiContainer);

        cdiContainer.boot();
        cdiContainer.getContextControl().startContexts();

        // now do some random stuff
        BeanManager beanManager = cdiContainer.getBeanManager();
        Assert.assertNotNull(beanManager);

        Set<Bean<?>> beans = beanManager.getBeans(CarRepair.class);
        Bean<?> bean = beanManager.resolve(beans);

        CarRepair carRepair = (CarRepair)
                beanManager.getReference(bean, CarRepair.class, beanManager.createCreationalContext(bean));

        Assert.assertNotNull(carRepair);

        Car car = carRepair.getCar();

        Assert.assertNotNull(car);
        Assert.assertNotNull(car.getUser());


        carRepair.getCar().getUser().setName("tester");
        Assert.assertEquals("tester", car.getUser().getName());

        Assert.assertFalse(CarRepair.isPreDestroyCalled());
        Assert.assertFalse(Car.isPreDestroyCalled());
        Assert.assertFalse(TestUser.isPreDestroyCalled());

        cdiContainer.getContextControl().stopContexts();

        Assert.assertTrue(CarRepair.isPreDestroyCalled());
        Assert.assertTrue(Car.isPreDestroyCalled());
        Assert.assertTrue(TestUser.isPreDestroyCalled());

        cdiContainer.shutdown();
    }

    @Test
    public void testNewRequests()
    {
        CdiContainer cdiContainer = CdiContainerLoader.getCdiContainer();
        Assert.assertNotNull(cdiContainer);

        cdiContainer.boot();
        cdiContainer.getContextControl().startContext(SessionScoped.class);
        cdiContainer.getContextControl().startContext(RequestScoped.class);

        BeanManager beanManager = cdiContainer.getBeanManager();
        Assert.assertNotNull(beanManager);

        TestUser testUser = resolveInstance(beanManager, TestUser.class);

        Assert.assertNotNull(testUser);
        testUser.setName("tester");


        CarRepair carRepair = resolveInstance(beanManager, CarRepair.class);

        Assert.assertNotNull(carRepair);

        Car car = carRepair.getCar();

        Assert.assertNotNull(car);
        Assert.assertNotNull(car.getUser());
        Assert.assertEquals("tester", car.getUser().getName());


        carRepair.getCar().getUser().setName("tck-tester");
        Assert.assertEquals("tck-tester", testUser.getName());

        cdiContainer.getContextControl().stopContext(RequestScoped.class);
        cdiContainer.getContextControl().startContext(RequestScoped.class);

        try
        {
            testUser = resolveInstance(beanManager, TestUser.class);

            Assert.assertNotNull(testUser);
            Assert.assertNotNull(testUser.getName());
            Assert.assertEquals("tck-tester", testUser.getName());
        }
        catch (ContextNotActiveException e)
        {
            Assert.fail(e.getMessage());
        }

        try
        {
            carRepair = resolveInstance(beanManager, CarRepair.class);

            Assert.assertNotNull(carRepair);

            car = carRepair.getCar();

            Assert.assertNotNull(car);
            Assert.assertNotNull(car.getUser());
            Assert.assertNotNull(car.getUser().getName());
            Assert.assertEquals("tck-tester", car.getUser().getName());
        }
        catch (ContextNotActiveException e)
        {
            Assert.fail(e.getMessage());
        }

        cdiContainer.shutdown();
    }

    private <T> T resolveInstance(BeanManager beanManager, Class<T> beanClass)
    {
        Set<Bean<?>> beans = beanManager.getBeans(beanClass);
        Bean<?> bean = beanManager.resolve(beans);

        return (T) beanManager.getReference(bean, beanClass, beanManager.createCreationalContext(bean));
    }
}
