import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { createEmptyActivityTemplateForm } from "@/features/activity-templates/model.js";

const mocks = vi.hoisted(() => ({
    abilities: {
        canCreate: true,
        canData: true,
        canDelete: true,
        canExport: true,
        canPublish: true,
        canRetry: true,
        canUpdate: true,
        canView: true,
    },
    appCode: "lalu",
    cloneActivityTemplate: vi.fn(),
    confirm: vi.fn(),
    createActivityTemplate: vi.fn(),
    deleteActivityTemplate: vi.fn(),
    getActivityTemplate: vi.fn(),
    listActivityTemplates: vi.fn(),
    listActivityTemplateVersions: vi.fn(),
    listGifts: vi.fn(),
    listResourceGroups: vi.fn(),
    reload: vi.fn(),
    setActivityTemplateStatus: vi.fn(),
    showToast: vi.fn(),
    updateActivityTemplate: vi.fn(),
    validateActivityTemplate: vi.fn(),
}));

vi.mock("@/app/app-scope/AppScopeProvider.jsx", () => ({
    useAppScope: () => ({ appCode: mocks.appCode }),
}));

vi.mock("@/features/activity-templates/api", () => ({
    cloneActivityTemplate: mocks.cloneActivityTemplate,
    createActivityTemplate: mocks.createActivityTemplate,
    deleteActivityTemplate: mocks.deleteActivityTemplate,
    getActivityTemplate: mocks.getActivityTemplate,
    listActivityTemplates: mocks.listActivityTemplates,
    listActivityTemplateVersions: mocks.listActivityTemplateVersions,
    setActivityTemplateStatus: mocks.setActivityTemplateStatus,
    updateActivityTemplate: mocks.updateActivityTemplate,
    validateActivityTemplate: mocks.validateActivityTemplate,
}));

vi.mock("@/features/activity-templates/permissions.js", () => ({
    useActivityTemplateAbilities: () => mocks.abilities,
}));

vi.mock("@/features/resources/api", () => ({
    listGifts: mocks.listGifts,
    listResourceGroups: mocks.listResourceGroups,
}));

vi.mock("@/shared/hooks/useAdminQuery.js", () => ({
    useAdminQuery: () => ({
        data: { items: [], page: 1, pageSize: 20, total: 0 },
        error: "",
        loading: false,
        reload: mocks.reload,
    }),
}));

vi.mock("@/shared/hooks/useRegionOptions.js", () => ({
    useRegionOptions: () => ({ loadingRegions: false, regionOptions: [] }),
}));

vi.mock("@/shared/ui/ConfirmProvider.jsx", () => ({
    useConfirm: () => mocks.confirm,
}));

vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
    useToast: () => ({ showToast: mocks.showToast }),
}));

import { loadActiveReferenceOptions, nextCopyCode, useActivityTemplatesPage } from "./useActivityTemplatesPage.js";

beforeEach(() => {
    mocks.appCode = "lalu";
    mocks.confirm.mockResolvedValue(true);
    mocks.reload.mockResolvedValue(undefined);
    mocks.validateActivityTemplate.mockResolvedValue({ issues: [], valid: true });
});

afterEach(() => {
    mocks.abilities.canUpdate = true;
    vi.clearAllMocks();
});

test("keeps the saved identity and revision when the second publish command fails", async () => {
    const saved = savedTemplateFixture();
    mocks.createActivityTemplate.mockResolvedValue(saved);
    mocks.setActivityTemplateStatus.mockRejectedValue(new Error("activity period overlaps another template"));
    const { result } = renderHook(() => useActivityTemplatesPage());

    act(() => result.current.openCreate());
    act(() => result.current.setForm(completePublishForm()));
    await act(async () => {
        await result.current.saveAndPublish();
    });

    expect(mocks.createActivityTemplate).toHaveBeenCalledTimes(1);
    expect(mocks.setActivityTemplateStatus).toHaveBeenCalledWith("template-101", "published", 11);
    expect(result.current.editorMode).toBe("edit");
    expect(result.current.form.templateId).toBe("template-101");
    expect(result.current.form.revision).toBe(11);
    expect(result.current.editorDirty).toBe(false);
    expect(result.current.validationIssues[0].message).toContain("草稿已保存，但发布失败");
    expect(mocks.showToast).toHaveBeenCalledWith("草稿已保存，但发布失败", "error");
});

test("runs owner validation when a creator can publish without update permission", async () => {
    mocks.abilities.canUpdate = false;
    const saved = savedTemplateFixture();
    mocks.createActivityTemplate.mockResolvedValue(saved);
    mocks.setActivityTemplateStatus.mockResolvedValue({ ...saved, status: "published" });
    const { result } = renderHook(() => useActivityTemplatesPage());

    act(() => result.current.openCreate());
    act(() => result.current.setForm(completePublishForm()));
    await act(async () => {
        await result.current.saveAndPublish();
    });

    expect(mocks.validateActivityTemplate).toHaveBeenCalledTimes(1);
    expect(mocks.createActivityTemplate).toHaveBeenCalledTimes(1);
    expect(mocks.setActivityTemplateStatus).toHaveBeenCalledTimes(1);
});

test("asks before discarding a dirty editor and keeps it open when cancelled", async () => {
    const { result } = renderHook(() => useActivityTemplatesPage());
    act(() => result.current.openCreate());
    act(() => result.current.setForm((current) => ({ ...current, name: "Changed" })));
    expect(result.current.editorDirty).toBe(true);

    mocks.confirm.mockResolvedValueOnce(false);
    await act(async () => result.current.closeEditor());
    expect(result.current.editorMode).toBe("create");

    mocks.confirm.mockResolvedValueOnce(true);
    await act(async () => result.current.closeEditor());
    expect(result.current.editorMode).toBe("");
    expect(result.current.editorDirty).toBe(false);
});

test("asks before reloading a revision conflict over dirty local changes", async () => {
    const template = savedTemplateFixture();
    mocks.getActivityTemplate.mockResolvedValue(template);
    const { result } = renderHook(() => useActivityTemplatesPage());

    act(() => result.current.openEdit(template));
    await waitFor(() => expect(result.current.form.templateId).toBe("template-101"));
    act(() => result.current.setForm((current) => ({ ...current, name: "Local merge" })));
    expect(result.current.editorDirty).toBe(true);

    mocks.confirm.mockResolvedValueOnce(false);
    await act(async () => result.current.reloadEditor());
    expect(mocks.getActivityTemplate).toHaveBeenCalledTimes(1);
    expect(result.current.form.name).toBe("Local merge");

    mocks.confirm.mockResolvedValueOnce(true);
    await act(async () => result.current.reloadEditor());
    expect(mocks.getActivityTemplate).toHaveBeenCalledTimes(2);
    expect(result.current.form.name).toBe(template.name);
    expect(result.current.editorDirty).toBe(false);
});

test("clears every overlay and stale form when App scope changes", async () => {
    const { result, rerender } = renderHook(() => useActivityTemplatesPage());
    act(() => result.current.openCreate());
    act(() => result.current.setForm((current) => ({ ...current, name: "Old App draft" })));
    expect(result.current.editorMode).toBe("create");

    mocks.appCode = "huwaa";
    rerender();

    await waitFor(() => expect(result.current.editorMode).toBe(""));
    expect(result.current.form.appCode).toBe("huwaa");
    expect(result.current.form.name).toBe("");
    expect(result.current.cloneSource).toBeNull();
    expect(result.current.versionSource).toBeNull();
});

test("copy code uses second-level time and entropy", () => {
    const first = nextCopyCode("gift_challenge", Date.UTC(2026, 6, 14, 10, 20, 30), 0.1);
    const nextSecond = nextCopyCode("gift_challenge", Date.UTC(2026, 6, 14, 10, 20, 31), 0.1);
    const nextEntropy = nextCopyCode("gift_challenge", Date.UTC(2026, 6, 14, 10, 20, 30), 0.2);

    expect(first).not.toBe(nextSecond);
    expect(first).not.toBe(nextEntropy);
    expect(first.length).toBeLessThanOrEqual(64);
});

test("loads active gift and resource-group options in owner-service page-size chunks", async () => {
    const fetchPage = vi
        .fn()
        .mockResolvedValueOnce({ items: Array.from({ length: 100 }, (_, index) => ({ id: index + 1 })), total: 150 })
        .mockResolvedValueOnce({ items: Array.from({ length: 50 }, (_, index) => ({ id: index + 101 })), total: 150 });

    const result = await loadActiveReferenceOptions(fetchPage);

    expect(fetchPage).toHaveBeenNthCalledWith(1, { page: 1, page_size: 100, status: "active" });
    expect(fetchPage).toHaveBeenNthCalledWith(2, { page: 2, page_size: 100, status: "active" });
    expect(fetchPage).toHaveBeenCalledTimes(2);
    expect(result).toMatchObject({ page: 1, pageSize: 100, total: 150 });
    expect(result.items).toHaveLength(150);
});

function completePublishForm() {
    const form = createEmptyActivityTemplateForm("lalu");
    form.allRegions = true;
    form.name = "Gift Challenge";
    form.templateCode = "gift_challenge_2026";
    form.timeRange = { endMs: 1_800_086_400_000, startMs: 1_800_000_000_000 };
    form.titleEn = "Gift Challenge";
    form.rulesEn = "Rules";
    form.gifts = [
        {
            coinPrice: "1999",
            giftId: "gift-1",
            iconUrl: "https://cdn.example.com/gift.webp",
            name: "Gift",
            priceVersion: "v1",
        },
    ];
    form.tasks[0].rewardResourceGroupId = "71";
    form.dailyRankRewards.forEach((reward, index) => {
        reward.resourceGroupId = String(80 + index);
    });
    form.totalRankRewards.forEach((reward, index) => {
        reward.resourceGroupId = String(90 + index);
    });
    return form;
}

function savedTemplateFixture() {
    const form = completePublishForm();
    return {
        activityType: "gift_challenge",
        allRegions: true,
        appCode: "lalu",
        assets: [],
        dailyLeaderboardSize: 100,
        displayConfigJson: form.displayConfigJson,
        endMs: Number(form.timeRange.endMs),
        gifts: form.gifts.map((gift, index) => ({ ...gift, coinPrice: Number(gift.coinPrice), sortOrder: index + 1 })),
        lifecycleStatus: "draft",
        locales: [
            { locale: "en", rules: form.rulesEn, title: form.titleEn },
            { locale: "ar", rules: "", title: "" },
        ],
        name: form.name,
        publishedVersion: 0,
        rankRewards: [
            ...form.dailyRankRewards.map((reward, index) => ({
                boardType: "daily",
                rankFrom: Number(reward.rankFrom),
                rankTo: Number(reward.rankTo),
                resourceGroupId: Number(reward.resourceGroupId),
                sortOrder: index + 1,
            })),
            ...form.totalRankRewards.map((reward, index) => ({
                boardType: "total",
                rankFrom: Number(reward.rankFrom),
                rankTo: Number(reward.rankTo),
                resourceGroupId: Number(reward.resourceGroupId),
                sortOrder: index + 1,
            })),
        ],
        regionIds: [],
        revision: 11,
        startMs: Number(form.timeRange.startMs),
        status: "draft",
        tasks: form.tasks.map((task, index) => ({
            rewardResourceGroupId: Number(task.rewardResourceGroupId),
            sortOrder: index + 1,
            targetValue: Number(task.targetValue),
            taskKey: task.taskKey,
            taskType: task.taskType,
        })),
        templateCode: form.templateCode,
        templateId: "template-101",
        totalLeaderboardSize: 100,
    };
}
