/*
 * Copyright (c) 2016-present, RxJava Contributors.
 *
 * Licensed 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 io.reactivex.rxjava3.internal.schedulers;

import static org.junit.Assert.*;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.*;

import org.junit.Test;

import io.reactivex.rxjava3.core.RxJavaTest;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.exceptions.TestException;
import io.reactivex.rxjava3.internal.functions.Functions;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.testsupport.TestHelper;

public class ScheduledRunnableTest extends RxJavaTest {

    @Test
    public void dispose() {
        CompositeDisposable set = new CompositeDisposable();
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
        set.add(run);

        assertFalse(run.isDisposed());

        set.dispose();

        assertTrue(run.isDisposed());
    }

    @Test
    public void disposeRun() {
        CompositeDisposable set = new CompositeDisposable();
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
        set.add(run);

        assertFalse(run.isDisposed());

        run.dispose();
        run.dispose();

        assertTrue(run.isDisposed());
    }

    @Test
    public void setFutureCancelRace() {
        for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
            set.add(run);

            final FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, 0);

            Runnable r1 = new Runnable() {
                @Override
                public void run() {
                    run.setFuture(ft);
                }
            };

            Runnable r2 = new Runnable() {
                @Override
                public void run() {
                    run.dispose();
                }
            };

            TestHelper.race(r1, r2);

            assertEquals(0, set.size());
        }
    }

    @Test
    public void setFutureRunRace() {
        for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
            set.add(run);

            final FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, 0);

            Runnable r1 = new Runnable() {
                @Override
                public void run() {
                    run.setFuture(ft);
                }
            };

            Runnable r2 = new Runnable() {
                @Override
                public void run() {
                    run.run();
                }
            };

            TestHelper.race(r1, r2);

            assertEquals(0, set.size());
        }
    }

    @Test
    public void disposeRace() {
        for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
            set.add(run);

            Runnable r1 = new Runnable() {
                @Override
                public void run() {
                    run.dispose();
                }
            };

            TestHelper.race(r1, r1);

            assertEquals(0, set.size());
        }
    }

    @Test
    public void runDispose() {
        for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
            set.add(run);

            Runnable r1 = new Runnable() {
                @Override
                public void run() {
                    run.call();
                }
            };

            Runnable r2 = new Runnable() {
                @Override
                public void run() {
                    run.dispose();
                }
            };

            TestHelper.race(r1, r2);

            assertEquals(0, set.size());
        }
    }

    @Test
    public void pluginCrash() {
        Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                throw new TestException("Second");
            }
        });

        CompositeDisposable set = new CompositeDisposable();
        final ScheduledRunnable run = new ScheduledRunnable(new Runnable() {
            @Override
            public void run() {
                throw new TestException("First");
            }
        }, set);
        set.add(run);

        try {
            run.run();

            fail("Should have thrown!");
        } catch (TestException ex) {
            assertEquals("Second", ex.getMessage());
        } finally {
            Thread.currentThread().setUncaughtExceptionHandler(null);
        }
        assertTrue(run.isDisposed());

        assertEquals(0, set.size());
    }

    @Test
    public void crashReported() {
        List<Throwable> errors = TestHelper.trackPluginErrors();
        try {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(new Runnable() {
                @Override
                public void run() {
                    throw new TestException("First");
                }
            }, set);
            set.add(run);

            try {
                run.run();
                fail("Should have thrown!");
            } catch (TestException expected) {
                // expected
            }

            assertTrue(run.isDisposed());

            assertEquals(0, set.size());

            TestHelper.assertUndeliverable(errors, 0, TestException.class, "First");
        } finally {
            RxJavaPlugins.reset();
        }
    }

    @Test
    public void withoutParentDisposed() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        run.dispose();
        run.call();
    }

    @Test
    public void withParentDisposed() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, new CompositeDisposable());
        run.dispose();
        run.call();
    }

    @Test
    public void withFutureDisposed() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null));
        run.dispose();
        run.call();
    }

    @Test
    public void withFutureDisposed2() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        run.dispose();
        run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null));
        run.call();
    }

    @Test
    public void withFutureDisposed3() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        run.dispose();
        run.set(2, Thread.currentThread());
        run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null));
        run.call();
    }

    @Test
    public void runFuture() {
        for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) {
            CompositeDisposable set = new CompositeDisposable();
            final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
            set.add(run);

            final FutureTask<Void> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, null);

            Runnable r1 = new Runnable() {
                @Override
                public void run() {
                    run.call();
                }
            };

            Runnable r2 = new Runnable() {
                @Override
                public void run() {
                    run.setFuture(ft);
                }
            };

            TestHelper.race(r1, r2);
        }
    }

    @Test
    public void syncWorkerCancelRace() {
        for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) {
            final CompositeDisposable set = new CompositeDisposable();
            final AtomicBoolean interrupted = new AtomicBoolean();
            final AtomicInteger sync = new AtomicInteger(2);
            final AtomicInteger syncb = new AtomicInteger(2);

            Runnable r0 = new Runnable() {
                @Override
                public void run() {
                    set.dispose();
                    if (sync.decrementAndGet() != 0) {
                        while (sync.get() != 0) { }
                    }
                    if (syncb.decrementAndGet() != 0) {
                        while (syncb.get() != 0) { }
                    }
                    for (int j = 0; j < 1000; j++) {
                        if (Thread.currentThread().isInterrupted()) {
                            interrupted.set(true);
                            break;
                        }
                    }
                }
            };

            final ScheduledRunnable run = new ScheduledRunnable(r0, set);
            set.add(run);

            final FutureTask<Void> ft = new FutureTask<>(run, null);

            Runnable r2 = new Runnable() {
                @Override
                public void run() {
                    if (sync.decrementAndGet() != 0) {
                        while (sync.get() != 0) { }
                    }
                    run.setFuture(ft);
                    if (syncb.decrementAndGet() != 0) {
                        while (syncb.get() != 0) { }
                    }
                }
            };

            TestHelper.race(ft, r2);

            assertFalse("The task was interrupted", interrupted.get());
        }
    }

    @Test
    public void disposeAfterRun() {
        final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);

        run.run();
        assertEquals(ScheduledRunnable.DONE, run.get(ScheduledRunnable.FUTURE_INDEX));

        run.dispose();
        assertEquals(ScheduledRunnable.DONE, run.get(ScheduledRunnable.FUTURE_INDEX));
    }

    @Test
    public void syncDisposeIdempotent() {
        final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        run.set(ScheduledRunnable.THREAD_INDEX, Thread.currentThread());

        run.dispose();
        assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
        run.dispose();
        assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
        run.run();
        assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
    }

    @Test
    public void asyncDisposeIdempotent() {
        final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);

        run.dispose();
        assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
        run.dispose();
        assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
        run.run();
        assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX));
    }

    @Test
    public void noParentIsDisposed() {
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null);
        assertFalse(run.isDisposed());
        run.run();
        assertTrue(run.isDisposed());
    }

    @Test
    public void withParentIsDisposed() {
        CompositeDisposable set = new CompositeDisposable();
        ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
        set.add(run);

        assertFalse(run.isDisposed());

        run.run();
        assertTrue(run.isDisposed());

        assertFalse(set.remove(run));
    }

    @Test
    public void toStringStates() {
        CompositeDisposable set = new CompositeDisposable();
        ScheduledRunnable task = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);

        assertEquals("ScheduledRunnable[Waiting]", task.toString());

        task.set(ScheduledRunnable.THREAD_INDEX, Thread.currentThread());

        assertEquals("ScheduledRunnable[Running on " + Thread.currentThread() + "]", task.toString());

        task.dispose();

        assertEquals("ScheduledRunnable[Disposed(Sync)]", task.toString());

        task.set(ScheduledRunnable.FUTURE_INDEX, ScheduledRunnable.DONE);

        assertEquals("ScheduledRunnable[Finished]", task.toString());

        task = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set);
        task.dispose();

        assertEquals("ScheduledRunnable[Disposed(Async)]", task.toString());
    }
}
