package org.example;

import org.example.Utils.CacheUtil;
import org.example.Utils.DbOperator;
import org.example.Utils.DynamicJdbcTemplateRegistry;
import org.example.Utils.JsEngine;
import org.example.Utils.RequestUtil;
import org.example.Utils.ResourceExecutors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.util.concurrent.ThreadPoolExecutor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ResourceGovernanceRegressionTest {

    @AfterEach
    void resetStaticState() {
        DynamicJdbcTemplateRegistry.resetForTests();
        CacheUtil.resetForTests();
        JsEngine.resetCacheForTests();
        ResourceExecutors.resetForTests();
        RequestUtil.clear();
    }

    @Test
    void dbOperatorConstructorInitializesDerivedJdbcState() throws Exception {
        DataSource dataSource = new DriverManagerDataSource();
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        DbOperator dbOperator = new DbOperator(jdbcTemplate);

        assertSame(jdbcTemplate, dbOperator.getJdbcTemplate());
        assertSame(dataSource, readField(dbOperator, "currentDataSource"));
        NamedParameterJdbcTemplate named = readField(dbOperator, "namedJdbcTemplate");
        assertSame(jdbcTemplate.getDataSource(), named.getJdbcTemplate().getDataSource());
    }

    @Test
    void dynamicRegistryReusesJdbcTemplateForSameConnectionString() {
        DynamicJdbcTemplateRegistry.configureForTests(32, Duration.ofMinutes(60));
        CloseTrackingDataSource dataSource = new CloseTrackingDataSource();

        JdbcTemplate first = DynamicJdbcTemplateRegistry.getOrCreate(
                "jdbc:test://tenant-a",
                ignored -> new JdbcTemplate(dataSource)
        );
        JdbcTemplate second = DynamicJdbcTemplateRegistry.getOrCreate(
                "jdbc:test://tenant-a",
                ignored -> new JdbcTemplate(new CloseTrackingDataSource())
        );

        assertSame(first, second);
        assertEquals(1, DynamicJdbcTemplateRegistry.getActivePoolCount());
        assertFalse(dataSource.closed);
    }

    @Test
    void dynamicRegistryClosesOldestPoolWhenLimitIsExceeded() {
        DynamicJdbcTemplateRegistry.configureForTests(1, Duration.ofMinutes(60));
        CloseTrackingDataSource firstDataSource = new CloseTrackingDataSource();
        CloseTrackingDataSource secondDataSource = new CloseTrackingDataSource();

        DynamicJdbcTemplateRegistry.getOrCreate("jdbc:test://tenant-a", ignored -> new JdbcTemplate(firstDataSource));
        DynamicJdbcTemplateRegistry.getOrCreate("jdbc:test://tenant-b", ignored -> new JdbcTemplate(secondDataSource));

        assertTrue(firstDataSource.closed);
        assertFalse(secondDataSource.closed);
        assertEquals(1, DynamicJdbcTemplateRegistry.getActivePoolCount());
    }

    @Test
    void dynamicRegistryClosesIdlePools() {
        DynamicJdbcTemplateRegistry.configureForTests(32, Duration.ZERO);
        CloseTrackingDataSource dataSource = new CloseTrackingDataSource();

        DynamicJdbcTemplateRegistry.getOrCreate("jdbc:test://tenant-a", ignored -> new JdbcTemplate(dataSource));
        DynamicJdbcTemplateRegistry.closeIdlePools();

        assertTrue(dataSource.closed);
        assertEquals(0, DynamicJdbcTemplateRegistry.getActivePoolCount());
    }

    @Test
    void requestClearRemovesEveryThreadLocalCache() throws Exception {
        setThreadLocal("rawContentCache", new byte[]{1, 2, 3});
        setThreadLocal("cachedEntityStream", new ByteArrayInputStream(new byte[]{4}));
        setThreadLocal("cachedInputStream", new ByteArrayInputStream(new byte[]{5}));
        setThreadLocal("readEntityBodyMode", RequestUtil.ReadEntityBodyMode.Buffered);
        setThreadLocal("hasWorkerRequest", Boolean.FALSE);

        RequestUtil.clear();

        assertThreadLocalEmpty("rawContentCache");
        assertThreadLocalEmpty("cachedEntityStream");
        assertThreadLocalEmpty("cachedInputStream");
        assertThreadLocalEmpty("readEntityBodyMode");
        assertThreadLocalEmpty("hasWorkerRequest");
    }

    @Test
    void cacheUtilEvictsOldEntriesWhenCapacityIsExceeded() {
        CacheUtil.setMaxEntriesForTests(2);

        CacheUtil.set("a", "one", Duration.ofMinutes(10), null);
        CacheUtil.set("b", "two", Duration.ofMinutes(10), null);
        CacheUtil.set("c", "three", Duration.ofMinutes(10), null);

        assertEquals(2, CacheUtil.sizeForTests());
        assertEquals("two", CacheUtil.get("b"));
        assertEquals("three", CacheUtil.get("c"));
    }

    @Test
    void jsEngineEvictsOldEntriesWhenCapacityIsExceeded() {
        JsEngine.setMaxCacheEntriesForTests(2);

        JsEngine.putCachedResultForTests("a", 1);
        JsEngine.putCachedResultForTests("b", 2);
        JsEngine.putCachedResultForTests("c", 3);

        assertEquals(2, JsEngine.cacheSizeForTests());
        assertFalse(JsEngine.hasCachedResultForTests("a"));
        assertTrue(JsEngine.hasCachedResultForTests("b"));
        assertTrue(JsEngine.hasCachedResultForTests("c"));
    }

    @Test
    void jsEngineEvaluatesSimpleExpression() {
        Object result = JsEngine.Eval("1 + 1");

        assertEquals("2", String.valueOf(result));
    }

    @Test
    void jsEngineBlocksJavaAccessByDefault() {
        Object result = JsEngine.Eval("Java.type('java.lang.System').getProperty('user.home')");

        assertEquals(null, result);
    }

    @Test
    void resourceExecutorsUseBoundedQueues() {
        ResourceExecutors.configureForTests(2, 4, 200, 4, 16, 500);

        ThreadPoolExecutor fileExecutor = ResourceExecutors.fileCleanupExecutorForTests();
        ThreadPoolExecutor pushExecutor = ResourceExecutors.pushExecutorForTests();

        assertEquals(2, fileExecutor.getCorePoolSize());
        assertEquals(4, fileExecutor.getMaximumPoolSize());
        assertEquals(200, fileExecutor.getQueue().remainingCapacity());
        assertEquals(4, pushExecutor.getCorePoolSize());
        assertEquals(16, pushExecutor.getMaximumPoolSize());
        assertEquals(500, pushExecutor.getQueue().remainingCapacity());
    }

    @SuppressWarnings("unchecked")
    private static void setThreadLocal(String fieldName, Object value) throws Exception {
        ThreadLocal<Object> threadLocal = readStaticField(RequestUtil.class, fieldName);
        threadLocal.set(value);
    }

    private static void assertThreadLocalEmpty(String fieldName) throws Exception {
        ThreadLocal<?> threadLocal = readStaticField(RequestUtil.class, fieldName);
        assertEquals(null, threadLocal.get());
    }

    @SuppressWarnings("unchecked")
    private static <T> T readField(Object target, String fieldName) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return (T) field.get(target);
    }

    @SuppressWarnings("unchecked")
    private static <T> T readStaticField(Class<?> type, String fieldName) throws Exception {
        Field field = type.getDeclaredField(fieldName);
        field.setAccessible(true);
        return (T) field.get(null);
    }

    private static class CloseTrackingDataSource extends AbstractDataSource implements AutoCloseable {
        boolean closed;

        @Override
        public Connection getConnection() throws SQLException {
            throw new SQLException("No real connection in resource lifecycle tests");
        }

        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            throw new SQLException("No real connection in resource lifecycle tests");
        }

        @Override
        public void close() {
            closed = true;
        }
    }
}
