import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, expect, test, vi } from "vitest";
import { PERMISSIONS } from "@/app/permissions";
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
import {
    adjustLuckyGiftPool,
    createLuckyGiftExperiment,
    endLuckyGiftExperiment,
    fetchLuckyGiftDraws,
    fetchLuckyGiftConfigVersions,
    fetchLuckyGiftExperiments,
    fetchOpsDashboard,
    rollbackLuckyGiftConfig,
    saveLuckyGiftConfig,
    setLuckyGiftExperimentOverrides,
    updateLuckyGiftExperiment,
} from "./api.js";
import { initialOpsCenterView, OpsCenterApp } from "./OpsCenterApp.jsx";

vi.mock("./api.js", () => ({
    DEFAULT_POOL_ID: "default",
    adjustLuckyGiftPool: vi.fn(),
    createLuckyGiftExperiment: vi.fn(),
    endLuckyGiftExperiment: vi.fn(),
    fetchLuckyGiftDraws: vi.fn(),
    fetchLuckyGiftConfigVersions: vi.fn(),
    fetchLuckyGiftExperiments: vi.fn(),
    fetchOpsDashboard: vi.fn(),
    luckyGiftPoolKey: (item = {}) =>
        `${item.app_code || item.appCode}:${item.pool_id || item.poolId}:${item.strategy_version || item.strategyVersion || "fixed_v2"}`,
    rollbackLuckyGiftConfig: vi.fn(),
    saveLuckyGiftConfig: vi.fn(),
    setLuckyGiftExperimentOverrides: vi.fn(),
    updateLuckyGiftExperiment: vi.fn(),
}));

beforeEach(() => {
    vi.clearAllMocks();
    window.history.replaceState({}, "", "/");
    saveLuckyGiftConfig.mockResolvedValue({ app_code: "lalu", pool_id: "lucky" });
    createLuckyGiftExperiment.mockResolvedValue({
        experiment: { experiment_id: "exp-new", status: "running", treatment_rule_version: 4 },
        treatment_config: { app_code: "lalu", pool_id: "lucky", rule_version: 4 },
    });
    fetchLuckyGiftExperiments.mockResolvedValue([mockRunningExperiment()]);
    updateLuckyGiftExperiment.mockResolvedValue({ ...mockRunningExperiment(), treatment_traffic_percent: 20 });
    setLuckyGiftExperimentOverrides.mockResolvedValue({ ...mockRunningExperiment(), overrides: [] });
    endLuckyGiftExperiment.mockResolvedValue({
        experiment: {
            ...mockRunningExperiment(),
            ended_at_ms: 1767000600000,
            final_rule_version: 4,
            status: "completed",
            winner_arm: "treatment",
        },
        final_config: { app_code: "lalu", pool_id: "super_lucky", rule_version: 4 },
    });
    adjustLuckyGiftPool.mockResolvedValue({
        adjustment: { balance_after: 39_559_017, balance_before: 38_559_017 },
        idempotent_replay: false,
    });
    fetchLuckyGiftConfigVersions.mockResolvedValue([]);
    rollbackLuckyGiftConfig.mockResolvedValue({
        config: { app_code: "lalu", pool_id: "super_lucky", rule_version: 22 },
        sourceRuleVersion: 20,
    });
    fetchLuckyGiftDraws.mockResolvedValue({
        items: [
            {
                app_code: "lalu",
                created_at_ms: 1767000000000,
                draw_id: "draw-1",
                effective_reward_coins: 100,
                external_user_id: "ext-1",
                gift_id: "gift-9",
                multiplier_ppm: 2_000_000,
                pool_id: "super_lucky",
                reward_status: "granted",
                rule_version: 2,
            },
        ],
        page: 1,
        pageSize: 20,
        total: 1,
    });
    fetchOpsDashboard.mockResolvedValue({
        experiments: [mockRunningExperiment()],
        giftHostDiamondRatiosByApp: {
            lalu: { lucky: 10, super_lucky: 1 },
            yumi: { lucky: 10, super_lucky: 1 },
        },
        apps: [
            { app_code: "lalu", app_name: "Lalu", source: "registry", status: "active", updated_at_ms: 1767000000000 },
            { app_code: "yumi", app_name: "Yumi", source: "fixed", status: "active" },
        ],
        appSummaries: [
            {
                actual_rtp_ppm: 900000,
                app_code: "lalu",
                failed_draws: 0,
                granted_draws: 3,
                pending_draws: 0,
                total_draws: 3,
                total_reward_coins: 900,
                total_spent_coins: 1000,
                unique_rooms: 2,
                unique_users: 2,
            },
        ],
        luckyGifts: [
            {
                ...mockDynamicConfig(),
                app_code: "lalu",
                enabled: true,
                is_default: false,
                pool_id: "lucky",
                rule_version: 3,
            },
            {
                ...mockFixedConfig(95),
                app_code: "lalu",
                enabled: false,
                is_default: false,
                pool_id: "lucky",
                rule_version: 2,
            },
            {
                ...mockFixedConfig(92),
                app_code: "lalu",
                enabled: false,
                is_default: false,
                pool_id: "super_lucky",
                rule_version: 2,
            },
            {
                ...mockDynamicConfig(),
                app_code: "yumi",
                enabled: false,
                is_default: true,
                pool_id: "default",
                rule_version: 0,
            },
        ],
        poolBalances: [
            {
                app_code: "lalu",
                available_balance: 37950017,
                balance: 38559017,
                manual_credit_total: 1_000_000,
                manual_debit_total: 200_000,
                materialized: true,
                pool_id: "lucky",
                reserve_floor: 609000,
                strategy_version: "dynamic_v3",
                updated_at_ms: 1767000000000,
            },
            {
                app_code: "lalu",
                available_balance: 0,
                balance: 900,
                manual_credit_total: 100,
                manual_debit_total: 20,
                materialized: true,
                pool_id: "lucky",
                reserve_floor: 900,
                strategy_version: "fixed_v2",
                updated_at_ms: 1767000000000,
            },
            {
                app_code: "lalu",
                available_balance: 9_000_000,
                balance: 10_000_000,
                materialized: true,
                pool_id: "super_lucky",
                reserve_floor: 1_000_000,
                strategy_version: "dynamic_v3",
            },
            {
                app_code: "lalu",
                available_balance: 1_500_000,
                balance: 2_000_000,
                materialized: true,
                pool_id: "super_lucky",
                reserve_floor: 500_000,
                strategy_version: "fixed_v2",
            },
            {
                app_code: "yumi",
                available_balance: 361500,
                balance: 382500,
                materialized: false,
                pool_id: "default",
                reserve_floor: 21000,
                strategy_version: "dynamic_v3",
            },
        ],
        summary: {
            activeApps: 2,
            configuredPools: 2,
            enabledPools: 1,
            failedDraws: 0,
            grantedDraws: 3,
            pendingDraws: 0,
            poolAvailableTotal: 38311517,
            poolBalanceTotal: 38941517,
            poolReserveTotal: 630000,
            totalDraws: 3,
            totalRewardCoins: 900,
            totalSpentCoins: 1000,
        },
    });
});

// 实验权限常量与 permissions.ts 并行开发，测试使用后端约定的权限码字面量（两者最终一致）。
const EXPERIMENT_PERMISSION = "lucky-gift:experiment";
const allPermissions = [
    PERMISSIONS.luckyGiftPoolCredit,
    PERMISSIONS.luckyGiftPoolDebit,
    PERMISSIONS.luckyGiftUpdate,
    EXPERIMENT_PERMISSION,
];

// 活跃实验挂在 lalu/super_lucky：该奖池行必须显示实验徽章并禁用常规发布，
// lalu/lucky（已启用且无实验）则暴露“创建 AB 实验”入口。
function mockRunningExperiment() {
    return {
        app_code: "lalu",
        arm_stats: [
            {
                actual_rtp_percent: 95.2,
                arm: "control",
                rule_version: 2,
                strategy_version: "dynamic_v3",
                total_draws: 100,
                total_reward_coins: 952,
                total_spent_coins: 1000,
                unique_users: 40,
            },
            {
                actual_rtp_percent: 97.9,
                arm: "treatment",
                rule_version: 3,
                strategy_version: "fixed_v2",
                total_draws: 20,
                total_reward_coins: 196,
                total_spent_coins: 200,
                unique_users: 6,
            },
        ],
        control_rule_version: 2,
        experiment_id: "exp-1",
        name: "rtp-up",
        overrides: [{ arm: "treatment", user_key: "1001" }],
        pool_id: "super_lucky",
        started_at_ms: 1767000000000,
        status: "running",
        treatment_rule_version: 3,
        treatment_traffic_percent: 5,
    };
}

function renderApp(permissions = allPermissions) {
    return render(
        <ToastProvider>
            <OpsCenterApp can={(code) => permissions.includes(code)} />
        </ToastProvider>,
    );
}

test("renders overview with aggregated kpis and pool balances", async () => {
    renderApp();

    expect(await screen.findByRole("heading", { name: "运营配置中心" })).toBeTruthy();
    expect(screen.getByRole("navigation", { name: "运营中心导航" })).toBeTruthy();
    ["数据总览", "幸运礼物配置", "抽奖记录", "应用列表"].forEach((label) => {
        expect(screen.getByRole("button", { name: new RegExp(label) })).toBeTruthy();
    });

    expect(await screen.findByText("运营应用")).toBeTruthy();
    // 抽奖次数 KPI 必须是跨应用聚合值，而不是第一个应用的汇总（KPI 卡和汇总表列头各出现一次）。
    expect(screen.getAllByText("抽奖次数").length).toBeGreaterThan(0);
    expect(screen.getAllByText("38,941,517").length).toBeGreaterThan(0);
    expect(screen.getByText("奖池实时水位")).toBeTruthy();
    expect(screen.getAllByText("已入账").length).toBeGreaterThan(1);
    expect(screen.getByText("默认水位")).toBeTruthy();
    expect(fetchOpsDashboard).toHaveBeenCalledTimes(1);
});

test("opens the requested config view from the legacy admin document redirect", async () => {
    expect(initialOpsCenterView("?view=configs")).toBe("configs");
    expect(initialOpsCenterView("?view=unknown")).toBe("overview");
    window.history.replaceState({}, "", "/ops-center/?view=configs");

    renderApp();

    expect(await screen.findByRole("button", { name: "添加配置" })).toBeTruthy();
    expect(screen.getAllByText("lucky").length).toBeGreaterThan(0);
});

test("lists every pool config including published non-default pools", async () => {
    renderApp();
    await screen.findByText("运营应用");

    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));

    // 核心回归：lalu 的 lucky / super_lucky 已发布奖池必须出现在配置列表里。
    expect((await screen.findAllByText("lucky")).length).toBeGreaterThan(1);
    expect(screen.getByText("super_lucky")).toBeTruthy();
    expect(screen.getByText("v3")).toBeTruthy();
    expect(screen.getAllByText("v2").length).toBeGreaterThan(0);
    expect(screen.getByText("已启用")).toBeTruthy();
    expect(screen.getAllByText("已停用").length).toBeGreaterThan(1);
    expect(screen.getByText("默认草稿")).toBeTruthy();
});

test("shows strategy-specific balances and adjustment actions directly in the config list", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));

    expect(await screen.findByRole("button", { name: "添加水位 lalu / lucky / dynamic_v3" })).toBeTruthy();
    expect(screen.getByRole("button", { name: "扣减水位 lalu / lucky / dynamic_v3" })).toBeEnabled();
    expect(screen.getByRole("button", { name: "添加水位 lalu / lucky / fixed_v2" })).toBeTruthy();
    expect(screen.getByRole("button", { name: "扣减水位 lalu / lucky / fixed_v2" })).toBeDisabled();
    expect(screen.getByText("38,559,017")).toBeTruthy();
    expect(screen.getByText("900")).toBeTruthy();
});

test("shows credit and debit buttons only when the standalone session grants each permission", async () => {
    renderApp([PERMISSIONS.luckyGiftPoolCredit]);
    await screen.findByText("运营应用");

    expect(screen.getByRole("button", { name: "添加水位 lalu / lucky / dynamic_v3" })).toBeTruthy();
    expect(screen.queryByRole("button", { name: "扣减水位 lalu / lucky / dynamic_v3" })).toBeNull();
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    expect(screen.queryByRole("button", { name: "添加配置" })).toBeNull();
    expect(screen.queryByRole("button", { name: /新增版本 lalu/ })).toBeNull();
});

test("submits a pool credit, reports before and after balances, closes, and refreshes", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: "添加水位 lalu / lucky / dynamic_v3" }));

    const dialog = await screen.findByRole("dialog", { name: "添加奖池水位" });
    expect(within(dialog).getByText("dynamic_v3")).toBeTruthy();
    fireEvent.change(within(dialog).getByLabelText(/调整金币/), { target: { value: "1000000" } });
    fireEvent.change(within(dialog).getByLabelText(/调整原因/), { target: { value: "运营补水" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "确认添加" }));

    await waitFor(() => {
        expect(adjustLuckyGiftPool).toHaveBeenCalledWith(
            expect.objectContaining({
                adjustmentId: expect.any(String),
                amountCoins: 1_000_000,
                appCode: "lalu",
                direction: "in",
                poolId: "lucky",
                reason: "运营补水",
                strategyVersion: "dynamic_v3",
            }),
        );
    });
    expect(await screen.findByText(/38,559,017 → 39,559,017/)).toBeTruthy();
    await waitFor(() => expect(screen.queryByRole("dialog", { name: "添加奖池水位" })).toBeNull());
    expect(fetchOpsDashboard).toHaveBeenCalledTimes(2);
});

test("keeps the form and adjustment id stable when a debit request is retried", async () => {
    adjustLuckyGiftPool.mockRejectedValueOnce(new Error("可用水位已变化")).mockResolvedValueOnce({
        adjustment: { balance_after: 38_558_917, balance_before: 38_559_017 },
        idempotent_replay: false,
    });
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: "扣减水位 lalu / lucky / dynamic_v3" }));

    const dialog = await screen.findByRole("dialog", { name: "扣减奖池水位" });
    fireEvent.change(within(dialog).getByLabelText(/调整金币/), { target: { value: "100" } });
    fireEvent.change(within(dialog).getByLabelText(/调整原因/), { target: { value: "冲正" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "确认扣减" }));

    expect(await screen.findByText("可用水位已变化")).toBeTruthy();
    expect(within(dialog).getByLabelText(/调整金币/)).toHaveValue("100");
    expect(within(dialog).getByLabelText(/调整原因/)).toHaveValue("冲正");
    const firstAdjustmentID = adjustLuckyGiftPool.mock.calls[0][0].adjustmentId;

    fireEvent.click(within(dialog).getByRole("button", { name: "确认扣减" }));
    await waitFor(() => expect(adjustLuckyGiftPool).toHaveBeenCalledTimes(2));
    expect(adjustLuckyGiftPool.mock.calls[1][0].adjustmentId).toBe(firstAdjustmentID);
});

test("creates a disabled dynamic draft without copying another app monetary limits", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    fireEvent.click(await screen.findByRole("button", { name: "添加配置" }));

    const dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByText("添加幸运礼物配置")).toBeTruthy();
    expect(within(dialog).getByLabelText("策略版本").textContent).toContain("dynamic_v3");
    expect(within(dialog).getByLabelText(/初始奖池金币/).value).toBe("0");
    // 第一条 lalu 已发布配置的单次上限为 100 万；新建跨 App 配置必须归零并保持停用。
    expect(within(dialog).getByLabelText(/单次返奖上限/).value).toBe("0");
    expect(within(dialog).getByText("发布后保持停用")).toBeTruthy();
});

test("opens config dialog from a published pool row and saves a new version", async () => {
    renderApp();
    await screen.findByText("运营应用");

    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    await screen.findAllByText("lucky");

    fireEvent.click(screen.getByRole("button", { name: "新增版本 lalu / lucky / dynamic_v3" }));
    const dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByText("新增幸运礼物版本")).toBeTruthy();
    expect(within(dialog).getByLabelText("策略版本").textContent).toContain("dynamic_v3");
    expect(within(dialog).getByLabelText(/初始奖池金币/).value).toBe("0");
    expect(within(dialog).getByLabelText(/初始奖池金币/)).toBeDisabled();
    expect(within(dialog).getByLabelText(/大奖倍率/).value).toBe("200, 500, 1000");
    expect(within(dialog).getByLabelText("新手阶段第 1 档概率 %").readOnly).toBe(true);

    fireEvent.change(within(dialog).getByLabelText(/目标 RTP \(%\)/), { target: { value: "88" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "保存配置" }));

    await waitFor(() => {
        expect(saveLuckyGiftConfig).toHaveBeenCalledWith(
            expect.objectContaining({
                app_code: "lalu",
                anchor_rate_percent: 10,
                initial_pool_coins: 0,
                jackpot_multipliers: [200, 500, 1000],
                pool_id: "lucky",
                profit_rate_percent: 1,
                stages: expect.arrayContaining([expect.objectContaining({ stage: "novice" })]),
                strategy_version: "dynamic_v3",
                target_rtp_percent: 88,
            }),
        );
    });
});

test("blocks an invalid enabled dynamic rule before calling the save API", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    // dashboard 已完成加载，切换本地视图和打开弹窗都是同步状态；避免在全量并发测试中额外轮询文本。
    expect(screen.getAllByText("lucky").length).toBeGreaterThan(0);
    fireEvent.click(screen.getByRole("button", { name: "新增版本 lalu / lucky / dynamic_v3" }));

    const dialog = await screen.findByRole("dialog");
    fireEvent.change(within(dialog).getByLabelText(/低水位金币/), { target: { value: "0" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "保存配置" }));

    expect(await within(dialog).findByRole("alert")).toHaveTextContent("高水位必须大于正数低水位");
    expect(saveLuckyGiftConfig).not.toHaveBeenCalled();

    // 奖档编辑同样必须清掉上一次提交错误，避免用户修正后仍看到已经过期的阻断原因。
    fireEvent.change(within(dialog).getByLabelText("新手阶段第 1 档倍率"), { target: { value: "0.1" } });
    expect(within(dialog).queryByRole("alert")).toBeNull();
});

function mockDynamicConfig() {
    return {
        anchor_daily_payout_cap: 6_000_000,
        anchor_rate_percent: 10,
        control_band_percent: 3,
        device_daily_payout_cap: 4_000_000,
        effective_from_ms: 0,
        gift_price_reference: 100,
        high_water_nonzero_factor_percent: 130,
        high_watermark_coins: 20_000_000,
        initial_pool_coins: 0,
        jackpot_global_rtp_max_percent: 98,
        jackpot_multipliers: [200, 500, 1000],
        jackpot_spend_threshold_coins: 50_000,
        jackpot_user_72h_rtp_max_percent: 96,
        jackpot_user_day_rtp_max_percent: 96,
        loss_streak_guarantee: 5,
        low_water_nonzero_factor_percent: 70,
        low_watermark_coins: 10_000_000,
        max_jackpot_hits_per_user_day: 5,
        max_single_payout: 1_000_000,
        normal_max_equivalent_draws: 20_000,
        novice_max_equivalent_draws: 2_000,
        pool_rate_percent: 89,
        profit_rate_percent: 1,
        recharge_boost_factor_percent: 110,
        recharge_boost_window_ms: 300_000,
        room_hourly_payout_cap: 5_000_000,
        settlement_window_wager: 1_000_000,
        stages: mockStages(89),
        strategy_version: "dynamic_v3",
        target_rtp_percent: 89,
        user_daily_payout_cap: 3_000_000,
        user_hourly_payout_cap: 2_000_000,
    };
}

function mockFixedConfig(targetRTP) {
    return {
        control_band_percent: 1,
        gift_price_reference: 100,
        normal_max_equivalent_draws: 20_000,
        novice_max_equivalent_draws: 2_000,
        pool_rate_percent: targetRTP + 1,
        settlement_window_wager: 1_000_000,
        stages: mockStages(targetRTP),
        strategy_version: "fixed_v2",
        target_rtp_percent: targetRTP,
    };
}

function mockStages(targetRTP) {
    return ["novice", "normal", "advanced"].map((stage, index) => ({
        min_recharge_30d_coins: index === 0 ? 0 : index,
        min_recharge_7d_coins: index === 2 ? 1 : 0,
        stage,
        tiers: [
            {
                enabled: true,
                high_water_only: false,
                multiplier: 0,
                probability_percent: 100 - targetRTP,
                tier_id: `${stage}_none`,
            },
            {
                enabled: true,
                high_water_only: false,
                multiplier: 1,
                probability_percent: targetRTP,
                tier_id: `${stage}_1x`,
            },
        ],
    }));
}

test("loads draw records for the first app with server pagination", async () => {
    renderApp();
    await screen.findByText("运营应用");

    fireEvent.click(screen.getByRole("button", { name: /抽奖记录/ }));

    await waitFor(() => {
        expect(fetchLuckyGiftDraws).toHaveBeenCalledWith(
            expect.objectContaining({ appCode: "lalu", page: 1, poolId: "", status: "" }),
        );
    });
    expect(await screen.findByText("ext-1")).toBeTruthy();
    expect(screen.getByText("已发放")).toBeTruthy();
    expect(screen.getByText("2x")).toBeTruthy();
});

test("shows app registry and fixed integrations in apps view", async () => {
    renderApp();
    await screen.findByText("运营应用");

    fireEvent.click(screen.getByRole("button", { name: /应用列表/ }));

    expect(await screen.findByText("Lalu")).toBeTruthy();
    expect(screen.getByText("主数据")).toBeTruthy();
    expect(screen.getByText("外部接入")).toBeTruthy();
});

test("flags pools with a running experiment and blocks their regular publishing", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));

    // running 实验在“实验”列展示放量徽章；该奖池的常规发布被禁用并说明原因。
    expect(await screen.findByText("实验中 5%")).toBeTruthy();
    const publishButton = screen.getByRole("button", { name: "新增版本 lalu / super_lucky / fixed_v2" });
    expect(publishButton).toBeDisabled();
    expect(publishButton).toHaveAttribute("title", "实验进行中，结束实验后才能发布");
    expect(screen.getByRole("button", { name: "实验详情 lalu / super_lucky / fixed_v2" })).toBeTruthy();
    // 无实验的行不受影响；只有已启用的已发布配置才有“创建 AB 实验”入口（停用行没有可对照版本）。
    expect(screen.getByRole("button", { name: "新增版本 lalu / lucky / dynamic_v3" })).toBeEnabled();
    expect(screen.getByRole("button", { name: "创建 AB 实验 lalu / lucky / dynamic_v3" })).toBeTruthy();
    expect(screen.queryByRole("button", { name: "创建 AB 实验 lalu / lucky / fixed_v2" })).toBeNull();
    expect(screen.queryByRole("button", { name: "创建 AB 实验 lalu / super_lucky / fixed_v2" })).toBeNull();
});

test("hides experiment management entries without the experiment permission", async () => {
    renderApp([PERMISSIONS.luckyGiftUpdate]);
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));

    // 无实验权限仍能看到徽章和实验详情（后端 GET 只要求 lucky-gift:view），但没有创建入口。
    expect(await screen.findByText("实验中 5%")).toBeTruthy();
    expect(screen.getByRole("button", { name: "实验详情 lalu / super_lucky / fixed_v2" })).toBeTruthy();
    expect(screen.queryByRole("button", { name: "创建 AB 实验 lalu / lucky / dynamic_v3" })).toBeNull();
});

test("creates an ab experiment from an enabled config row with a locked enabled switch", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    fireEvent.click(await screen.findByRole("button", { name: "创建 AB 实验 lalu / lucky / dynamic_v3" }));

    const dialog = await screen.findByRole("dialog");
    expect(within(dialog).getByText("创建 AB 实验")).toBeTruthy();
    // 实验组配置必须直接可运行：启用开关锁定为开，奖池 ID 锁定在当前行。
    expect(within(dialog).getByRole("switch", { name: "发布状态" })).toBeDisabled();
    expect(within(dialog).getByRole("switch", { name: "发布状态" })).toBeChecked();
    expect(within(dialog).getByLabelText(/^奖池/)).toBeDisabled();

    // 名称必填：空名称提交被表单校验拦截（原生 required + 纯函数校验双重兜底），不发请求。
    fireEvent.click(within(dialog).getByRole("button", { name: "创建实验" }));
    expect(createLuckyGiftExperiment).not.toHaveBeenCalled();

    fireEvent.change(within(dialog).getByLabelText(/实验名称/), { target: { value: "rtp-98" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "创建实验" }));

    await waitFor(() => {
        expect(createLuckyGiftExperiment).toHaveBeenCalledWith(
            "lalu",
            expect.objectContaining({
                config: expect.objectContaining({ app_code: "lalu", enabled: true, pool_id: "lucky" }),
                name: "rtp-98",
                overrides: [],
                treatment_traffic_percent: 5,
            }),
        );
    });
    await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
    expect(fetchOpsDashboard).toHaveBeenCalledTimes(2);
});

test("manages a running experiment: stats, traffic, overrides and ending with a winner", async () => {
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    fireEvent.click(await screen.findByRole("button", { name: "实验详情 lalu / super_lucky / fixed_v2" }));

    const dialog = await screen.findByRole("dialog", { name: /实验管理/ });
    // 打开弹窗即拉 with_stats=1 的统计并展示两组卡片。
    await waitFor(() =>
        expect(fetchLuckyGiftExperiments).toHaveBeenCalledWith("lalu", { poolId: "super_lucky", withStats: true }),
    );
    expect(await within(dialog).findByText("97.90%")).toBeTruthy();
    expect(within(dialog).getByText("95.20%")).toBeTruthy();
    // “实验组”同时出现在分组卡片、白名单分组标签和新增分组下拉中，用 getAllByText 断言存在。
    expect(within(dialog).getAllByText("对照组").length).toBeGreaterThan(0);
    expect(within(dialog).getAllByText("实验组").length).toBeGreaterThan(0);

    // 两组卡片按策略精确绑定独立账本；从对照组入口补水必须提交 dynamic_v3，不能误调实验组的 V2 水位。
    expect(within(dialog).getByText("10,000,000")).toBeTruthy();
    expect(within(dialog).getByText("2,000,000")).toBeTruthy();
    fireEvent.click(within(dialog).getByRole("button", { name: "添加水位 lalu / super_lucky / dynamic_v3" }));
    const poolDialog = await screen.findByRole("dialog", { name: "添加奖池水位" });
    fireEvent.change(within(poolDialog).getByLabelText(/调整金币/), { target: { value: "1000000" } });
    fireEvent.change(within(poolDialog).getByLabelText(/调整原因/), { target: { value: "实验对照组补水" } });
    fireEvent.click(within(poolDialog).getByRole("button", { name: "确认添加" }));
    await waitFor(() =>
        expect(adjustLuckyGiftPool).toHaveBeenCalledWith(
            expect.objectContaining({
                appCode: "lalu",
                direction: "in",
                poolId: "super_lucky",
                strategyVersion: "dynamic_v3",
            }),
        ),
    );
    await waitFor(() => expect(screen.queryByRole("dialog", { name: "添加奖池水位" })).toBeNull());

    fireEvent.change(within(dialog).getByLabelText(/实验组流量/), { target: { value: "20" } });
    fireEvent.click(within(dialog).getByRole("button", { name: "应用" }));
    await waitFor(() =>
        expect(updateLuckyGiftExperiment).toHaveBeenCalledWith("lalu", "exp-1", { treatment_traffic_percent: 20 }),
    );
    expect(await screen.findByText("已调整实验组流量为 20%")).toBeTruthy();

    await waitFor(() => expect(within(dialog).getByRole("button", { name: "移除白名单 1001" })).toBeEnabled());
    fireEvent.click(within(dialog).getByRole("button", { name: "移除白名单 1001" }));
    await waitFor(() =>
        expect(setLuckyGiftExperimentOverrides).toHaveBeenCalledWith("lalu", "exp-1", {
            removes: ["1001"],
            upserts: [],
        }),
    );

    // 结束实验必须二次确认：确认层说明获胜配置将重新发布并全量生效。
    await waitFor(() => expect(within(dialog).getByRole("button", { name: "选择实验组为最终版本" })).toBeEnabled());
    fireEvent.click(within(dialog).getByRole("button", { name: "选择实验组为最终版本" }));
    expect(endLuckyGiftExperiment).not.toHaveBeenCalled();
    fireEvent.click(await within(dialog).findByRole("button", { name: "确认结束并发布" }));
    await waitFor(() => expect(endLuckyGiftExperiment).toHaveBeenCalledWith("lalu", "exp-1", "treatment"));
    expect(await screen.findByText(/实验已结束：实验组配置已重新发布为 v4 并全量生效/)).toBeTruthy();
});

test("pauses a running experiment and keeps the dialog usable when the action fails", async () => {
    updateLuckyGiftExperiment.mockRejectedValueOnce(new Error("实验状态已变化"));
    renderApp();
    await screen.findByText("运营应用");
    fireEvent.click(screen.getByRole("button", { name: /幸运礼物配置/ }));
    fireEvent.click(await screen.findByRole("button", { name: "实验详情 lalu / super_lucky / fixed_v2" }));
    const dialog = await screen.findByRole("dialog", { name: /实验管理/ });

    fireEvent.click(within(dialog).getByRole("button", { name: "暂停实验" }));
    // 失败保留弹窗与输入，运营可以直接重试。
    expect(await screen.findByText("实验状态已变化")).toBeTruthy();
    expect(screen.getByRole("dialog", { name: /实验管理/ })).toBeTruthy();

    await waitFor(() => expect(within(dialog).getByRole("button", { name: "暂停实验" })).toBeEnabled());
    fireEvent.click(within(dialog).getByRole("button", { name: "暂停实验" }));
    await waitFor(() =>
        expect(updateLuckyGiftExperiment).toHaveBeenLastCalledWith("lalu", "exp-1", { status: "paused" }),
    );
    expect(await screen.findByText("实验已暂停，全部流量（含白名单）回到对照组")).toBeTruthy();
});
