import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { LuckyGiftConfigDialog } from "./LuckyGiftConfigDialog.jsx";

afterEach(() => {
    vi.unstubAllGlobals();
});

test("keeps every dynamic_v3 control reachable while locking rule-time pool injection", () => {
    renderDialog(dynamicConfig());

    const dialog = screen.getByRole("dialog");
    expect(within(dialog).getByRole("navigation", { name: "全局参数分区" })).toBeInTheDocument();
    ["规则", "资金", "水位", "大奖", "限额"].forEach((label) => {
        expect(within(dialog).getByRole("button", { name: label })).toBeInTheDocument();
    });

    expect(within(dialog).getByRole("switch", { name: "发布状态" })).toBeChecked();
    expect(within(dialog).getByLabelText(/初始奖池金币/)).toHaveValue(0);
    expect(within(dialog).getByLabelText(/初始奖池金币/)).toBeDisabled();
    [
        "低水位金币",
        "高水位金币",
        "充值加权窗口 (ms)",
        "独立大奖倍率",
        "全局大奖 RTP 上限 (%)",
        "用户日累计消费触发门槛（金币）",
        "单次返奖上限",
        "主播每日上限",
    ].forEach((label) =>
        expect(within(dialog).getByLabelText((accessibleName) => accessibleName.startsWith(label))).toBeInTheDocument(),
    );
    expect(within(dialog).getByLabelText(/用户日累计消费触发门槛/)).toBeEnabled();

    expect(within(dialog).getAllByRole("tab")).toHaveLength(3);
    expect(within(dialog).getByRole("columnheader", { name: "倍率" })).toBeInTheDocument();
    expect(within(dialog).getByRole("columnheader", { name: "普通中奖机会 %" })).toBeInTheDocument();
    expect(within(dialog).getByRole("heading", { name: "独立大奖路径" })).toBeInTheDocument();
    ["共享开奖前提", "48 小时亏损补偿", "累计消费达标"].forEach((heading) => {
        expect(within(dialog).getByRole("heading", { name: heading })).toBeInTheDocument();
    });
    expect(within(dialog).getByText(/实际 RTP 小于或等于配置上限才通过/)).toBeInTheDocument();
    expect(within(dialog).getByText(/每满一次门槛就产生 1 个大奖资格/)).toBeInTheDocument();
    expect(within(dialog).getByText(/大盘、水位、次数或限额不通过时资格会跨日保留/)).toBeInTheDocument();
    expect(within(dialog).getByLabelText("新手阶段第 1 档倍率")).toBeInTheDocument();
    expect(within(dialog).getByLabelText("新手阶段第 2 档概率 %")).toHaveAttribute("readonly");
    expect(within(dialog).getByRole("button", { name: "删除新手阶段第 1 档" })).toBeInTheDocument();
    expect(within(dialog).getByLabelText("新手阶段派生充值范围")).toHaveTextContent("7 日充值 < 0 或 30 日充值 < 1");
    expect(
        within(dialog).getByText("任一未达到即为新手；7 日与 30 日均达到门槛（含等于）后进入普通。"),
    ).toBeInTheDocument();

    fireEvent.click(within(dialog).getByRole("tab", { name: /普通/ }));
    expect(within(dialog).getByLabelText("7 日最低充值（金币）")).toHaveValue(0);
    expect(within(dialog).getByLabelText("30 日最低充值（金币）")).toHaveValue(1);
});

test("section navigation handles reduced motion and falls back after switching from dynamic_v3 to fixed_v2", async () => {
    vi.stubGlobal(
        "matchMedia",
        vi.fn().mockReturnValue({
            addEventListener: vi.fn(),
            matches: true,
            media: "(prefers-reduced-motion: reduce)",
            removeEventListener: vi.fn(),
        }),
    );
    renderDialog(dynamicConfig());

    const dialog = screen.getByRole("dialog");
    const sectionList = dialog.querySelector(".ops-config-section-list");
    const identitySection = dialog.querySelector('[data-config-section="identity"]');
    const waterSection = dialog.querySelector('[data-config-section="water"]');
    const scrollTo = vi.fn();
    Object.defineProperty(sectionList, "clientHeight", { configurable: true, value: 480 });
    Object.defineProperty(sectionList, "scrollHeight", { configurable: true, value: 1_400 });
    Object.defineProperty(sectionList, "scrollTo", { configurable: true, value: scrollTo });
    Object.defineProperty(sectionList, "getBoundingClientRect", {
        configurable: true,
        value: () => ({ top: 200 }),
    });
    Object.defineProperty(waterSection, "getBoundingClientRect", {
        configurable: true,
        value: () => ({ top: 520 }),
    });
    // 尺寸变化后监听器也必须从响应式外层重新绑定到桌面内层 scroller。
    fireEvent(window, new Event("resize"));

    fireEvent.click(within(dialog).getByRole("button", { name: "水位" }));
    expect(scrollTo).toHaveBeenLastCalledWith({ behavior: "auto", top: 312 });
    expect(within(dialog).getByRole("button", { name: "水位" })).toHaveAttribute("aria-current", "true");
    expect(within(dialog).getByRole("heading", { name: "动态水位" })).toHaveFocus();

    sectionList.scrollTop = 137;
    Object.defineProperty(identitySection, "getBoundingClientRect", {
        configurable: true,
        value: () => ({ top: 71 }),
    });
    fireEvent.click(within(dialog).getByRole("button", { name: "规则" }));
    expect(scrollTo).toHaveBeenLastCalledWith({ behavior: "auto", top: 0 });
    expect(within(dialog).getByRole("heading", { name: "规则身份" })).toHaveFocus();

    fireEvent.click(within(dialog).getByRole("button", { name: "水位" }));

    fireEvent.mouseDown(within(dialog).getByLabelText("策略版本"));
    fireEvent.click(screen.getByRole("option", { name: "fixed_v2（历史兼容）" }));

    await waitFor(() => {
        expect(within(dialog).queryByRole("button", { name: "水位" })).not.toBeInTheDocument();
        expect(within(dialog).getByRole("button", { name: "规则" })).toHaveAttribute("aria-current", "true");
    });
    expect(within(dialog).queryByLabelText(/初始奖池金币/)).not.toBeInTheDocument();
    expect(within(dialog).getByRole("switch", { name: "发布状态" })).toBeInTheDocument();
});

test.each([390, 1024])(
    "uses the dialog content scroller for %ipx responsive navigation and active-section sync",
    (viewportWidth) => {
        renderDialog(dynamicConfig());

        const dialog = screen.getByRole("dialog");
        const sectionNav = dialog.querySelector(".ops-config-section-nav");
        const sectionList = dialog.querySelector(".ops-config-section-list");
        const outerScroller = sectionList.closest(".ops-config-dialog-content");
        const innerScrollTo = vi.fn();
        const outerScrollTo = vi.fn();
        // 不向组件暴露 viewport 宽度；测试通过导航的真实 flex 布局区分手机横栏和平板左栏。
        sectionNav.style.flexDirection = viewportWidth === 390 ? "row" : "column";
        Object.defineProperty(sectionNav, "getBoundingClientRect", {
            configurable: true,
            value: () => ({ height: 50 }),
        });
        Object.defineProperty(sectionList, "clientHeight", { configurable: true, value: 1_200 });
        Object.defineProperty(sectionList, "scrollHeight", { configurable: true, value: 1_200 });
        Object.defineProperty(sectionList, "scrollTo", { configurable: true, value: innerScrollTo });
        Object.defineProperty(outerScroller, "scrollTop", { configurable: true, value: 1_837, writable: true });
        Object.defineProperty(outerScroller, "scrollTo", { configurable: true, value: outerScrollTo });
        Object.defineProperty(outerScroller, "getBoundingClientRect", {
            configurable: true,
            value: () => ({ top: 120 }),
        });

        const sectionTops = {
            funds: -800,
            identity: -1_000,
            jackpot: 180,
            limits: 800,
            water: -579,
        };
        for (const [sectionKey, top] of Object.entries(sectionTops)) {
            Object.defineProperty(
                dialog.querySelector(`[data-config-section="${sectionKey}"]`),
                "getBoundingClientRect",
                {
                    configurable: true,
                    value: () => ({ top }),
                },
            );
        }

        fireEvent.scroll(outerScroller);
        const expectedActiveSection = viewportWidth === 390 ? "大奖" : "水位";
        expect(within(dialog).getByRole("button", { name: expectedActiveSection })).toHaveAttribute(
            "aria-current",
            "true",
        );

        fireEvent.click(within(dialog).getByRole("button", { name: "水位" }));
        expect(outerScrollTo).toHaveBeenCalledWith({
            behavior: "smooth",
            // 横向 sticky nav 占 50px，目标标题落在其底边下方 8px；左栏布局不需要额外偏移。
            top: viewportWidth === 390 ? 1_080 : 1_130,
        });
        expect(innerScrollTo).not.toHaveBeenCalled();
    },
);

test("stage tabs expose complete aria relationships and support roving keyboard navigation", () => {
    renderDialog(dynamicConfig());

    const dialog = screen.getByRole("dialog");
    const noviceTab = within(dialog).getByRole("tab", { name: /新手/ });
    const normalTab = within(dialog).getByRole("tab", { name: /普通/ });
    const advancedTab = within(dialog).getByRole("tab", { name: /高阶/ });

    expect(noviceTab).toHaveAttribute("aria-controls", "ops-stage-panel-novice");
    expect(noviceTab).toHaveAttribute("tabindex", "0");
    expect(normalTab).toHaveAttribute("tabindex", "-1");
    expect(within(dialog).getByRole("tabpanel")).toHaveAttribute("aria-labelledby", "ops-stage-tab-novice");

    fireEvent.keyDown(noviceTab, { key: "ArrowRight" });
    expect(normalTab).toHaveFocus();
    expect(normalTab).toHaveAttribute("aria-selected", "true");
    expect(within(dialog).getByRole("tabpanel")).toHaveAttribute("id", "ops-stage-panel-normal");
    expect(within(dialog).getByLabelText("正常阶段第 1 档倍率")).toBeInTheDocument();

    fireEvent.keyDown(normalTab, { key: "End" });
    expect(advancedTab).toHaveFocus();
    expect(advancedTab).toHaveAttribute("aria-selected", "true");
    fireEvent.keyDown(advancedTab, { key: "Home" });
    expect(noviceTab).toHaveFocus();
    expect(noviceTab).toHaveAttribute("aria-selected", "true");
});

test("fixed_v2 hides only dynamic controls and preserves immutable snapshot fields on submit", () => {
    const onSubmit = vi.fn();
    renderDialog(fixedConfig(), { onSubmit });

    const dialog = screen.getByRole("dialog");
    expect(within(dialog).getByRole("switch", { name: "发布状态" })).toBeChecked();
    expect(within(dialog).queryByRole("button", { name: "水位" })).not.toBeInTheDocument();
    expect(within(dialog).queryByText(/资金合计/)).not.toBeInTheDocument();
    expect(within(dialog).queryByLabelText(/初始奖池金币/)).not.toBeInTheDocument();
    expect(within(dialog).getAllByRole("tab")).toHaveLength(3);

    fireEvent.click(within(dialog).getByRole("button", { name: "保存配置" }));

    expect(onSubmit).toHaveBeenCalledWith(
        expect.objectContaining({
            effective_from_ms: 987_654_321,
            enabled: true,
            strategy_version: "fixed_v2",
            stages: expect.arrayContaining([
                expect.objectContaining({
                    stage: "novice",
                    tiers: expect.arrayContaining([
                        expect.objectContaining({
                            enabled: true,
                            high_water_only: false,
                            tier_id: "novice_none",
                        }),
                    ]),
                }),
            ]),
        }),
    );
});

test("saves an ordinary tier and the independent jackpot list when they share a multiplier", () => {
    const onSubmit = vi.fn();
    const stages = stagesFor(89).map((stage) =>
        stage.stage === "normal"
            ? {
                  ...stage,
                  tiers: [
                      { ...stage.tiers[0], probability_percent: 10.99 },
                      stage.tiers[1],
                      {
                          enabled: true,
                          high_water_only: true,
                          multiplier: 200,
                          probability_percent: 0.01,
                          tier_id: "normal_200x",
                      },
                  ],
              }
            : stage,
    );
    renderDialog(dynamicConfig({ stages }), { onSubmit });

    const dialog = screen.getByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "保存配置" }));

    expect(within(dialog).queryByRole("alert")).not.toBeInTheDocument();
    expect(onSubmit).toHaveBeenCalledWith(
        expect.objectContaining({
            jackpot_multipliers: [200, 500, 1000],
            stages: expect.arrayContaining([
                expect.objectContaining({
                    stage: "normal",
                    tiers: expect.arrayContaining([
                        expect.objectContaining({
                            high_water_only: true,
                            multiplier: 200,
                            probability_percent: 0.01,
                            tier_id: "normal_200x",
                        }),
                    ]),
                }),
            ]),
        }),
    );
});

test("focuses the validation summary when the fixed action bar submission is rejected", async () => {
    const onSubmit = vi.fn();
    renderDialog(dynamicConfig({ low_watermark_coins: 0 }), { onSubmit });

    const dialog = screen.getByRole("dialog");
    fireEvent.click(within(dialog).getByRole("button", { name: "保存配置" }));

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

function renderDialog(config, overrides = {}) {
    return render(
        <LuckyGiftConfigDialog
            appOptions={[
                ["lalu", "Lalu"],
                ["yumi", "Yumi"],
            ]}
            config={config}
            giftHostDiamondRatiosByApp={{
                lalu: { lucky: 10, super_lucky: 1 },
                yumi: { lucky: 10, super_lucky: 1 },
            }}
            mode="edit"
            saving={false}
            onClose={vi.fn()}
            onSubmit={vi.fn()}
            {...overrides}
        />,
    );
}

function dynamicConfig(overrides = {}) {
    return {
        anchor_daily_payout_cap: 6_000_000,
        anchor_rate_percent: 10,
        app_code: "lalu",
        control_band_percent: 3,
        device_daily_payout_cap: 4_000_000,
        effective_from_ms: 123_456_789,
        enabled: true,
        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_id: "lucky",
        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,
        rule_version: 3,
        settlement_window_wager: 1_000_000,
        stages: stagesFor(89),
        strategy_version: "dynamic_v3",
        target_rtp_percent: 89,
        user_daily_payout_cap: 3_000_000,
        user_hourly_payout_cap: 2_000_000,
        ...overrides,
    };
}

function fixedConfig() {
    return {
        app_code: "lalu",
        control_band_percent: 1,
        effective_from_ms: 987_654_321,
        enabled: true,
        gift_price_reference: 100,
        pool_id: "legacy",
        pool_rate_percent: 95,
        rule_version: 8,
        settlement_window_wager: 1_000_000,
        stages: stagesFor(95),
        strategy_version: "fixed_v2",
        target_rtp_percent: 95,
    };
}

function stagesFor(targetRTP) {
    return ["novice", "normal", "advanced"].map((stage, index) => ({
        min_recharge_30d_coins: index === 0 ? 0 : 1,
        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`,
            },
        ],
    }));
}
