package com.mosquito.project.permission;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.output.MigrateResult;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.testcontainers.containers.PostgreSQLContainer;

import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

/**
 * 权限码规范化迁移测试
 * 仅使用 PostgreSQL 容器执行，避免 H2 方言差异导致误报。
 */
@DisplayName("权限码规范化迁移测试")
class PermissionCanonicalMigrationTest {

    private PostgreSQLContainer<?> postgres;

    @AfterEach
    void tearDown() {
        if (postgres != null) {
            postgres.stop();
        }
    }

    @Test
    @DisplayName("API Key 权限码应迁移为 canonical 四段式")
    void shouldMigrateApiKeyPermissionCodesToCanonical() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        Integer oldCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN (" +
                "'system.api-key.view','system.api-key.create','system.api-key.enable'," +
                "'system.api-key.disable','system.api-key.delete','system.api-key.reset','system.api-key.manage')",
            Integer.class
        );
        Integer canonicalCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN (" +
                "'system.api-key.view.ALL','system.api-key.create.ALL','system.api-key.enable.ALL'," +
                "'system.api-key.disable.ALL','system.api-key.delete.ALL','system.api-key.reset.ALL','system.api-key.manage.ALL')",
            Integer.class
        );

        assertThat(oldCodeCount).isEqualTo(0);
        assertThat(canonicalCodeCount).isEqualTo(7);
    }

    @Test
    @DisplayName("仪表盘监控权限码应迁移为 canonical 四段式")
    void shouldMigrateDashboardMonitorPermissionCodesToCanonical() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        Integer oldCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN (" +
                "'dashboard.chart.realtime','dashboard.chart.history','dashboard.kpi.config')",
            Integer.class
        );
        Integer canonicalCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN (" +
                "'dashboard.chart.realtime.ALL','dashboard.chart.history.ALL','dashboard.kpi.config.ALL')",
            Integer.class
        );

        assertThat(oldCodeCount).isEqualTo(0);
        assertThat(canonicalCodeCount).isEqualTo(3);
    }

    @Test
    @DisplayName("风险规则权限码应迁移为 canonical 四段式")
    void shouldMigrateRiskRulePermissionCodesToCanonical() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        Integer oldCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN ('risk.block.execute','risk.block.release')",
            Integer.class
        );
        Integer canonicalCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN ('risk.block.execute.ALL','risk.block.release.ALL')",
            Integer.class
        );

        assertThat(oldCodeCount).isEqualTo(0);
        assertThat(canonicalCodeCount).isEqualTo(2);
    }

    @Test
    @DisplayName("用户标签权限码应迁移为 canonical 四段式")
    void shouldMigrateUserTagPermissionCodesToCanonical() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        Integer oldCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN ('user.tag.add','user.tag.view')",
            Integer.class
        );
        Integer canonicalCodeCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code IN ('user.tag.add.ALL','user.tag.view.ALL')",
            Integer.class
        );

        assertThat(oldCodeCount).isEqualTo(0);
        assertThat(canonicalCodeCount).isEqualTo(2);
    }

    @Test
    @DisplayName("V73/V75 引入的目标权限码应全部完成 canonical 迁移")
    void shouldCanonicalizeAllTargetPermissionsFromV73AndV75() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        Integer legacyCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission " +
                "WHERE permission_code IN (" +
                "'dashboard.chart.realtime','dashboard.chart.history','dashboard.kpi.config'," +
                "'risk.block.execute','risk.block.release'," +
                "'user.tag.add','user.tag.view'," +
                "'system.api-key.view','system.api-key.create','system.api-key.enable'," +
                "'system.api-key.disable','system.api-key.delete','system.api-key.reset','system.api-key.manage')",
            Integer.class
        );
        Integer canonicalCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission " +
                "WHERE permission_code IN (" +
                "'dashboard.chart.realtime.ALL','dashboard.chart.history.ALL','dashboard.kpi.config.ALL'," +
                "'risk.block.execute.ALL','risk.block.release.ALL'," +
                "'user.tag.add.ALL','user.tag.view.ALL'," +
                "'system.api-key.view.ALL','system.api-key.create.ALL','system.api-key.enable.ALL'," +
                "'system.api-key.disable.ALL','system.api-key.delete.ALL','system.api-key.reset.ALL','system.api-key.manage.ALL')",
            Integer.class
        );

        assertThat(legacyCount).isEqualTo(0);
        assertThat(canonicalCount).isEqualTo(14);
    }

    @Test
    @DisplayName("Canonical权限码全集强校验（基线驱动）")
    void shouldValidateCanonicalPermissionsAgainstBaseline() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        // 从基线文件读取期望权限码集合
        Set<String> expectedPermissions = loadBaselinePermissions();
        assertThat(expectedPermissions)
            .as("基线文件不应为空")
            .isNotEmpty();

        // 获取数据库中所有canonical格式的权限码（四段式，结尾为.ALL）
        Set<String> actualPermissions = new HashSet<>(jdbcTemplate.queryForList(
            "SELECT DISTINCT permission_code FROM sys_permission WHERE permission_code LIKE '%.ALL'",
            String.class
        ));

        // 计算缺失和多余的权限码
        Set<String> missingPermissions = new HashSet<>(expectedPermissions);
        missingPermissions.removeAll(actualPermissions);

        Set<String> extraPermissions = new HashSet<>(actualPermissions);
        extraPermissions.removeAll(expectedPermissions);

        // 生成详细错误信息
        if (!missingPermissions.isEmpty()) {
            System.out.println("缺失的权限码 (" + missingPermissions.size() + "): " + missingPermissions);
        }
        if (!extraPermissions.isEmpty()) {
            System.out.println("多余的权限码 (" + extraPermissions.size() + "): " + extraPermissions);
        }

        // 断言：数据库权限码集合必须与基线完全匹配
        assertThat(actualPermissions)
            .as("数据库权限码数量应与基线一致，期望: %d，实际: %d", expectedPermissions.size(), actualPermissions.size())
            .hasSize(expectedPermissions.size());

        assertThat(missingPermissions)
            .as("不应有缺失的权限码")
            .isEmpty();

        assertThat(extraPermissions)
            .as("不应有多余的权限码")
            .isEmpty();
    }

    @Test
    @DisplayName("Legacy权限码应为0（所有旧格式权限码已迁移）")
    void shouldHaveZeroLegacyPermissionCodes() {
        JdbcTemplate jdbcTemplate = migrateWithFlyway();

        // 查询是否存在非canonical格式的权限码（即不以.ALL结尾的）
        Integer legacyCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_permission WHERE permission_code NOT LIKE '%.ALL'",
            Integer.class
        );

        assertThat(legacyCount)
            .as("Legacy权限码应为0，实际为: %d", legacyCount)
            .isEqualTo(0);
    }

    private Set<String> loadBaselinePermissions() {
        Set<String> permissions = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    getClass().getClassLoader().getResourceAsStream("permission/canonical-permissions-90.txt"),
                    StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                // 跳过注释和空行
                if (!line.isEmpty() && !line.startsWith("#")) {
                    permissions.add(line);
                }
            }
        } catch (Exception e) {
            fail("无法加载权限码基线文件: " + e.getMessage());
        }
        return permissions;
    }

    private JdbcTemplate migrateWithFlyway() {
        if (!isContainerRuntimeConfigured()) {
            if (isStrictMigrationMode()) {
                fail("严格模式下未检测到 Docker/Podman socket，禁止跳过 PostgreSQL 迁移验证");
            }
            Assumptions.assumeTrue(false, "未检测到 Docker/Podman socket，跳过 PostgreSQL 迁移验证");
        }

        postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("mosquito_permission_canonical_test")
            .withUsername("mosquito")
            .withPassword("mosquito");

        try {
            postgres.start();
        } catch (Throwable e) {
            if (isStrictMigrationMode()) {
                fail("严格模式下 PostgreSQL 容器启动失败: " + e.getMessage(), e);
            }
            Assumptions.assumeTrue(false, "无法启动 PostgreSQL 容器，跳过测试: " + e.getMessage());
        }

        DataSource dataSource = new DriverManagerDataSource(
            postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());

        Flyway flyway = Flyway.configure()
            .dataSource(dataSource)
            .locations("classpath:db/migration")
            .cleanDisabled(true)
            .load();
        MigrateResult result = flyway.migrate();
        assertThat(result.success).isTrue();

        return new JdbcTemplate(dataSource);
    }

    private boolean isContainerRuntimeConfigured() {
        String dockerHost = System.getenv("DOCKER_HOST");
        if (dockerHost != null && !dockerHost.isBlank()) {
            if (dockerHost.startsWith("unix://")) {
                Path socketPath = Path.of(dockerHost.substring("unix://".length()));
                return Files.exists(socketPath);
            }
            return true;
        }

        Path dockerSock = Path.of("/var/run/docker.sock");
        if (Files.exists(dockerSock)) {
            return true;
        }

        Path runUser = Path.of("/run/user");
        if (!Files.isDirectory(runUser)) {
            return false;
        }

        try (Stream<Path> userDirs = Files.list(runUser)) {
            return userDirs.anyMatch(dir -> Files.exists(dir.resolve("podman/podman.sock")));
        } catch (Exception e) {
            return false;
        }
    }

    private boolean isStrictMigrationMode() {
        return Boolean.parseBoolean(System.getProperty("migration.test.strict", "false"))
            || "true".equalsIgnoreCase(System.getenv("STRICT_MIGRATION_TESTS"));
    }
}
