package study.u42;

import java.sql.DriverManager;
import java.util.List;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import study.u42.mapper.ItemMapper;

@SpringBootApplication
@MapperScan("study.u42.mapper")
public class U42Lab {
    @org.springframework.context.annotation.Bean
    org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer tracePlugins() {
        return config -> { config.addInterceptor(new TracePlugins.First()); config.addInterceptor(new TracePlugins.Second()); config.addInterceptor(new TracePlugins.QueryCount()); };
    }
    static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
    static void verifyTarget(javax.sql.DataSource ds, String expectedSchema) throws Exception {
        try (var connection=ds.getConnection()) {
            String url=connection.getMetaData().getURL();
            if (!expectedSchema.equals(connection.getCatalog()) || !url.startsWith("jdbc:mysql://127.0.0.1:3306/" + expectedSchema + "?"))
                throw new IllegalStateException("refusing non-U42 data source");
        }
    }
    public static void main(String[] args) throws Exception {
        if (args.length != 1 || !args[0].matches("study_u42_[0-9a-f]{12}")) throw new IllegalArgumentException("expected unique U42 schema");
        String schema = args[0];
        String adminUrl = "jdbc:mysql://127.0.0.1:3306/?sslMode=DISABLED&allowPublicKeyRetrieval=true";
        String user = System.getenv().getOrDefault("U42_MYSQL_USER", "root");
        String password = System.getenv().getOrDefault("U42_MYSQL_PASSWORD", "");
        boolean created = false;
        try {
            try (var c = DriverManager.getConnection(adminUrl, user, password); var s = c.createStatement()) {
                s.executeUpdate("CREATE DATABASE `" + schema + "`"); created = true;
                System.out.println("U42 schema-created=" + schema);
                if ("1".equals(System.getenv("U42_TEST_ABORT_AFTER_CREATE"))) Runtime.getRuntime().halt(77);
                try (var rs = s.executeQuery("SELECT VERSION()")) { rs.next(); System.out.println("U42 mysql=" + rs.getString(1)); }
            }
            try (var pool = new com.zaxxer.hikari.HikariDataSource()) {
                pool.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/" + schema + "?sslMode=DISABLED&allowPublicKeyRetrieval=true");
                pool.setUsername(user); pool.setPassword(password); pool.setMaximumPoolSize(2);
                try { verifyTarget(pool, schema + "_wrong"); throw new AssertionError("wrong schema accepted"); }
                catch (IllegalStateException expected) { check(expected.getMessage().contains("refusing"), "wrong failure"); }
                verifyTarget(pool, schema);
                SpringApplication app = new SpringApplication(U42Lab.class);
                app.setWebApplicationType(WebApplicationType.NONE);
                app.addInitializers(context -> {
                    context.getBeanFactory().registerSingleton("dataSource", pool);
                    context.getEnvironment().getPropertySources().addFirst(new org.springframework.core.env.MapPropertySource(
                        "u42-isolated", java.util.Map.of("spring.sql.init.mode", "never", "spring.main.banner-mode", "off")));
                });
            try (ConfigurableApplicationContext ctx = app.run()) {
                var ds = ctx.getBean(javax.sql.DataSource.class);
                check(ds == pool, "unexpected data source bean");
                verifyTarget(ds, schema);
                System.out.println("U42 target=PASS wrong-schema=rejected actual-schema=verified");
                JdbcTemplate jdbc = ctx.getBean(JdbcTemplate.class);
                jdbc.execute("CREATE TABLE category(id INT PRIMARY KEY, tenant_id INT NOT NULL, label VARCHAR(80) NOT NULL)");
                jdbc.execute("CREATE TABLE item(id INT PRIMARY KEY, tenant_id INT NOT NULL, name VARCHAR(80) NOT NULL, category_id INT NULL)");
                jdbc.execute("CREATE TABLE item_status(id INT PRIMARY KEY, state VARCHAR(20) NULL)");
                jdbc.update("INSERT INTO category VALUES(1,10,'books')");
                ItemMapper mapper = ctx.getBean(ItemMapper.class);
                SqlSessionTemplate template = ctx.getBean(SqlSessionTemplate.class);
                SqlSessionFactory factory = ctx.getBean(SqlSessionFactory.class);
                TransactionTemplate tx = new TransactionTemplate(ctx.getBean(PlatformTransactionManager.class));
                TracePlugins.events.clear();
                mapper.insert(1, 10, "one");
                check(TracePlugins.events.equals(List.of("second:before", "first:before", "first:after", "second:after")), "plugin order=" + TracePlugins.events);
                System.out.println("U42 plugins=PASS outer=second inner=first");
                mapper.insert(2, 20, "secret");
                var mapped = mapper.item(1);
                check(mapped.getId()==1 && mapped.getTenantId()==10 && "one".equals(mapped.getName()), "explicit resultMap");
                check(mapper.search(10, List.of("one")).equals(List.of("one")), "dynamic where/foreach");
                check(mapper.prefix(10,"on").equals(List.of("one")), "bind prefix");
                check(mapper.rename(1,20,"wrong-tenant")==0, "wrong-tenant update");
                try { mapper.rename(1,10,null); throw new AssertionError("empty SET accepted"); }
                catch (org.springframework.dao.DataAccessException expected) {
                    Throwable cause=expected; while (cause != null && !(cause instanceof java.sql.SQLException)) cause=cause.getCause();
                    check(cause instanceof java.sql.SQLException sql && "42000".equals(sql.getSQLState()) && sql.getErrorCode()==1064, "unexpected empty SET failure");
                }
                check("one".equals(mapper.name(1)), "empty SET changed data");
                check(mapper.rename(1,10,"one-updated")==1, "valid SET recovery");
                check(mapper.rename(1,10,"one")==1, "restore value");
                mapper.insertStatus(1,study.u42.mapper.Status.READY);
                mapper.insertStatus(2,null);
                check(mapper.status(1)==study.u42.mapper.Status.READY && mapper.status(2)==null, "enum/null handler");
                jdbc.update("INSERT INTO item_status(id,state) VALUES(3,'UNKNOWN')");
                try { mapper.status(3); throw new AssertionError("unknown enum accepted"); }
                catch (RuntimeException expected) { check(expected.toString().contains("UNKNOWN"), "wrong enum error"); }
                jdbc.update("UPDATE item_status SET state='DONE' WHERE id=3");
                check(mapper.status(3)==study.u42.mapper.Status.DONE, "enum recovery");
                System.out.println("U42 dynamic=PASS set-empty=error set-recovery=ok bind=ok enum-null=ok invalid-enum=recovered");
                check(mapper.search(10, List.of("secret")).isEmpty(), "cross-tenant data leaked");
                check(mapper.search(20, List.of("secret")).equals(List.of("secret")), "tenant positive control");
                check(mapper.byTenant(20).equals(List.of("secret")), "SQL builder provider did not bind tenant");
                check(mapper.page(10,1,0).equals(List.of("one")), "database pagination");
                check("generic".equals(mapper.dbName()), "Spring generic databaseId branch");
                var nativeProps = new java.util.Properties();
                nativeProps.setProperty("u42.url", "jdbc:mysql://127.0.0.1:3306/" + schema + "?sslMode=DISABLED&allowPublicKeyRetrieval=true");
                nativeProps.setProperty("u42.user", user); nativeProps.setProperty("u42.password", password);
                var builder = new org.apache.ibatis.session.SqlSessionFactoryBuilder();
                org.apache.ibatis.session.SqlSessionFactory nativeFactory;
                try (var input=org.apache.ibatis.io.Resources.getResourceAsReader("study/u42/native-config.xml")) { nativeFactory=builder.build(input,nativeProps); }
                check(nativeFactory.getConfiguration().getLocalCacheScope()==org.apache.ibatis.session.LocalCacheScope.SESSION, "resource property must override element property");
                check("mysql".equals(nativeFactory.getConfiguration().getDatabaseId()), "databaseId");
                try (var session=nativeFactory.openSession()) { check("mysql".equals(session.getMapper(ItemMapper.class).dbName()), "databaseId statement"); }
                nativeProps.setProperty("u42.scope", "STATEMENT");
                try (var input=org.apache.ibatis.io.Resources.getResourceAsReader("study/u42/native-config.xml")) {
                    check(builder.build(input,nativeProps).getConfiguration().getLocalCacheScope()==org.apache.ibatis.session.LocalCacheScope.STATEMENT, "builder properties must win");
                }
                System.out.println("U42 config=PASS resource-over-body builder-over-resource databaseId=mysql aliases-handlers-factories=loaded");
                check(mapper.search(10, List.of()).isEmpty(), "empty list must not expand access");
                check(mapper.search(10, List.of("one' OR 1=1 -- ")).isEmpty(), "injection must be a value");
                System.out.println("U42 mapping=PASS scanner=mapper xml=resultMap search-tenant=isolated empty-result=confirmed injection=bound");
                var emptyCtx = new AnnotationConfigApplicationContext();
                var scanner = new org.mybatis.spring.mapper.ClassPathMapperScanner(emptyCtx);
                scanner.registerFilters();
                check(scanner.scan("study.u42.missing") == 0, "wrong package should register no mapper");
                emptyCtx.close();
                check(ctx.getBeansOfType(ItemMapper.class).size() == 1, "correct scanner did not recover");
                System.out.println("U42 scan=PASS wrong-package=empty correct-package=one");
                tx.executeWithoutResult(status -> { mapper.insert(3, 10, "rollback"); status.setRollbackOnly(); });
                check(mapper.name(3) == null, "rollback did not remove row");
                tx.executeWithoutResult(status -> mapper.insert(4, 10, "committed"));
                check("committed".equals(mapper.name(4)), "commit missing");
                jdbc.update("UPDATE item SET category_id=1 WHERE id IN (1,2,4)");
                var category=mapper.categoryItems(1,10);
                check(category.getItems().size()==2 && "books".equals(category.getLabel()), "collection join aggregation");
                check("books".equals(mapper.itemCategory(1,10).getCategory().getLabel()), "association join");
                check(mapper.categoryItems(1,20)==null && mapper.itemCategory(1,20)==null, "join tenant filter");
                check(mapper.itemCategory(2,20).getCategory()==null, "cross-tenant association exposed");
                tx.executeWithoutResult(status -> {
                    TracePlugins.categoryQueries.set(0);
                    var lazy=mapper.lazyItem(1,10);
                    check(TracePlugins.categoryQueries.get()==0, "nested select ran eagerly");
                    check("books".equals(lazy.getCategory().getLabel()), "lazy category result");
                    check(TracePlugins.categoryQueries.get()==1, "nested select not observed on access");
                });
                System.out.println("U42 association=PASS collection=two association=one cross-tenant-join=filtered lazy=on-access");
                try { mapper.insert(4, 10, "duplicate"); throw new AssertionError("duplicate accepted"); }
                catch (org.springframework.dao.DuplicateKeyException expected) {
                    Throwable cause=expected; while (cause != null && !(cause instanceof java.sql.SQLException)) cause=cause.getCause();
                    check(cause instanceof java.sql.SQLException sql && "23000".equals(sql.getSQLState()) && sql.getErrorCode()==1062, "unexpected MySQL duplicate cause");
                    System.out.println("U42 translate=PASS exception=DuplicateKeyException sqlstate=23000 code=1062");
                }
                check("one".equals(mapper.name(1)), "recovery after failed statement");
                for (int op=0; op<3; op++) {
                    try { if(op==0)template.commit(); else if(op==1)template.rollback(); else template.close(); throw new AssertionError("manual control accepted"); }
                    catch (UnsupportedOperationException expected) { }
                }
                SqlSessionTemplate batch = new SqlSessionTemplate(factory, ExecutorType.BATCH);
                tx.executeWithoutResult(status -> {
                    mapper.name(1);
                    try { batch.selectOne("study.u42.mapper.ItemMapper.name", 1); throw new AssertionError("mixed executor accepted"); }
                    catch (org.springframework.dao.TransientDataAccessResourceException expected) { }
                });
                check("one".equals(mapper.name(1)), "session not recovered after mixed executor");
                TransactionTemplate independent = new TransactionTemplate(ctx.getBean(PlatformTransactionManager.class));
                independent.setPropagationBehavior(org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW);
                tx.executeWithoutResult(status -> {
                    mapper.insert(6,10,"outer-rollback");
                    independent.executeWithoutResult(inner -> batch.getMapper(ItemMapper.class).insert(5, 10, "inner"));
                    status.setRollbackOnly();
                });
                check("inner".equals(mapper.name(5)), "requires-new batch not committed independently");
                check(mapper.name(6)==null, "outer transaction failed to roll back");
                check(!org.springframework.transaction.support.TransactionSynchronizationManager.hasResource(factory), "session holder leaked");
                check(pool.getHikariPoolMXBean().getActiveConnections()==0, "pooled connection leaked");
                System.out.println("U42 transaction=PASS rollback=absent commit=visible manual=forbidden mixed-executor=rejected requires-new=committed recovery=ok");
                try (Cursor<String> c=mapper.cursor()) {
                    try { c.iterator(); throw new AssertionError("escaped cursor remained open"); }
                    catch (IllegalStateException expected) { check(expected.getMessage().toLowerCase().contains("closed"), "unexpected cursor failure"); }
                }
                tx.executeWithoutResult(status -> {
                    try (Cursor<String> c=mapper.cursor()) { int n=0; for (String ignored : c) n++; check(n==4,"cursor count"); }
                    catch (java.io.IOException e) { throw new RuntimeException(e); }
                });
                var captured = new java.util.concurrent.atomic.AtomicReference<Cursor<String>>();
                try {
                    tx.executeWithoutResult(status -> {
                        try (Cursor<String> c=mapper.cursor()) {
                            captured.set(c); c.iterator().next(); throw new IllegalArgumentException("consumer failure");
                        } catch (java.io.IOException e) { throw new RuntimeException(e); }
                    });
                    throw new AssertionError("consumer failure missing");
                } catch (IllegalArgumentException expected) { check("consumer failure".equals(expected.getMessage()), "unexpected consumer error"); }
                check(captured.get()!=null && !captured.get().isOpen(), "cursor leaked after exception");
                check(pool.getHikariPoolMXBean().getActiveConnections()==0, "connection leaked after cursor");
                System.out.println("U42 cursor=PASS escaped=closed transaction=consumed failure=closed sql-builder=executed");
            }
            }
        } finally {
            if (created) {
                try (var c=DriverManager.getConnection(adminUrl,user,password); var s=c.createStatement()) {
                    s.executeUpdate("DROP DATABASE `" + schema + "`");
                    try (var rs=s.executeQuery("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='" + schema + "'")) { check(!rs.next(), "schema cleanup failed"); }
                }
                System.out.println("U42 cleanup=PASS own-schema=removed");
            }
        }
    }
}
