package study.runtime;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.*;
import org.springframework.core.task.*;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.*;

class TaskProbe {
  @Configuration(proxyBeanMethods=false)
  static class UserExecutor {
    @Bean @ConditionalOnProperty(name="lab.user-executor",havingValue="true")
    Executor userExecutor(){return Runnable::run;}
  }
  @Configuration(proxyBeanMethods=false)
  @ConditionalOnProperty(name="lab.cron-invalid",havingValue="true")
  static class InvalidCron {
    @Scheduled(cron="*/1 * * * * *",initialDelay=1) public void invalid(){}
  }
  static void check(boolean pass,String reason){if(!pass)throw new AssertionError(reason);}
  static Object run(ConfigurableApplicationContext context)throws Exception{
    if(!context.containsBean("applicationTaskExecutor")){
      check(context.containsBean("userExecutor"),"unexpected missing executor");
      AtomicInteger calls=new AtomicInteger();context.getBean("userExecutor",Executor.class).execute(calls::incrementAndGet);
      return Map.of("userBackoff",calls.get()==1,"applicationTaskExecutor",false);
    }
    AsyncTaskExecutor executor=context.getBean("applicationTaskExecutor",AsyncTaskExecutor.class);
    TaskScheduler scheduler=context.getBean(TaskScheduler.class);
    var output=new LinkedHashMap<String,Object>();output.put("executor",executor.getClass().getName());output.put("scheduler",scheduler.getClass().getName());
    boolean virtual=executor instanceof SimpleAsyncTaskExecutor;
    boolean unbounded=context.getEnvironment().getProperty("lab.task-case","").equals("unbounded");
    var started=new CountDownLatch(1);var release=new CountDownLatch(1);var complete=new AtomicInteger();var wasVirtual=new AtomicBoolean();
    var first=executor.submit(()->{wasVirtual.set(Thread.currentThread().isVirtual());started.countDown();try{if(!release.await(5,TimeUnit.SECONDS))throw new AssertionError("release timeout");complete.incrementAndGet();}catch(InterruptedException e){Thread.currentThread().interrupt();throw new IllegalStateException(e);}});
    check(started.await(3,TimeUnit.SECONDS),"first task did not start");
    Future<?> second=null,third=null;boolean rejected=false;
    try{
      if(!virtual)second=executor.submit(complete::incrementAndGet);
      try{third=executor.submit(complete::incrementAndGet);}catch(TaskRejectedException expected){rejected=true;}
      if(virtual){
        check(rejected&&wasVirtual.get(),"virtual concurrency rejection/virtual thread missing");
        output.put("concurrencyLimit",((SimpleAsyncTaskExecutor)executor).getConcurrencyLimit());
      }else{
        var pool=(ThreadPoolTaskExecutor)executor;
        output.put("core",pool.getCorePoolSize());output.put("max",pool.getMaxPoolSize());output.put("queueCapacity",pool.getQueueCapacity());output.put("poolSizeWhileHeld",pool.getPoolSize());output.put("queuedWhileHeld",pool.getThreadPoolExecutor().getQueue().size());
        check(unbounded?!rejected&&pool.getPoolSize()==1&&pool.getThreadPoolExecutor().getQueue().size()==2:rejected,"platform rejection/unbounded queue mismatch");
      }
    }finally{release.countDown();}
    first.get(3,TimeUnit.SECONDS);if(second!=null)second.get(3,TimeUnit.SECONDS);if(third!=null)third.get(3,TimeUnit.SECONDS);
    int expected=virtual?1:unbounded?3:2;check(complete.get()==expected,"side effect count");
    output.put("completed",complete.get());output.put("rejected",rejected);output.put("workerVirtual",wasVirtual.get());output.put("userExecutorPresent",context.containsBean("userExecutor"));
    if(scheduler instanceof ThreadPoolTaskScheduler platform)output.put("schedulingPoolSize",platform.getScheduledThreadPoolExecutor().getCorePoolSize());
    return output;
  }
}
