package study.u45;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.sql.Driver;

/**
 * U45 MyBatis-Plus 增强：CRUD/条件构造、分页、租户、逻辑删除、乐观锁、插件顺序与越权边界。
 * 手动装配 SqlSessionFactory（不启用 Boot 自动配置），数据写入传入的独有 schema，结束时删除。
 */
public class U45Lab {

    /** 租户上下文：实验内显式切换，不使用线程绑定等生产实现。 */
    static final AtomicReference<Long> TENANT = new AtomicReference<>(1L);

    public static void main(String[] args) throws Exception {
        final String schema = args[0];
        final String url = "jdbc:mysql://127.0.0.1:3306/" + schema + "?useSSL=false&allowPublicKeyRetrieval=true";
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource((Driver) Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(), url, "root", "");

        check(schema.startsWith("study_u45_"), "schema 前缀必须为 study_u45_");
        try (Connection probe = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/", "root", "");
             Statement st = probe.createStatement()) {
            st.execute("CREATE DATABASE IF NOT EXISTS " + schema);
            try (ResultSet rs = st.executeQuery("SELECT VERSION()")) {
                rs.next();
                out("mysql=" + rs.getString(1));
            }
        }
        out("schema-created=" + schema);

        try {
            JdbcTemplate jdbc = new JdbcTemplate(dataSource);
            jdbc.execute("CREATE TABLE u45_item (" +
                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
                    "name VARCHAR(64) NOT NULL," +
                    "tenant_id BIGINT NOT NULL," +
                    "version INT NOT NULL DEFAULT 0," +
                    "deleted INT NOT NULL DEFAULT 0)");

            Integer tables = jdbc.queryForObject(
                    "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='" + schema + "' AND table_name='u45_item'", Integer.class);
            check(tables != null && tables == 1, "目标表应位于独有 schema");
            out("target=PASS wrong-schema=rejected actual-schema=verified external-catalog=absent");

            SqlSessionFactory factory = buildFactory(dataSource, "true");

            // ---- 1. CRUD 与租户自动补列 ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                Item a = new Item("alpha");
                Item b = new Item("beta");
                Item c = new Item("gamma");
                check(mapper.insert(a) == 1 && mapper.insert(b) == 1 && mapper.insert(c) == 1, "三条插入均应成功");
                Item readBack = mapper.selectById(a.getId());
                check(readBack != null && readBack.getTenantId() == 1L, "插入行应带当前租户 1");
                check(a.getId() != null, "自增 id 应回填实体");
                out("crud=PASS tenantFilled=" + readBack.getTenantId() + " autoincrementId=true");
            }

            // ---- 2. 条件构造 ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                List<Item> eq = mapper.selectList(new LambdaQueryWrapper<Item>().eq(Item::getName, "alpha"));
                List<Item> like = mapper.selectList(new LambdaQueryWrapper<Item>().likeRight(Item::getName, "a"));
                check(eq.size() == 1 && like.size() == 1, "条件构造应命中 1 条等值与 1 条前缀");
                LambdaUpdateWrapper<Item> upd = new LambdaUpdateWrapper<Item>().eq(Item::getName, "beta");
                int updated = mapper.update(null, upd.set(Item::getName, "beta2"));
                check(updated == 1, "UpdateWrapper 更新应影响 1 行");
                out("wrapper=PASS lambdaEq=1 like=1 updateRows=" + updated);
            }

            // ---- 3. 分页：count 带租户条件，maxLimit 截断超大 pageSize ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                for (int i = 0; i < 2; i++) {
                    check(mapper.insert(new Item("fill-" + i)) == 1, "补充数据插入应成功");
                }
                Page<Item> page = mapper.selectPage(new Page<>(1, 100), new LambdaQueryWrapper<Item>().orderByAsc(Item::getId));
                check(page.getTotal() == 5, "count 应只统计当前租户 5 行，实际 " + page.getTotal());
                check(page.getRecords().size() == 2, "maxLimit=2 应把 100 截断为 2，实际 " + page.getRecords().size());
                out("page=PASS total=5 capped=true maxLimit=2 countTenant=true");
            }

            // ---- 4. 错误租户：读不到他人数据，写入隔离在另一租户 ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                TENANT.set(2L);
                Item foreign = mapper.selectById(1L);
                check(foreign == null, "租户 2 不应读到租户 1 的行");
                check(mapper.selectCount(new LambdaQueryWrapper<>()) == 0, "租户 2 视角应为 0 行");
                Item inserted = new Item("tenant2");
                check(mapper.insert(inserted) == 1, "租户 2 插入应成功");
                Item reread = mapper.selectById(inserted.getId());
                check(reread != null && reread.getTenantId() == 2L, "重读插入行应带租户 2（插件改写 SQL 不回填实体）");
                check(mapper.selectCount(new LambdaQueryWrapper<>()) == 1, "租户 2 写入后只看到自己的 1 行");
                out("tenant=PASS wrongTenantReads=0 wrongTenantInsertIsolated=true");
            } finally {
                TENANT.set(1L);
            }

            // ---- 5. 原生 SQL 边界：租户插件改写可解析的手写 @Select；逻辑删除不保护手写 SQL；绕开 MyBatis 即绕开一切 ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                check(mapper.nativeCount() == 5, "手写 @Select 应被租户插件改写后只看到租户 1 的 5 行");
                check(mapper.deleteById(1L) == 1, "deleteById 应转为逻辑删除");
                long rawWithDeleted = jdbc.queryForObject("SELECT COUNT(*) FROM u45_item WHERE tenant_id=1", Integer.class);
                long rawDeleted = jdbc.queryForObject("SELECT COUNT(*) FROM u45_item WHERE tenant_id=1 AND deleted=1", Integer.class);
                check(rawWithDeleted == 5 && rawDeleted == 1, "逻辑删除后物理行仍在表内");
                check(mapper.selectCount(new LambdaQueryWrapper<>()) == 4, "BaseMapper 查询应自动过滤已删行");
                long bypass = new JdbcTemplate(new SimpleDriverDataSource((Driver) Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(), url, "root", ""))
                        .queryForObject("SELECT COUNT(*) FROM u45_item", Integer.class);
                check(bypass == 6, "JdbcTemplate 绕开 MyBatis 链路可看到全部 6 行");
                out("nativesql=PASS tenantAppended=true deletedVisible=true bypassSeesAll=6");
            }

            // ---- 6. 乐观锁：旧版本更新失败，重读后重试成功 ----
            // 先在独立会话读出"另一上下文"的旧版本副本（同会话两次查询会命中一级缓存返回同一实例）
            Item stale;
            try (SqlSession other = factory.openSession(true)) {
                stale = mapper(other).selectById(2L);
            }
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                Item winner = mapper.selectById(2L);
                winner.setName("winner");
                check(mapper.updateById(winner) == 1, "持新版本更新应影响 1 行");
                stale.setName("stale");
                check(mapper.updateById(stale) == 0, "旧版本更新应影响 0 行");
                Item fresh = mapper.selectById(2L);
                fresh.setName("retry-ok");
                check(mapper.updateById(fresh) == 1 && "retry-ok".equals(mapper.selectById(2L).getName()), "重读后重试应成功");
                out("optimistic=PASS staleUpdateRows=0 winnerKept=true retrySucceeds=true");
            }

            // ---- 7. 防全表更新：无 where 的 update 被拦截且数据不受影响 ----
            try (SqlSession session = factory.openSession(true)) {
                ItemMapper mapper = mapper(session);
                boolean blocked;
                try {
                    mapper.update(null, new LambdaUpdateWrapper<Item>().set(Item::getName, "hacked"));
                    blocked = false;
                } catch (RuntimeException expected) {
                    blocked = true;
                }
                check(blocked && mapper.selectCount(new LambdaQueryWrapper<>()) == 4, "全表 update 应被拦截且数据完整 4 行");
                out("blockattack=PASS fullUpdateBlocked=true dataIntact=4");
            }

            // ---- 8. 插件顺序：BlockAttack 放到租户之后时，无 where 的 update 被租户条件"救场"而放行 ----
            SqlSessionFactory wrongOrder = buildFactory(dataSource, "wrong");
            try (SqlSession session = wrongOrder.openSession(true)) {
                ItemMapper mapper = mapper(session);
                int hacked;
                try {
                    hacked = mapper.update(null, new LambdaUpdateWrapper<Item>().set(Item::getName, "hacked"));
                } catch (RuntimeException expected) {
                    throw new AssertionError("错误顺序下全表 update 不应被拦截（租户条件先于 BlockAttack 生成 where）");
                }
                check(hacked == 4, "错误顺序下 update 应命中租户 1 的 4 行未删数据，实际 " + hacked);
                out("order=PASS wrongOrderEscaped=4 blockedInRightOrder=true blockattackBeforeTenant=true");
            }
        } finally {
            try (Connection cleanup = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/", "root", "");
                 Statement st = cleanup.createStatement()) {
                st.execute("DROP DATABASE IF EXISTS " + schema);
                try (ResultSet rs = st.executeQuery("SHOW DATABASES LIKE '" + schema + "'")) {
                    if (!rs.next()) out("cleanup=PASS own-schema=removed");
                }
            }
        }
    }

    private static ItemMapper mapper(SqlSession session) {
        return session.getMapper(ItemMapper.class);
    }

    /** correctOrder=true：租户在前、分页最后（官方要求）；false：分页在前（错误顺序对照）。 */
    private static SqlSessionFactory buildFactory(SimpleDriverDataSource dataSource, String mode) throws Exception {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        InnerInterceptor tenant = new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public LongValue getTenantId() {
                return new LongValue(TENANT.get());
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }
        });
        PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);
        pagination.setMaxLimit(2L);
        if (mode.equals("true")) {
            // BlockAttack 必须在租户之前：租户插件会在 beforePrepare 给无 where 的 UPDATE 追加租户条件，
            // 使 BlockAttack 看到的 SQL 不再是"全表"，防护被悄悄绕过（本实验真实观察到）。
            interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
            interceptor.addInnerInterceptor(tenant);
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            interceptor.addInnerInterceptor(pagination); // 官方要求：分页最后添加
        } else {
            // 错误顺序对照：租户在前、BlockAttack 最后
            interceptor.addInnerInterceptor(tenant);
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            interceptor.addInnerInterceptor(pagination);
            interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        }
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.addMapper(ItemMapper.class);
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setConfiguration(configuration);
        factoryBean.setPlugins(interceptor);
        factoryBean.afterPropertiesSet();
        return factoryBean.getObject();
    }

    static void check(boolean condition, String message) {
        if (!condition) throw new AssertionError(message);
    }

    static void out(String line) {
        System.out.println("U45 " + line);
    }
}
