package study.spring;

import java.util.*;
import java.util.concurrent.*;
import org.h2.jdbcx.JdbcDataSource;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.*;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.aop.support.AopUtils;

/** Public teaching command: real annotation infrastructure, H2 and observable rows. */
public class AnnotatedTxLab {
  @Configuration(proxyBeanMethods=false)
  @EnableTransactionManagement(proxyTargetClass=true)
  static class Config {
    @Bean JdbcDataSource dataSource() {
      var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:annotated;DB_CLOSE_DELAY=-1"); return ds;
    }
    @Bean JdbcTemplate jdbc(JdbcDataSource ds) { return new JdbcTemplate(ds); }
    @Bean DataSourceTransactionManager transactionManager(JdbcDataSource ds) { return new DataSourceTransactionManager(ds); }
    @Bean Service service(JdbcTemplate jdbc) { return new Service(jdbc); }
  }
  public static class Service {
    final JdbcTemplate jdbc;
    boolean active;
    Service(JdbcTemplate jdbc) { this.jdbc = jdbc; }
    public boolean wasActive() { return active; }
    public void self() { external(); }
    @Transactional
    public void external() {
      active = TransactionSynchronizationManager.isActualTransactionActive();
      jdbc.update("insert into item values(1)");
      throw new IllegalStateException("expected business failure");
    }
    @Transactional
    public void checkedDefault() throws Exception { write(); throw new Exception("expected checked failure"); }
    @Transactional(rollbackFor=Exception.class)
    public void checkedRollback() throws Exception { write(); throw new Exception("expected checked failure"); }
    void write() {
      active = TransactionSynchronizationManager.isActualTransactionActive();
      jdbc.update("insert into item values(1)");
    }
    @Transactional
    public CompletableFuture<String> future(boolean alreadyFailed) {
      write();
      return alreadyFailed ? CompletableFuture.failedFuture(new IllegalStateException("future failure"))
          : new CompletableFuture<>();
    }
    @Transactional
    public void thread() throws Exception {
      write();
      var executor = Executors.newSingleThreadExecutor();
      try {
        boolean workerActive = executor.submit(() -> {
          boolean worker = TransactionSynchronizationManager.isActualTransactionActive();
          jdbc.update("insert into item values(2)"); return worker;
        }).get(3, TimeUnit.SECONDS);
        if (workerActive) throw new AssertionError("transaction leaked to worker");
      } finally { executor.shutdownNow(); }
      throw new IllegalStateException("expected business failure");
    }
  }
  public static void main(String[] args) {
    String mode = args.length == 0 ? "external" : args[0];
    if (!Set.of("external", "self", "checked", "checked-rollback", "future-failed", "future-late", "thread").contains(mode)) throw new IllegalArgumentException(mode);
    try (var c = new AnnotationConfigApplicationContext(Config.class)) {
      var jdbc = c.getBean(JdbcTemplate.class);
      jdbc.execute("drop table if exists item"); jdbc.execute("create table item(id int primary key)");
      var service = c.getBean(Service.class);
      try {
        if (mode.equals("self")) service.self();
        else if (mode.equals("checked")) service.checkedDefault();
        else if (mode.equals("checked-rollback")) service.checkedRollback();
        else if (mode.equals("thread")) service.thread();
        else if (mode.startsWith("future-")) {
          var future = service.future(mode.equals("future-failed"));
          if (mode.equals("future-late")) future.completeExceptionally(new IllegalStateException("future failure"));
          if (!future.isCompletedExceptionally()) throw new AssertionError("future must fail");
        }
        else service.external();
        if (!mode.startsWith("future-")) throw new AssertionError("missing business failure");
      } catch (Exception expected) {
        if (!Set.of("expected business failure", "expected checked failure").contains(expected.getMessage())) throw expected;
      }
      boolean active = service.wasActive();
      var rows = jdbc.queryForList("select id from item order by id", Integer.class);
      var expectedRows = mode.equals("thread") ? List.of(2)
          : Set.of("external", "checked-rollback", "future-failed").contains(mode) ? List.<Integer>of() : List.of(1);
      if (!AopUtils.isAopProxy(service) || active == mode.equals("self") || !rows.equals(expectedRows)
          || TransactionSynchronizationManager.isActualTransactionActive())
        throw new AssertionError(mode + " active=" + active + " rows=" + rows);
      System.out.println("PASS annotated " + mode + " active=" + active + " rows=" + rows);
    } catch (Exception e) { throw new RuntimeException(e); }
  }
}
