package com.easyagents.scheduler.spring.boot;

import com.easyagents.scheduler.ConcurrencyPolicy;
import com.easyagents.scheduler.MisfirePolicy;
import com.easyagents.scheduler.OnceSchedulePlan;
import com.easyagents.scheduler.ScheduleDefinition;
import com.easyagents.scheduler.ScheduleException;
import com.easyagents.scheduler.ScheduleFireContext;
import com.easyagents.scheduler.ScheduleHandler;
import com.easyagents.scheduler.ScheduleId;
import com.easyagents.scheduler.ScheduleService;
import org.h2.jdbcx.JdbcDataSource;
import org.h2.tools.RunScript;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.MapPropertySource;

import javax.sql.DataSource;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * {@link EasyAgentsSchedulerAutoConfiguration} 的 Spring Boot 2.7/3.5 装配测试。
 */
public class EasyAgentsSchedulerAutoConfigurationTest {

    /**
     * 验证默认关闭时不创建调度器，也不要求 DataSource。
     */
    @Test
    public void shouldStayDisabledByDefault() {
        try (AnnotationConfigApplicationContext context = newContext(Map.of())) {
            assertFalse(context.containsBean(
                EasyAgentsSchedulerAutoConfiguration.SCHEDULER_BEAN_NAME
            ));
        }
    }

    /**
     * 验证显式启用后完成属性绑定、Handler 收集、启动和关闭。
     */
    @Test
    public void shouldConfigureAndStartScheduler() throws Exception {
        JdbcDataSource dataSource = dataSource();
        executeSchema(dataSource);
        CountDownLatch latch = new CountDownLatch(1);
        Map<String, Object> properties = enabledProperties();
        properties.put("easy-agents.scheduler.data-source-bean-name", "schedulerDataSource");

        SpringApplication application = new SpringApplication(AutoDiscoveryApplication.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.setDefaultProperties(properties);
        application.addInitializers(applicationContext -> {
            GenericApplicationContext genericContext =
                (GenericApplicationContext) applicationContext;
            genericContext.registerBean(
                "schedulerDataSource",
                DataSource.class,
                () -> dataSource
            );
            genericContext.registerBean(
                "unrelatedDataSource",
                DataSource.class,
                () -> dataSource()
            );
            genericContext.registerBean(
                "testScheduleHandler",
                ScheduleHandler.class,
                () -> handler(latch)
            );
        });
        ConfigurableApplicationContext context = application.run();
        try {
            ScheduleService service = context.getBean(ScheduleService.class);
            assertNotNull(service);
            ScheduleDefinition definition = definition();
            service.create(definition);
            service.triggerNow(definition.id(), "starter-invocation", Map.of());
            assertTrue("starter handler did not execute", latch.await(5, TimeUnit.SECONDS));
        } finally {
            context.close();
        }

        // Spring 销毁 Scheduler 后，调用方持有的 DataSource 仍然可用。
        try (
            Connection connection = dataSource.getConnection();
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM QRTZ_JOB_DETAILS")
        ) {
            assertTrue(resultSet.next());
            assertEquals(1, resultSet.getInt(1));
        }
    }

    /**
     * 验证调度器等待 Spring Boot 数据库脚本初始化完成后再启动。
     */
    @Test
    public void shouldWaitForDatabaseInitializationBeforeStartingScheduler() throws Exception {
        JdbcDataSource dataSource = dataSource();
        Map<String, Object> properties = enabledProperties();
        properties.put("easy-agents.scheduler.data-source-bean-name", "schedulerDataSource");
        properties.put("spring.sql.init.mode", "always");
        properties.put(
            "spring.sql.init.schema-locations",
            "classpath:quartz-schema/h2-2.5.2.sql"
        );

        SpringApplication application = new SpringApplication(AutoDiscoveryApplication.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.setDefaultProperties(properties);
        application.addInitializers(applicationContext -> {
            GenericApplicationContext genericContext =
                (GenericApplicationContext) applicationContext;
            genericContext.registerBean(
                "schedulerDataSource",
                DataSource.class,
                () -> dataSource
            );
        });

        try (ConfigurableApplicationContext context = application.run()) {
            assertNotNull(context.getBean(ScheduleService.class));
            try (
                Connection connection = dataSource.getConnection();
                Statement statement = connection.createStatement();
                ResultSet resultSet = statement.executeQuery(
                    "SELECT COUNT(*) FROM QRTZ_SCHEDULER_STATE"
                )
            ) {
                assertTrue(resultSet.next());
            }
        }
    }

    /**
     * 验证多个 DataSource 未明确选择时启动失败并提供可操作信息。
     */
    @Test
    public void shouldRejectAmbiguousDataSources() {
        AnnotationConfigApplicationContext context = context(enabledProperties());
        context.registerBean("firstDataSource", DataSource.class, () -> dataSource());
        context.registerBean("secondDataSource", DataSource.class, () -> dataSource());
        context.register(EasyAgentsSchedulerAutoConfiguration.class);
        try {
            context.refresh();
            fail("expected ambiguous DataSource failure");
        } catch (RuntimeException exception) {
            ScheduleException scheduleException = findCause(exception, ScheduleException.class);
            assertNotNull(scheduleException);
            assertTrue(scheduleException.getMessage().contains("data-source-bean-name"));
        } finally {
            context.close();
        }
    }

    private static AnnotationConfigApplicationContext newContext(
        Map<String, Object> properties
    ) {
        AnnotationConfigApplicationContext context = context(properties);
        context.register(EasyAgentsSchedulerAutoConfiguration.class);
        context.refresh();
        return context;
    }

    private static AnnotationConfigApplicationContext context(Map<String, Object> properties) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getEnvironment().getPropertySources().addFirst(
            new MapPropertySource("scheduler-test", properties)
        );
        return context;
    }

    private static Map<String, Object> enabledProperties() {
        Map<String, Object> properties = new HashMap<>();
        properties.put("easy-agents.scheduler.enabled", "true");
        properties.put(
            "easy-agents.scheduler.quartz.scheduler-name",
            "starter-scheduler-" + UUID.randomUUID()
        );
        properties.put("easy-agents.scheduler.quartz.instance-id", "NON_CLUSTERED");
        properties.put("easy-agents.scheduler.quartz.clustered", "false");
        properties.put("easy-agents.scheduler.quartz.thread-count", "2");
        properties.put(
            "easy-agents.scheduler.quartz.batch-trigger-acquisition-max-count", "2"
        );
        properties.put(
            "easy-agents.scheduler.quartz.batch-trigger-acquisition-fire-ahead-time-window-millis",
            "500"
        );
        properties.put("easy-agents.scheduler.quartz.misfire-threshold-millis", "1000");
        return properties;
    }

    private static JdbcDataSource dataSource() {
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL("jdbc:h2:mem:starter-" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1");
        dataSource.setUser("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    private static void executeSchema(JdbcDataSource dataSource) throws Exception {
        InputStream stream = EasyAgentsSchedulerAutoConfigurationTest.class
            .getClassLoader()
            .getResourceAsStream("quartz-schema/h2-2.5.2.sql");
        if (stream == null) {
            throw new IllegalStateException("H2 Quartz schema resource not found");
        }
        try (
            Connection connection = dataSource.getConnection();
            InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)
        ) {
            RunScript.execute(connection, reader);
        }
    }

    private static ScheduleHandler handler(CountDownLatch latch) {
        return new ScheduleHandler() {
            @Override
            public String code() {
                return "starter-handler";
            }

            @Override
            public void execute(ScheduleFireContext context) {
                latch.countDown();
            }
        };
    }

    private static ScheduleDefinition definition() {
        return new ScheduleDefinition(
            new ScheduleId("starter", "trigger"),
            "starter-handler",
            new OnceSchedulePlan(Instant.parse("2099-01-01T00:00:00Z")),
            MisfirePolicy.FIRE_ONCE_NOW,
            ConcurrencyPolicy.DISALLOW,
            true,
            Map.of("source", "starter"),
            "starter test"
        );
    }

    private static <T extends Throwable> T findCause(Throwable failure, Class<T> type) {
        Throwable current = failure;
        while (current != null) {
            if (type.isInstance(current)) {
                return type.cast(current);
            }
            current = current.getCause();
        }
        return null;
    }

    /**
     * 只启用 Spring Boot 自动配置发现的测试应用。
     */
    @Configuration(proxyBeanMethods = false)
    @EnableAutoConfiguration
    static class AutoDiscoveryApplication {
    }
}
