/*_############################################################################
  _##
  _##  SNMP4J - TaskSchedulerTest.java
  _##
  _##  Copyright (C) 2003-2026  Frank Fock (SNMP4J.org)
  _##
  _##  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 org.snmp4j.util;

import org.junit.Test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.assertTrue;

public class TaskSchedulerTest {

    /**
     * Regression test for the run() deadlock that occurred when the pool was full and additional
     * tasks were added to the scheduler.
     * <p>
     * Setup: a pool with capacity 1 and three scheduler tasks {@code A}, {@code B}, {@code C}.
     * Task {@code A} is picked first and starts running on the pool's only worker. The scheduler
     * then tries to submit {@code B} but the pool is now busy, so it waits for capacity.
     * From inside {@code A.run()} we recursively call {@link TaskScheduler#addTask(SchedulerTask)}
     * to schedule {@code C}.
     * <p>
     * Pre-fix behaviour: the scheduler held its monitor while waiting on the pool, so the
     * recursive {@code addTask} blocked, {@code A} could never return, the pool stayed full, and
     * the scheduler waited forever — deadlock. With the fix in place, {@code addTask} returns
     * promptly and {@code C} is eventually executed.
     */
    @Test(timeout = 10_000)
    public void recursiveAddTaskFromWorkerDoesNotDeadlock() throws Exception {
        ThreadPool pool = ThreadPool.create("TaskSchedulerDeadlockTest", 1);
        TaskScheduler scheduler = new TaskScheduler(pool);
        Thread schedulerThread = new Thread(scheduler, "TaskSchedulerThread");
        schedulerThread.setDaemon(true);
        schedulerThread.start();

        AtomicLong addTaskDurationNanos = new AtomicLong(-1);
        CountDownLatch sentinelExecuted = new CountDownLatch(1);

        OneShotSchedulerTask taskC = new OneShotSchedulerTask(sentinelExecuted::countDown);

        // Task A: sleep so the scheduler has time to pick B and then block on the saturated pool,
        // then attempt a recursive addTask(C). We measure how long the recursive add takes; before
        // the fix it would never return.
        OneShotSchedulerTask taskA = new OneShotSchedulerTask(() -> {
            try {
                Thread.sleep(300L);
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
            long t0 = System.nanoTime();
            scheduler.addTask(taskC);
            addTaskDurationNanos.set(System.nanoTime() - t0);
        });
        OneShotSchedulerTask taskB = new OneShotSchedulerTask(() -> { /* trivial */ });

        scheduler.addTask(taskA);
        scheduler.addTask(taskB);

        assertTrue("Scheduled sentinel task C never ran — likely deadlock in TaskScheduler.run()",
                sentinelExecuted.await(5, TimeUnit.SECONDS));

        long durationMillis = TimeUnit.NANOSECONDS.toMillis(addTaskDurationNanos.get());
        assertTrue("Recursive addTask blocked for " + durationMillis +
                        " ms while the pool was busy — TaskScheduler.run() must not hold its monitor " +
                        "during ThreadPool.wait(...)",
                durationMillis < 1500L);

        scheduler.setStop(true);
        pool.stop();
        schedulerThread.join(2000);
    }

    private static final class OneShotSchedulerTask implements SchedulerTask {
        private final Runnable body;
        private volatile boolean started;
        private volatile boolean finished;

        OneShotSchedulerTask(Runnable body) {
            this.body = body;
        }

        @Override
        public boolean isReadyToRun() {
            return !started && !finished;
        }

        @Override
        public boolean isDone() {
            return finished;
        }

        @Override
        public void run() {
            started = true;
            try {
                body.run();
            } finally {
                finished = true;
            }
        }

        @Override
        public void shutdown() {
        }

        @Override
        public void terminate() {
        }

        @Override
        public void join() {
        }

        @Override
        public boolean join(long timeoutMillis) {
            return finished;
        }

        @Override
        public void interrupt() {
        }
    }
}
