package study.readiness;

import java.nio.file.*;
import java.time.Duration;
import java.util.*;
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.WebApplicationContextRunner;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration;
import org.springframework.boot.session.data.redis.autoconfigure.SessionDataRedisAutoConfiguration;
import org.springframework.context.annotation.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import tools.jackson.databind.json.JsonMapper;
import static org.junit.jupiter.api.Assertions.*;

/** Real Redis-backed repositories in an owned namespace. No web server is started. */
class SessionProbeTest {
  static final Map<String,Object> results=new LinkedHashMap<>();
  static String namespace(){return "u24:session:"+UUID.randomUUID().toString().replace("-","");}
  @AfterAll static void save()throws Exception{Files.writeString(Path.of("target/session-results.json"),new JsonMapper().writerWithDefaultPrettyPrinter().writeValueAsString(results));}
  static WebApplicationContextRunner runner(String namespace,String repositoryType){
    return new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataRedisAutoConfiguration.class,SessionAutoConfiguration.class,SessionDataRedisAutoConfiguration.class))
      .withPropertyValues("spring.data.redis.host=127.0.0.1","spring.session.timeout=90s","spring.session.data.redis.namespace="+namespace,"spring.session.data.redis.repository-type="+repositoryType,"spring.session.data.redis.configure-action=none");
  }
  static Set<String> ownedKeys(StringRedisTemplate redis,String namespace){return redis.keys(namespace+":*");}
  static Map<String,Object> conditions(org.springframework.context.ConfigurableApplicationContext c){
    var out=new LinkedHashMap<String,Object>();ConditionEvaluationReport.get(c.getBeanFactory()).getConditionAndOutcomesBySource().forEach((source,value)->{
      if(source.contains("Session")){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;
  }
  @Test void defaultRepository(){
    String namespace=namespace();var data=new LinkedHashMap<String,Object>();
    runner(namespace,"default").run(c->{
      assertNull(c.getStartupFailure());
      var repository=c.getBean(SessionRepository.class);
      assertInstanceOf(RedisSessionRepository.class,repository);
      data.put("default",repository.getClass().getName());
      var session=repository.createSession();
      session.setAttribute("u24","owned-value");
      session.setMaxInactiveInterval(Duration.ofSeconds(90));
      repository.save(session);
      var reloaded=repository.findById(session.getId());
      assertNotNull(reloaded);
      assertEquals("owned-value",reloaded.getAttribute("u24"));
      assertEquals(90,reloaded.getMaxInactiveInterval().getSeconds());
      var redis=c.getBean(StringRedisTemplate.class);
      var keys=ownedKeys(redis,namespace);
      assertTrue(keys!=null&&!keys.isEmpty(),namespace);
      data.put("ownedKeys",keys.size());
      redis.delete(keys);
      assertTrue(ownedKeys(redis,namespace).isEmpty());
      data.put("cookieSaveRead",true);
      data.put("ownedKeysDeleted",true);
      data.put("conditions",conditions(c));
    });
    results.put("session",data);
  }
  @Test void indexedRepositorySeparateCapabilities(){
    String namespace=namespace();var data=new LinkedHashMap<String,Object>();
    runner(namespace,"indexed").run(c->{
      assertNull(c.getStartupFailure());
      var repository=c.getBean(SessionRepository.class);
      assertInstanceOf(RedisIndexedSessionRepository.class,repository);
      data.put("indexed",repository.getClass().getName());
      var configure=c.getBeansOfType(org.springframework.session.data.redis.config.ConfigureRedisAction.class);
      assertEquals(1,configure.size());
      assertSame(org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP,configure.values().iterator().next());
      data.put("configureActionNone",true);
      var session=repository.createSession();
      session.setAttribute("u24","indexed-value");
      repository.save(session);
      assertNotNull(repository.findById(session.getId()));
      var redis=c.getBean(StringRedisTemplate.class);
      var keys=ownedKeys(redis,namespace);
      assertTrue(keys!=null&&!keys.isEmpty(),namespace);
      redis.delete(keys);
      assertTrue(ownedKeys(redis,namespace).isEmpty());
      data.put("ownedKeysDeleted",true);
    });
    results.put("sessionIndexed",data);
  }
  @Test void userRepositoryTakesOver(){
    var data=new LinkedHashMap<String,Object>();
    runner(namespace(),"default").withUserConfiguration(UserRepository.class).run(c->{
      assertNull(c.getStartupFailure());
      assertSame(c.getBean("userRepository"),c.getBean(SessionRepository.class));
      assertEquals(0,c.getBeanNamesForType(RedisSessionRepository.class).length);
      data.put("userBackoff",true);
      data.put("conditions",conditions(c));
    });
    results.put("sessionTakeover",data);
  }
  @Test void missingClassSkips(){
    var data=new LinkedHashMap<String,Object>();
    runner(namespace(),"default").withClassLoader(new FilteredClassLoader(Session.class)).run(c->{
      assertNull(c.getStartupFailure());
      assertEquals(0,c.getBeanNamesForType(SessionRepository.class).length);
      data.put("missingClassSkipped",true);
    });
    results.put("sessionClassCondition",data);
  }
  @Configuration(proxyBeanMethods=false) static class UserRepository{
    @Bean SessionRepository<? extends Session> userRepository(){
      return new SessionRepository<>(){
        private final Map<String,Session> store=new LinkedHashMap<>();
        public Session createSession(){return new org.springframework.session.MapSession();}
        public void save(Session session){store.put(session.getId(),session);}
        public Session findById(String id){return store.get(id);}
        public void deleteById(String id){store.remove(id);}
      };
    }
  }
}
