/*
 * 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.ignite.internal.processors.cache.datastructures;

import java.util.Random;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteAtomicLong;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteException;
import org.apache.ignite.configuration.AtomicConfiguration;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.transactions.Transaction;

import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;

/**
 * Cache atomic long api test.
 */
public abstract class IgniteAtomicLongApiAbstractSelfTest extends IgniteAtomicsAbstractTest {
    /** Random number generator. */
    private static final Random RND = new Random();

    /** */
    private static final String TRANSACTIONAL_CACHE_NAME = "tx_cache";

    /** {@inheritDoc} */
    @Override protected int gridCount() {
        return 1;
    }

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);

        CacheConfiguration ccfg = new CacheConfiguration(DEFAULT_CACHE_NAME);

        ccfg.setName(TRANSACTIONAL_CACHE_NAME);
        ccfg.setAtomicityMode(TRANSACTIONAL);

        cfg.setCacheConfiguration(ccfg);

        return cfg;
    }

    /**
     * @throws Exception If failed.
     */
    public void testCreateRemove() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        String atomicName1 = "FIRST";

        String atomicName2 = "SECOND";

        IgniteAtomicLong atomic1 = ignite.atomicLong(atomicName1, 0, true);
        IgniteAtomicLong atomic2 = ignite.atomicLong(atomicName2, 0, true);
        IgniteAtomicLong atomic3 = ignite.atomicLong(atomicName1, 0, true);

        assertNotNull(atomic1);
        assertNotNull(atomic2);
        assertNotNull(atomic3);

        assert atomic1.equals(atomic3);
        assert atomic3.equals(atomic1);
        assert !atomic3.equals(atomic2);

        atomic1.close();
        atomic2.close();
        atomic3.close();

        assertNull(ignite.atomicLong(atomicName1, 0, false));
        assertNull(ignite.atomicLong(atomicName2, 0, false));

        try {
            atomic1.get();

            fail();
        }
        catch (IllegalStateException | IgniteException e) {
            info("Caught expected exception: " + e.getMessage());
        }
    }

    /**
     * @throws Exception If failed.
     */
    public void testIncrementAndGet() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long curAtomicVal = atomic.get();

        assert curAtomicVal + 1 == atomic.incrementAndGet();
        assert curAtomicVal + 1 == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testGetAndIncrement() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long curAtomicVal = atomic.get();

        assert curAtomicVal == atomic.getAndIncrement();
        assert curAtomicVal + 1 == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testDecrementAndGet() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long curAtomicVal = atomic.get();

        assert curAtomicVal - 1 == atomic.decrementAndGet();
        assert curAtomicVal - 1 == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testGetAndDecrement() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long curAtomicVal = atomic.get();

        assert curAtomicVal == atomic.getAndDecrement();
        assert curAtomicVal - 1 == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testGetAndAdd() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long delta = RND.nextLong();

        long curAtomicVal = atomic.get();

        assert curAtomicVal == atomic.getAndAdd(delta);
        assert curAtomicVal + delta == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testAddAndGet() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long delta = RND.nextLong();

        long curAtomicVal = atomic.get();

        assert curAtomicVal + delta == atomic.addAndGet(delta);
        assert curAtomicVal + delta == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testGetAndSet() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long newVal = RND.nextLong();

        long curAtomicVal = atomic.get();

        assert curAtomicVal == atomic.getAndSet(newVal);
        assert newVal == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    @SuppressWarnings({"NullableProblems", "ConstantConditions"})
    public void testCompareAndSet() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long newVal = RND.nextLong();

        final long oldVal = atomic.get();

        // Don't set new value.
        assert !atomic.compareAndSet(oldVal - 1, newVal);

        assert oldVal == atomic.get();

        // Set new value.
        assert atomic.compareAndSet(oldVal, newVal);

        assert newVal == atomic.get();
    }

    /**
     * @throws Exception If failed.
     */
    public void testGetAndSetInTx() throws Exception {
        info("Running test [name=" + getName() + ", cacheMode=" + atomicsCacheMode() + ']');

        Ignite ignite = grid(0);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        IgniteCache<Object, Object> cache = ignite.cache(TRANSACTIONAL_CACHE_NAME);

        try (Transaction tx = ignite.transactions().txStart()) {
            cache.put(1, 1);

            long newVal = RND.nextLong();

            long curAtomicVal = atomic.get();

            assert curAtomicVal == atomic.getAndSet(newVal);
            assert newVal == atomic.get();
        }
    }

    /**
     * Implementation of ignite data structures internally uses special system caches, need make sure that
     * transaction on these system caches do not intersect with transactions started by user.
     *
     * @throws Exception If failed.
     */
    public void testIsolation() throws Exception {
        Ignite ignite = grid(0);

        IgniteCache<Object, Object> cache = ignite.cache(TRANSACTIONAL_CACHE_NAME);

        IgniteAtomicLong atomic = ignite.atomicLong("atomic", 0, true);

        long curAtomicVal = atomic.get();

        try (Transaction tx = ignite.transactions().txStart()) {
            atomic.getAndIncrement();

            cache.put(1, 1);

            tx.rollback();
        }

        assertEquals(0, cache.size());
        assertEquals(curAtomicVal + 1, atomic.get());
    }

    /**
     * Tests that basic API works correctly when there are multiple structures in multiple groups.
     *
     * @throws Exception If failed.
     */
    public void testMultipleStructuresInDifferentGroups() throws Exception {
        Ignite ignite = grid(0);

        AtomicConfiguration cfg = new AtomicConfiguration().setGroupName("grp1");

        IgniteAtomicLong atomic1 = ignite.atomicLong("atomic1", 1, true);
        IgniteAtomicLong atomic2 = ignite.atomicLong("atomic2", 2, true);
        IgniteAtomicLong atomic3 = ignite.atomicLong("atomic3", cfg, 3, true);
        IgniteAtomicLong atomic4 = ignite.atomicLong("atomic4", cfg, 4, true);

        assertNull(ignite.atomicLong("atomic1", cfg, 1, false));
        assertNull(ignite.atomicLong("atomic2", cfg, 2, false));
        assertNull(ignite.atomicLong("atomic3", 3, false));
        assertNull(ignite.atomicLong("atomic4", 4, false));

        assertTrue(atomic1.compareAndSet(1, 11));
        assertTrue(atomic2.compareAndSet(2, 12));
        assertTrue(atomic3.compareAndSet(3, 13));
        assertTrue(atomic4.compareAndSet(4, 14));

        assertFalse(atomic1.compareAndSet(1, 0));
        assertFalse(atomic2.compareAndSet(2, 0));
        assertFalse(atomic3.compareAndSet(3, 0));
        assertFalse(atomic4.compareAndSet(4, 0));

        atomic2.close();
        atomic4.close();

        assertTrue(atomic2.removed());
        assertTrue(atomic4.removed());

        assertNull(ignite.atomicLong("atomic2", 2, false));
        assertNull(ignite.atomicLong("atomic4", cfg, 4, false));

        assertFalse(atomic1.removed());
        assertFalse(atomic3.removed());

        assertNotNull(ignite.atomicLong("atomic1", 1, false));
        assertNotNull(ignite.atomicLong("atomic3", cfg, 3, false));
    }
}