package study.readiness;

import java.nio.file.*;
import java.sql.*;
import java.time.Duration;
import java.util.*;
import javax.sql.DataSource;
import com.mongodb.client.*;
import org.bson.Document;
import org.junit.jupiter.api.*;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.jdbc.autoconfigure.*;
import org.springframework.boot.mongodb.autoconfigure.*;
import org.springframework.boot.data.mongodb.autoconfigure.*;
import org.springframework.boot.data.redis.autoconfigure.*;
import org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.*;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import tools.jackson.databind.json.JsonMapper;
import static org.junit.jupiter.api.Assertions.*;

class ConditionProbeTest {
  static final Map<String,Object> results=new LinkedHashMap<>();
  static final String MYSQL="jdbc:mysql://127.0.0.1:3306/?sslMode=DISABLED&allowPublicKeyRetrieval=true";
  static Map<String,Object> conditions(org.springframework.context.ConfigurableApplicationContext c,String part){
    var out=new LinkedHashMap<String,Object>();ConditionEvaluationReport.get(c.getBeanFactory()).getConditionAndOutcomesBySource().forEach((source,value)->{
      if(source.contains(part)){var messages=new ArrayList<String>();value.forEach(v->messages.add(v.getCondition().getClass().getSimpleName()+": "+v.getOutcome()));out.put(source,Map.of("matched",value.isFullMatch(),"outcomes",messages));}
    });return out;
  }
  static int query(DataSource ds){try(var c=ds.getConnection();var s=c.createStatement();var r=s.executeQuery("SELECT 1")){assertTrue(r.next());return r.getInt(1);}catch(SQLException e){throw new IllegalStateException(e);}}
  @AfterAll static void save()throws Exception{Files.createDirectories(Path.of("target"));Files.writeString(Path.of("target/probe-results.json"),new JsonMapper().writerWithDefaultPrettyPrinter().writeValueAsString(results));}
  @Test void jdbc(){
    var base=new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class)).withPropertyValues("spring.datasource.url="+MYSQL,"spring.datasource.username=root","spring.datasource.hikari.maximum-pool-size=1");
    var data=new LinkedHashMap<String,Object>();
    base.run(c->{assertNull(c.getStartupFailure());assertEquals(1,query(c.getBean(DataSource.class)));data.put("mysql",true);data.put("default",c.getBean(DataSource.class).getClass().getName());data.put("conditions",conditions(c,"DataSource"));});
    base.withUserConfiguration(UserData.class).run(c->{assertNull(c.getStartupFailure());assertSame(c.getBean("userData"),c.getBean(JdbcTemplate.class).getDataSource());assertEquals(1,query(c.getBean(DataSource.class)));assertEquals(0,c.getBeanNamesForType(com.zaxxer.hikari.HikariDataSource.class).length);data.put("userBackoff",true);});
    base.withUserConfiguration(TwoData.class).run(c->{assertNull(c.getStartupFailure());assertEquals(2,c.getBeanNamesForType(DataSource.class).length);assertEquals(0,c.getBeanNamesForType(JdbcTemplate.class).length);data.put("twoCandidatesNoTemplate",true);});
    base.withClassLoader(new FilteredClassLoader("org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType")).run(c->{assertNull(c.getStartupFailure());assertEquals(0,c.getBeanNamesForType(DataSource.class).length);data.put("missingClassSkipped",true);});
    results.put("jdbc",data);
  }
  @Configuration(proxyBeanMethods=false) static class UserData{@Bean DataSource userData(){return new DriverManagerDataSource(MYSQL,"root","");}}
  @Configuration(proxyBeanMethods=false) static class TwoData{@Bean DataSource first(){return new DriverManagerDataSource(MYSQL,"root","");}@Bean DataSource second(){return new DriverManagerDataSource(MYSQL,"root","");}}
  @Test void redis(){
    var base=new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataRedisAutoConfiguration.class)).withPropertyValues("spring.data.redis.host=127.0.0.1","spring.data.redis.database=13");var data=new LinkedHashMap<String,Object>();
    base.run(c->{assertNull(c.getStartupFailure());var factory=c.getBean(LettuceConnectionFactory.class);assertEquals(13,factory.getDatabase());try(var connection=factory.getConnection()){assertEquals("PONG",connection.ping());}data.put("ping",true);data.put("database",factory.getDatabase());data.put("default",factory.getClass().getName());data.put("conditions",conditions(c,"Redis"));});
    base.withUserConfiguration(UserRedis.class).run(c->{assertNull(c.getStartupFailure());assertSame(c.getBean("userFactory"),c.getBean(RedisConnectionFactory.class));assertEquals(1,c.getBeanNamesForType(RedisConnectionFactory.class).length);data.put("factoryBackoff",true);});
    base.withUserConfiguration(OtherTemplate.class).run(c->{assertNull(c.getStartupFailure());assertTrue(c.containsBean("redisTemplate"));assertTrue(c.containsBean("otherTemplate"));data.put("differentNameDoesNotBackoff",true);});
    base.withClassLoader(new FilteredClassLoader(RedisOperations.class)).run(c->{assertNull(c.getStartupFailure());assertEquals(0,c.getBeanNamesForType(RedisConnectionFactory.class).length);data.put("missingClassSkipped",true);});results.put("redis",data);
  }
  @Configuration(proxyBeanMethods=false) static class UserRedis{@Bean LettuceConnectionFactory userFactory(){return new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1",6379));}}
  @Configuration(proxyBeanMethods=false) static class OtherTemplate{@Bean RedisTemplate<Object,Object> otherTemplate(RedisConnectionFactory factory){var template=new RedisTemplate<Object,Object>();template.setConnectionFactory(factory);return template;}}
  @Test void mongo(){
    String database="u24_"+UUID.randomUUID().toString().replace("-","");
    var base=new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class,DataMongoAutoConfiguration.class)).withPropertyValues("spring.mongodb.uri=mongodb://127.0.0.1:27017/"+database+"?serverSelectionTimeoutMS=2000");var data=new LinkedHashMap<String,Object>();
    base.run(c->{assertNull(c.getStartupFailure());var factory=c.getBean(MongoDatabaseFactory.class);assertEquals(database,factory.getMongoDatabase().getName());assertEquals(1.0,((Number)factory.getMongoDatabase().runCommand(new Document("ping",1)).get("ok")).doubleValue());data.put("database",database);data.put("ping",true);data.put("template",c.getBean(MongoTemplate.class).getClass().getName());data.put("connectionDetails",c.getBean(MongoConnectionDetails.class).getConnectionString().getDatabase());data.put("conditions",conditions(c,"Mongo"));});
    base.withUserConfiguration(UserMongo.class).run(c->{assertNull(c.getStartupFailure());assertSame(c.getBean("userMongo"),c.getBean(MongoClient.class));assertEquals(1,c.getBeanNamesForType(MongoClient.class).length);data.put("userBackoff",true);});
    base.withClassLoader(new FilteredClassLoader(MongoClient.class)).run(c->{assertNull(c.getStartupFailure());assertEquals(0,c.getBeanNamesForType(MongoClient.class).length);data.put("missingClassSkipped",true);});data.put("writes",0);results.put("mongodb",data);
  }
  @Configuration(proxyBeanMethods=false) static class UserMongo{@Bean(destroyMethod="close") MongoClient userMongo(){return MongoClients.create("mongodb://127.0.0.1:27017/?serverSelectionTimeoutMS=2000");}}
  @Test void cache(){
    var base=new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class,DataRedisAutoConfiguration.class)).withPropertyValues("spring.cache.type=simple");var data=new LinkedHashMap<String,Object>();
    base.run(c->{assertNull(c.getStartupFailure());assertEquals(0,c.getBeanNamesForType(CacheManager.class).length);data.put("withoutEnableCaching",true);});
    base.withUserConfiguration(Caching.class).run(c->{assertNull(c.getStartupFailure());assertInstanceOf(ConcurrentMapCacheManager.class,c.getBean(CacheManager.class));data.put("simple",true);});
    base.withUserConfiguration(Caching.class,UserCache.class).run(c->{assertSame(c.getBean("userCache"),c.getBean(CacheManager.class));data.put("userBackoff",true);});
    String name="u24cache"+UUID.randomUUID().toString().replace("-","");String key=name+"::item";
    base.withUserConfiguration(Caching.class).withPropertyValues("spring.cache.type=redis","spring.cache.redis.time-to-live=5s","spring.data.redis.database=13").run(c->{
      assertNull(c.getStartupFailure());var cache=c.getBean(CacheManager.class).getCache(name);assertNotNull(cache);var redis=c.getBean(StringRedisTemplate.class);
      try{cache.put("item","value");assertEquals("value",cache.get("item",String.class));Long ttl=redis.getExpire(key);assertNotNull(ttl);assertTrue(ttl>=0&&ttl<=5);data.put("redisCacheTtlSeconds",ttl);data.put("redisRead",true);}finally{redis.delete(key);assertFalse(redis.hasKey(key));}data.put("ownedKeyDeleted",true);
    });results.put("cache",data);
  }
  @Configuration(proxyBeanMethods=false) @EnableCaching static class Caching{}
  @Configuration(proxyBeanMethods=false) static class UserCache{@Bean CacheManager userCache(){return new ConcurrentMapCacheManager();}}
}
