import { act, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { DatabiApp } from "./DatabiApp.jsx";
import {
  fetchFilterOptions,
  fetchSelfGameStatisticsOverview,
  fetchSocialBiFunnel,
  fetchSocialBiKpi,
  fetchSocialBiMaster,
  fetchSocialBiOverview,
  fetchSocialBiRequirements,
  fetchStatisticsOverview
} from "./api.js";

vi.mock("./api.js", () => ({
  fetchFilterOptions: vi.fn(),
  fetchSocialBiFilterOptions: vi.fn(),
  fetchSocialBiFunnel: vi.fn(),
  fetchSocialBiKpi: vi.fn(),
  fetchSocialBiMaster: vi.fn(),
  fetchSocialBiOverview: vi.fn(),
  fetchSocialBiRequirements: vi.fn(),
  fetchSelfGameStatisticsOverview: vi.fn(),
  fetchStatisticsOverview: vi.fn(),
  getCurrentAppCode: vi.fn(() => "lalu"),
  getDefaultAppOptions: vi.fn(() => [
    { code: "", label: "全部", value: "" },
    { code: "lalu", label: "Lalu", value: "lalu" },
    { code: "aslan", label: "Aslan", value: "aslan" },
    { code: "yumi", label: "Yumi", value: "yumi" }
  ]),
  setCurrentAppCode: vi.fn((value) => String(value || "lalu").toLowerCase())
}));

vi.mock("./charts/EChart.jsx", () => ({
  EChart: ({ option }) => (
    <div
      data-series={JSON.stringify((option?.series || []).map(({ data, name }) => ({ data, name })))}
      data-testid="databi-chart"
    />
  )
}));

beforeEach(() => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date("2026-06-06T08:00:00Z"));
  fetchFilterOptions.mockResolvedValue({
    appOptions: [
      { code: "", label: "全部", value: "" },
      { code: "lalu", label: "Lalu", value: "lalu" },
      { code: "aslan", label: "Aslan", value: "aslan" },
      { code: "yumi", label: "Yumi", value: "yumi" }
    ],
    countryOptions: [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }],
    regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }]
  });
  fetchSelfGameStatisticsOverview.mockResolvedValue({});
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [],
    operators: [],
    permissions: {},
    regions: []
  });
  fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
  fetchSocialBiFunnel.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
  fetchSocialBiKpi.mockResolvedValue({ items: [], period_month: "2026-06", summary: {} });
  fetchSocialBiRequirements.mockResolvedValue({ columns: [], daily_rows: [], section: "new_users", total: null });
});

afterEach(() => {
  window.history.pushState(null, "", "/");
  vi.useRealTimers();
  vi.clearAllMocks();
  vi.unstubAllGlobals();
});

test("keeps current data visible during scheduled refresh", async () => {
  const overview = { active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 };
  fetchStatisticsOverview
    .mockResolvedValueOnce(overview)
    .mockResolvedValueOnce(overview)
    .mockImplementationOnce(() => new Promise(() => {}));

  render(<DatabiApp />);

  await flushEffects();
  expect(screen.getAllByText("$1").length).toBeGreaterThan(0);
  expect(screen.getByLabelText("数据报表")).toBeTruthy();
  expect(document.querySelector(".report-metric-card.is-loading")).toBeNull();

  await act(async () => {
    vi.advanceTimersByTime(60_000);
  });

  expect(fetchStatisticsOverview).toHaveBeenCalledTimes(3);
  expect(screen.getAllByText("$1").length).toBeGreaterThan(0);
  expect(document.querySelector(".report-metric-card.is-loading")).toBeNull();
});

test("queries China natural day statistics when display timezone is Beijing", async () => {
  fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 });

  render(<DatabiApp />);

  await flushEffects();

  expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({
    endMs: Date.UTC(2026, 5, 6, 15, 59, 59),
    seriesEndMs: Date.UTC(2026, 5, 6, 15, 59, 59),
    seriesStartMs: Date.UTC(2026, 4, 30, 16, 0, 0),
    startMs: Date.UTC(2026, 5, 5, 16, 0, 0),
    statTz: "Asia/Shanghai"
  }));
});

test("renders legacy app metric columns in report view", async () => {
  fetchStatisticsOverview.mockResolvedValue({
    coin_seller_stock_coin: 400_000,
    coin_seller_transfer_coin: 160_000,
    salary_transfer_coin: 88_000,
    country_breakdown: [
      {
        coin_seller_stock_coin: 400_000,
        coin_seller_transfer_coin: 160_000,
        country: "巴西",
        country_id: 86,
        salary_transfer_coin: 88_000
      }
    ],
    updated_at_ms: 1
  });

  render(<DatabiApp />);

  await flushEffects();

  expect(screen.getByRole("columnheader", { name: "币商充值金币" })).toBeTruthy();
  expect(screen.getByRole("columnheader", { name: "币商出货金币" })).toBeTruthy();
  expect(screen.getByRole("columnheader", { name: "工资兑换金币" })).toBeTruthy();
  expect(screen.getAllByText("400,000").length).toBeGreaterThan(0);
  expect(screen.getAllByText("160,000").length).toBeGreaterThan(0);
  expect(screen.getAllByText("88,000").length).toBeGreaterThan(0);
});

test("switches report sidebar sections and stops data report polling on empty sections", async () => {
  fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 });

  render(<DatabiApp />);

  await flushEffects();

  const reportTypeTabs = screen.getByRole("tablist", { name: "报表类型" });
  expect(within(reportTypeTabs).getByRole("tab", { name: "数据报表" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy();

  await act(async () => {
    within(reportTypeTabs).getByRole("tab", { name: "财务报表" }).click();
  });

  expect(within(reportTypeTabs).getByRole("tab", { name: "财务报表" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByRole("region", { name: "财务报表" })).toBeTruthy();
  expect(screen.queryByRole("region", { name: "数据报表" })).toBeNull();
  const requestCountAfterFinanceSwitch = fetchStatisticsOverview.mock.calls.length;

  await act(async () => {
    vi.advanceTimersByTime(60_000);
  });

  expect(fetchStatisticsOverview).toHaveBeenCalledTimes(requestCountAfterFinanceSwitch);

  await act(async () => {
    within(reportTypeTabs).getByRole("tab", { name: "运营报表" }).click();
  });

  expect(within(reportTypeTabs).getByRole("tab", { name: "运营报表" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByRole("region", { name: "运营报表" })).toBeTruthy();

  await act(async () => {
    within(reportTypeTabs).getByRole("tab", { name: "数据报表" }).click();
  });
  await flushEffects();

  expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy();
  expect(fetchStatisticsOverview.mock.calls.length).toBeGreaterThan(requestCountAfterFinanceSwitch);
});

test("loads self game statistics from the big screen switch", async () => {
  fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 });
  fetchSelfGameStatisticsOverview.mockResolvedValue({
    events: [{ event_name: "page_open", event_count: 8, user_count: 5 }],
    match_totals: { created_matches: 3, matched_matches: 2, settled_matches: 1 },
    retention: { cohort_users: 5, day1_rate: 0.2, day7_rate: 0.1 }
  });

  render(<DatabiApp />);

  await flushEffects();
  await act(async () => {
    screen.getByRole("button", { name: "自研游戏" }).click();
  });
  await flushEffects();

  expect(fetchSelfGameStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", gameId: "all", statTz: "Asia/Shanghai" }));
  expect(screen.getByText("Dice / Rock H5 聚合统计")).toBeTruthy();
  expect(screen.getByText("创建局")).toBeTruthy();
});

test("renders real-room robot gift cards in a separate row", async () => {
  fetchStatisticsOverview.mockResolvedValue({
    real_room_robot_avg_room_gift_coin: 300,
    real_room_robot_gift_room_count: 2,
    real_room_robot_lucky_gift_coin: 200,
    real_room_robot_normal_gift_coin: 100,
    real_room_robot_super_gift_coin: 300,
    updated_at_ms: 1
  });

  render(<DatabiApp />);

  await flushEffects();
  await act(async () => {
    screen.getByRole("tab", { name: "大屏 BI" }).click();
  });

  const businessSection = screen.getByLabelText("业务指标");
  const robotGiftSection = screen.getByLabelText("真人房机器人送礼指标");
  expect(within(businessSection).queryByText("真人房机器人普通礼物")).toBeNull();
  expect(within(robotGiftSection).getByText("真人房机器人普通礼物")).toBeTruthy();
  expect(within(robotGiftSection).getByText("真人房机器人幸运礼物")).toBeTruthy();
  expect(within(robotGiftSection).getByText("真人房机器人超级幸运礼物")).toBeTruthy();
  expect(within(robotGiftSection).getByText("真人房机器人房均礼物")).toBeTruthy();
});

test("routes databi social page to Social BI and loads real overview rows", async () => {
  window.history.pushState(null, "", "/databi/social/");
  const laluTotal = {
    active_users: 20,
    arpu_usd_minor: 410,
    arppu_usd_minor: 615,
    avg_mic_online_ms: 120_000,
    consumed_coin: 900_000,
    consume_output_delta: 450_000,
    consume_output_ratio: 0.5,
    d1_retention_rate: 0.25,
    game_profit_rate: 0.17,
    game_turnover: 300_000,
    gift_coin_spent: 123_456,
    google_recharge_usd_minor: 700,
    mifapay_recharge_usd_minor: 800,
    new_users: 3,
    output_coin: 450_000,
    paid_conversion_rate: 0.2,
    paid_users: 4,
    recharge_usd_minor: 12_300,
    recharge_users: 4
  };
  const aslanRegionRow = {
    active_users: 10,
    new_users: 1,
    paid_users: 2,
    recharge_usd_minor: 5000,
    recharge_users: 2,
    region_code: "MENA",
    region_id: 11,
    region_name: "中东大区"
  };
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      { app_code: "lalu", app_name: "Lalu", kind: "hyapp", logo_url: "https://media.example.com/lalu.png" },
      { app_code: "aslan", app_name: "Aslan", kind: "legacy", logo_url: "https://media.example.com/aslan.png" }
    ],
    operators: [
      { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 }
    ],
    permissions: {},
    regions: [{ app_code: "aslan", countries: ["SA"], region_code: "MENA", region_id: 11, region_name: "中东大区" }]
  });
  fetchSocialBiOverview.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        country_breakdown: [],
        daily_country_breakdown: [],
        daily_region_breakdown: [],
        daily_series: [],
        kind: "hyapp",
        region_breakdown: [],
        total: laluTotal
      },
      {
        app_code: "aslan",
        app_name: "Aslan",
        country_breakdown: [],
        daily_country_breakdown: [],
        daily_region_breakdown: [],
        daily_series: [],
        kind: "legacy",
        region_breakdown: [aslanRegionRow],
        total: { active_users: 10, new_users: 1, paid_users: 2, recharge_usd_minor: 5000, recharge_users: 2 }
      }
    ]
  });
  fetchSocialBiKpi.mockResolvedValue({
    items: [
      {
        app_code: "aslan",
        app_name: "Aslan",
        month_recharge_usd_minor: 90_000,
        operator_account: "omar",
        operator_name: "Omar",
        operator_user_id: 9,
        range_new_users: 1,
        range_recharge_usd_minor: 5000,
        region_id: 11,
        region_name: "中东大区",
        team: "中东运营部",
        team_id: 2
      }
    ],
    period_month: "2026-06",
    summary: { month_recharge_usd_minor: 90_000, operator_count: 1, range_recharge_usd_minor: 5000 }
  });

  render(<DatabiApp />);

  await flushEffects();

  // v2 Shell：主视图容器 + 侧栏视图 tab，默认选中“经营概览”。
  expect(screen.getByLabelText("Social BI 数据中心")).toBeTruthy();
  const viewTabs = screen.getByRole("tablist", { name: "数据视图" });
  ["经营概览", "地区洞察", "运营中心", "留存质量", "数据明细", "埋点漏斗"].forEach((name) => {
    expect(within(viewTabs).getByRole("tab", { name })).toBeTruthy();
  });
  expect(within(viewTabs).getByRole("tab", { name: "经营概览" })).toHaveAttribute("aria-selected", "true");

  expect(fetchSocialBiMaster).toHaveBeenCalledTimes(1);
  expect(fetchSocialBiOverview).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
  expect(fetchSocialBiFunnel).not.toHaveBeenCalled();
  expect(fetchSocialBiKpi).not.toHaveBeenCalled();

  // 经营概览：App 名、Hero 汇总充值（12,300 + 5,000 USD 分 = $173）、
  // App 对比里 Lalu 的单 App 充值（$123）、区域 Top5 里的中东大区。
  expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0);
  expect(document.querySelector('img[src="https://media.example.com/lalu.png"]')).toBeTruthy();
  expect(document.querySelector('img[src="https://media.example.com/aslan.png"]')).toBeTruthy();
  expect(screen.getAllByText("充值 ($)").length).toBeGreaterThan(0);
  expect(screen.getByText("付费用户")).toBeTruthy();
  expect(screen.getAllByText("$173").length).toBeGreaterThan(0);
  expect(screen.getAllByText("$123").length).toBeGreaterThan(0);
  expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0);

  // 侧栏切换：地区洞察 —— 区域下钻表出现，Aslan 中东大区行充值 $50。
  await act(async () => {
    within(viewTabs).getByRole("tab", { name: "地区洞察" }).click();
  });
  expect(within(viewTabs).getByRole("tab", { name: "地区洞察" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByText("区域下钻")).toBeTruthy();
  expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0);
  expect(screen.getAllByText("$50").length).toBeGreaterThan(0);

  // 侧栏切换：运营中心 —— 运营人员 Omar 上榜，只展示负责范围内的充值数据。
  await act(async () => {
    within(viewTabs).getByRole("tab", { name: "运营中心" }).click();
  });
  await flushEffects();
  expect(within(viewTabs).getByRole("tab", { name: "运营中心" })).toHaveAttribute("aria-selected", "true");
  expect(fetchSocialBiKpi).toHaveBeenCalledTimes(1);
  expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
  expect(screen.getAllByText("Omar").length).toBeGreaterThan(0);
  expect(screen.getAllByText("$900").length).toBeGreaterThan(0);
});

test("filters operation KPI by person and shows totals with App and region amounts", async () => {
  window.history.pushState(null, "", "/databi/social/?view=team&operator=9");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      { app_code: "aslan", app_name: "Aslan", kind: "legacy" },
      { app_code: "yumi", app_name: "Yumi", kind: "legacy" }
    ],
    operators: [
      {
        account: "omar",
        name: "Omar",
        scopes: [{ app_code: "aslan", region_id: 11 }, { app_code: "yumi", region_id: 20 }],
        team: "中东运营部",
        team_id: 2,
        user_id: 9
      }
    ],
    permissions: {},
    regions: [
      { app_code: "aslan", region_id: 11, region_name: "中东大区" },
      { app_code: "yumi", region_id: 20, region_name: "南亚大区" }
    ]
  });
  fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
  fetchSocialBiKpi.mockResolvedValue({
    items: [
      {
        app_code: "aslan",
        app_name: "Aslan",
        month_recharge_usd_minor: 90_000,
        operator_account: "omar",
        operator_name: "Omar",
        operator_user_id: 9,
        range_recharge_usd_minor: 5000,
        region_id: 11,
        region_name: "中东大区",
        team: "中东运营部"
      },
      {
        app_code: "yumi",
        app_name: "Yumi",
        month_recharge_usd_minor: 50_000,
        operator_account: "omar",
        operator_name: "Omar",
        operator_user_id: 9,
        range_recharge_usd_minor: 3000,
        region_id: 20,
        region_name: "南亚大区",
        team: "中东运营部"
      }
    ],
    operator_app_rows: [
      {
        app_code: "aslan",
        app_name: "Aslan",
        month_recharge_usd_minor: 90_000,
        operator_account: "omar",
        operator_name: "Omar",
        operator_user_id: 9,
        range_recharge_usd_minor: 5000,
        region_count: 1,
        region_names: "中东大区",
        team: "中东运营部"
      },
      {
        app_code: "yumi",
        app_name: "Yumi",
        month_recharge_usd_minor: 50_000,
        operator_account: "omar",
        operator_name: "Omar",
        operator_user_id: 9,
        range_recharge_usd_minor: 3000,
        region_count: 1,
        region_names: "南亚大区",
        team: "中东运营部"
      }
    ],
    period_month: "2026-06",
    summary: { month_recharge_usd_minor: 140_000, operator_count: 1, range_recharge_usd_minor: 8000 }
  });

  render(<DatabiApp />);
  await flushEffects();

  expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ operatorUserId: 9 }));
  expect(screen.getByRole("combobox", { name: "运营人员筛选" })).toHaveValue("Omar");
  expect(window.location.search).toContain("operator=9");
  const summary = screen.getByLabelText("充值金额汇总");
  expect(within(summary).getByText("Omar")).toBeTruthy();
  expect(within(summary).getByText("$80")).toBeTruthy();
  expect(within(summary).getByText("$1,400")).toBeTruthy();
  expect(screen.getByRole("cell", { name: "中东大区" })).toBeTruthy();
  expect(screen.getByRole("cell", { name: "南亚大区" })).toBeTruthy();

  await act(async () => {
    screen.getByRole("radio", { name: "按人员×App" }).click();
  });

  expect(screen.getByRole("cell", { name: "中东大区" })).toBeTruthy();
  expect(screen.getByRole("cell", { name: "南亚大区" })).toBeTruthy();
  expect(screen.getAllByText("$50").length).toBeGreaterThan(0);
  expect(screen.getAllByText("$30").length).toBeGreaterThan(0);
});

test("defaults retention quality to canonical active cohort and can switch to new-user cohort", async () => {
  window.history.pushState(null, "", "/databi/social/?view=retention&apps=lalu");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" }],
    operators: [],
    permissions: {},
    regions: []
  });
  fetchSocialBiOverview.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        country_breakdown: [],
        daily_region_breakdown: [],
        daily_series: [
          {
            active_users: 20,
            d1_retention_base_users: 4,
            d1_retention_rate: 0.25,
            d1_retention_users: 1,
            d30_retention_base_users: 2,
            d30_retention_rate: 0.5,
            d30_retention_users: 1,
            d7_retention_base_users: 3,
            d7_retention_rate: 0.3333,
            d7_retention_users: 1,
            new_users: 4,
            stat_day: "2026-06-05"
          }
        ],
        kind: "hyapp",
        region_breakdown: [],
        total: {
          active_users: 20,
          d1_retention_base_users: 4,
          d1_retention_rate: 0.25,
          d1_retention_users: 1,
          d30_retention_base_users: 2,
          d30_retention_rate: 0.5,
          d30_retention_users: 1,
          d7_retention_base_users: 3,
          d7_retention_rate: 0.3333,
          d7_retention_users: 1,
          new_users: 4
        }
      }
    ]
  });
  fetchSocialBiRequirements.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        kind: "hyapp",
        sections: [
          {
            daily_series: [
              {
                metrics: {
                  active_d1_active_base_users: 12,
                  active_d1_active_rate: 0.5,
                  active_d1_active_users: 6,
                  active_d30_active_base_users: 3,
                  active_d30_active_rate: 0.3333,
                  active_d30_active_users: 1,
                  active_d7_active_base_users: 6,
                  active_d7_active_rate: 0.5,
                  active_d7_active_users: 3
                },
                stat_day: "2026-06-04"
              },
              {
                metrics: {
                  active_d1_active_base_users: 8,
                  active_d1_active_rate: 0.5,
                  active_d1_active_users: 4,
                  active_d30_active_base_users: 2,
                  active_d30_active_rate: 0,
                  active_d30_active_users: 0,
                  active_d7_active_base_users: 4,
                  active_d7_active_rate: 0.25,
                  active_d7_active_users: 1
                },
                stat_day: "2026-06-05"
              },
              {
                metrics: {
                  active_d1_active_base_users: null,
                  active_d1_active_rate: null,
                  active_d1_active_users: null,
                  active_d30_active_base_users: null,
                  active_d30_active_rate: null,
                  active_d30_active_users: null,
                  active_d7_active_base_users: null,
                  active_d7_active_rate: null,
                  active_d7_active_users: null
                },
                stat_day: "2026-06-06"
              }
            ],
            key: "retention",
            total: {
              metrics: {
                active_d1_active_base_users: 20,
                active_d1_active_rate: 0.5,
                active_d1_active_users: 10,
                active_d30_active_base_users: 5,
                active_d30_active_rate: 0.2,
                active_d30_active_users: 1,
                active_d7_active_base_users: 10,
                active_d7_active_rate: 0.4,
                active_d7_active_users: 4
              }
            }
          }
        ]
      }
    ]
  });

  render(<DatabiApp />);
  await flushEffects();

  expect(fetchSocialBiRequirements).toHaveBeenCalledWith(expect.objectContaining({
    newUserType: "all",
    payerType: "all",
    section: "retention",
    userRole: "all"
  }));
  // Overview 的 access 结果落地不能改变 requirements callback 身份，否则同一轮 D30 查询会重复执行。
  expect(fetchSocialBiRequirements).toHaveBeenCalledTimes(1);
  const modeSwitch = screen.getByRole("radiogroup", { name: "留存口径" });
  expect(within(modeSwitch).getByRole("radio", { name: "整体次留" })).toHaveAttribute("aria-checked", "true");
  expect(screen.getByText("活跃基数与次日留存")).toBeTruthy();
  expect(screen.getByText("整体用户留存趋势")).toBeTruthy();
  expect(screen.getByRole("columnheader", { name: "活跃用户基数" })).toBeTruthy();
  expect(screen.getByRole("columnheader", { name: "次留人数" })).toBeTruthy();
  expect(screen.getAllByText("50.0%").length).toBeGreaterThan(0);
  expect(screen.getAllByText(/活跃基数 20 人/).length).toBeGreaterThan(0);
  expect(screen.getAllByText(/已接入 Social App 汇总/).length).toBeGreaterThan(0);
  const overallCharts = screen.getAllByTestId("databi-chart");
  const comboSeries = JSON.parse(overallCharts[0].dataset.series);
  const trendSeries = JSON.parse(overallCharts[1].dataset.series);
  expect(comboSeries.find((series) => series.name === "整体次日留存").data).toEqual([50, 50, null]);
  expect(trendSeries.find((series) => series.name === "整体次日留存").data).toEqual([50, 50, null]);

  await act(async () => {
    within(modeSwitch).getByRole("radio", { name: "新增次留" }).click();
  });

  expect(within(modeSwitch).getByRole("radio", { name: "新增次留" })).toHaveAttribute("aria-checked", "true");
  expect(screen.getByText("新增与次日留存")).toBeTruthy();
  expect(screen.getByText("新增用户留存趋势")).toBeTruthy();
  expect(screen.getByRole("columnheader", { name: "新增" })).toBeTruthy();
  expect(screen.getAllByText("25.0%").length).toBeGreaterThan(0);
  expect(screen.getAllByText(/注册基数 4 人/).length).toBeGreaterThan(0);
});

test("marks overall retention as a partial subset when one app fails", async () => {
  window.history.pushState(null, "", "/databi/social/?view=retention");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      { app_code: "lalu", app_name: "Lalu", kind: "hyapp" },
      { app_code: "huwaa", app_name: "Huwaa", kind: "hyapp" }
    ],
    operators: [],
    permissions: {},
    regions: []
  });
  fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
  fetchSocialBiRequirements.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        kind: "hyapp",
        sections: [{
          daily_series: [{
            active_d1_active_base_users: 10,
            active_d1_active_rate: 0.5,
            active_d1_active_users: 5,
            stat_day: "2026-06-05"
          }],
          key: "retention",
          total: {
            active_d1_active_base_users: 10,
            active_d1_active_rate: 0.5,
            active_d1_active_users: 5
          }
        }]
      },
      { app_code: "huwaa", app_name: "Huwaa", error: "statistics timeout", kind: "hyapp" }
    ]
  });

  render(<DatabiApp />);
  await flushEffects();

  expect(screen.getByRole("alert")).toHaveTextContent("部分 App 整体留存加载失败");
  expect(screen.getByRole("alert")).toHaveTextContent("Huwaa: statistics timeout");
  expect(screen.getAllByText(/成功返回的 Social App 子集/).length).toBeGreaterThan(0);
  expect(screen.queryByText(/已接入 Social App 汇总/)).toBeNull();
});

test("renders social BI data requirements table with section filters and CSV export", async () => {
  window.history.pushState(null, "", "/databi/social/?view=table&regions=lalu:9,lalu:11");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" }],
    operators: [],
    permissions: {},
    regions: []
  });
  fetchSocialBiOverview.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [{ app_code: "lalu", app_name: "Lalu", daily_series: [], kind: "hyapp", total: {} }]
  });
  fetchSocialBiRequirements.mockImplementation(async ({ section }) => {
    if (section === "revenue") {
      return {
        access: { all: true, scopes: [] },
        apps: [{
          app_code: "lalu",
          app_name: "Lalu",
          kind: "hyapp",
          sections: [{
            columns: [
              { key: "recharge_users", label: "充值用户", type: "count" },
              { key: "recharge_usd_minor", label: "充值金额", type: "money_minor" }
            ],
            daily_series: [{ metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "2026-06-05" }],
            key: section,
            total: { label: "汇总", metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "total" }
          }]
        }]
      };
    }
    return {
      access: { all: true, scopes: [] },
      apps: [{
        app_code: "lalu",
        app_name: "Lalu",
        kind: "hyapp",
        sections: [{
          columns: [
            { key: "app_download_users", label: "下载APP人数", type: "count" },
            { key: "registration_success_rate", label: "注册成功率", type: "ratio" }
          ],
          daily_series: [{ metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "2026-06-05" }],
          key: section,
          total: { label: "汇总", metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "total" }
        }]
      }]
    };
  });

  render(<DatabiApp />);
  await flushEffects();

  expect(fetchSocialBiOverview).toHaveBeenCalledWith(
    expect.objectContaining({ appCodes: ["lalu"], regionIds: [9, 11], statTz: "Asia/Shanghai" })
  );
  const viewTabs = screen.getByRole("tablist", { name: "数据视图" });
  expect(within(viewTabs).getByRole("tab", { name: "数据明细" })).toHaveAttribute("aria-selected", "true");

  await act(async () => {
    screen.getByRole("radio", { name: "数据需求" }).click();
  });
  await flushEffects();

  expect(fetchSocialBiRequirements).toHaveBeenCalledWith(expect.objectContaining({
    payerType: "all",
    regionIds: [9, 11],
    section: "revenue",
    userRole: "all"
  }));
  const requirementTabs = screen.getByRole("tablist", { name: "数据需求分组" });
  expect(within(requirementTabs).getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
    "充值",
    "新增",
    "日活",
    "礼物",
    "游戏",
    "幸运礼物V3",
    "留存"
  ]);
  expect(screen.getByRole("tab", { name: "充值" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByRole("radiogroup", { name: "用户身份" })).toBeTruthy();
  expect(screen.getByRole("radiogroup", { name: "付费身份" })).toBeTruthy();
  expect(screen.getByText("充值用户")).toBeTruthy();
  expect(screen.getByText("充值金额")).toBeTruthy();
  expect(screen.getAllByText("$123").length).toBeGreaterThan(0);

  await act(async () => {
    screen.getByRole("tab", { name: "新增" }).click();
  });
  await flushEffects();
  expect(fetchSocialBiRequirements).toHaveBeenLastCalledWith(expect.objectContaining({ section: "new_users" }));
  expect(screen.getByRole("tab", { name: "新增" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByText("下载APP人数")).toBeTruthy();
  expect(screen.getByText("注册成功率")).toBeTruthy();
  expect(screen.getAllByText("12").length).toBeGreaterThan(0);
  expect(screen.getAllByText("66.66%").length).toBeGreaterThan(0);
  expect(screen.queryByRole("radiogroup", { name: "用户身份" })).toBeNull();
  expect(screen.queryByRole("radiogroup", { name: "付费身份" })).toBeNull();

  await act(async () => {
    screen.getByRole("tab", { name: "充值" }).click();
  });
  await flushEffects();
  expect(screen.getByRole("radiogroup", { name: "用户身份" })).toBeTruthy();
  expect(screen.getByRole("radiogroup", { name: "付费身份" })).toBeTruthy();

  await act(async () => {
    screen.getByRole("radio", { name: "主播" }).click();
  });
  await flushEffects();
  await act(async () => {
    screen.getByRole("radio", { name: "已付费" }).click();
  });
  await flushEffects();

  expect(fetchSocialBiRequirements).toHaveBeenLastCalledWith(expect.objectContaining({
    payerType: "paid",
    section: "revenue",
    userRole: "host"
  }));
  expect(screen.getByText("充值用户")).toBeTruthy();
  expect(screen.getByText("充值金额")).toBeTruthy();
  expect(screen.getAllByText("$123").length).toBeGreaterThan(0);

  const urlMock = { createObjectURL: vi.fn(() => "blob:social-bi-requirements"), revokeObjectURL: vi.fn() };
  const anchorClick = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
  vi.stubGlobal("URL", urlMock);
  await act(async () => {
    screen.getByRole("button", { name: "导出 CSV" }).click();
  });
  expect(urlMock.createObjectURL).toHaveBeenCalledTimes(1);
  expect(anchorClick).toHaveBeenCalledTimes(1);
  anchorClick.mockRestore();
});

test("groups social BI wide-table daily rows by app instead of interleaving by date", async () => {
  window.history.pushState(null, "", "/databi/social/?view=table");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      // 带 logo：AppIdentity 渲染图片而不是字母头像，单元格 textContent 恰好等于 App 名。
      { app_code: "lalu", app_name: "Lalu", kind: "hyapp", logo_url: "https://media.example.com/lalu.png" },
      { app_code: "aslan", app_name: "Aslan", kind: "legacy", logo_url: "https://media.example.com/aslan.png" }
    ],
    operators: [],
    permissions: {},
    regions: []
  });
  const dailyRow = (statDay, rechargeUsdMinor) => ({ recharge_usd_minor: rechargeUsdMinor, recharge_users: 1, stat_day: statDay });
  fetchSocialBiOverview.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        country_breakdown: [],
        daily_region_breakdown: [],
        daily_series: [dailyRow("2026-06-04", 100), dailyRow("2026-06-05", 200)],
        kind: "hyapp",
        region_breakdown: [],
        total: { recharge_usd_minor: 300, recharge_users: 2 }
      },
      {
        // Aslan 各日充值都高于 Lalu：旧的“按日倒序 + 日内充值倒序”排序会把两个 App 一天一行穿插，
        // 新排序必须仍按 master 顺序把 Lalu 块排在前面。
        app_code: "aslan",
        app_name: "Aslan",
        country_breakdown: [],
        daily_region_breakdown: [],
        daily_series: [dailyRow("2026-06-04", 900), dailyRow("2026-06-05", 800)],
        kind: "legacy",
        region_breakdown: [],
        total: { recharge_usd_minor: 1700, recharge_users: 2 }
      }
    ]
  });

  render(<DatabiApp />);
  await flushEffects();

  const table = document.querySelector(".sbi-dt-table");
  expect(table).toBeTruthy();
  const headerCells = [...table.querySelectorAll("thead tr:first-child th")].map((cell) => cell.textContent.trim());
  expect(headerCells.slice(0, 2)).toEqual(["App", "日期"]);

  // 行序：汇总/平均值之后，每个 App 的日行连成块（块内日期倒序），不按日期把 App 穿插。
  const bodyRows = [...table.querySelectorAll("tbody tr")];
  const dimCells = bodyRows.map((row) => {
    const cells = row.querySelectorAll("td");
    return [cells[0].textContent.trim(), cells[1].textContent.trim()];
  });
  expect(dimCells).toEqual([
    ["全部", "汇总"],
    ["平均值", "--"],
    ["Lalu", "2026-06-05"],
    ["Lalu", "2026-06-04"],
    ["Aslan", "2026-06-05"],
    ["Aslan", "2026-06-04"]
  ]);
  // 分块分隔线只标在后一个块的首行。
  expect(bodyRows[4].classList.contains("sbi-dt-group-start")).toBe(true);
  expect(bodyRows[3].classList.contains("sbi-dt-group-start")).toBe(false);
});

test("renders social BI funnel view and limits funnel apps to supported apps", async () => {
  window.history.pushState(null, "", "/databi/social/?view=funnel");
  fetchSocialBiMaster.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      { app_code: "lalu", app_name: "Lalu", kind: "hyapp" },
      { app_code: "huwaa", app_name: "Huwaa", kind: "hyapp" },
      { app_code: "fami", app_name: "Fami", kind: "hyapp" },
      { app_code: "aslan", app_name: "Aslan", kind: "legacy" },
      { app_code: "yumi", app_name: "Yumi", kind: "legacy" }
    ],
    operators: [],
    permissions: {},
    regions: []
  });
  fetchSocialBiFunnel.mockResolvedValue({
    access: { all: true, scopes: [] },
    apps: [
      {
        app_code: "lalu",
        app_name: "Lalu",
        cohorts: [
          {
            base_users: 1,
            d1_retention_rate: 1,
            d1_retention_users: 1,
            dimension: "country",
            label: "US",
            steps: [
              { event_name: "login_success", user_count: 1 },
              { event_name: "profile_complete", user_count: 1 },
              { event_name: "room_join_success", user_count: 1 },
              { event_name: "stay_3m", user_count: 1 },
              { event_name: "send_message", user_count: 1 }
            ],
            value: "US"
          }
        ],
        kind: "hyapp",
        steps: [
          { event_name: "login_start", event_count: 2, label: "登录开始", user_count: 2 },
          { event_name: "login_success", event_count: 1, label: "登录成功", user_count: 1 },
          { event_name: "room_join_success", event_count: 1, label: "进房成功", user_count: 1 },
          { event_name: "room_join_fail", event_count: 1, is_failure: true, label: "进房失败", user_count: 1 },
          { event_name: "stay_3m", event_count: 1, label: "停留 3 分钟", user_count: 1 },
          { event_name: "send_message", event_count: 1, label: "发消息", user_count: 1 }
        ],
        totals: {
          d1_retention_base_users: 1,
          d1_retention_rate: 1,
          d1_retention_users: 1,
          login_start_users: 2,
          login_success_users: 1,
          room_join_fail_users: 1,
          room_join_success_users: 1
        }
      }
    ]
  });

  render(<DatabiApp />);

  await flushEffects();

  expect(screen.getByRole("tab", { name: "埋点漏斗" })).toHaveAttribute("aria-selected", "true");
  expect(screen.getByRole("button", { name: "全部漏斗 App" })).toBeTruthy();
  expect(screen.getByRole("button", { name: /Lalu/ })).toBeTruthy();
  expect(screen.getByRole("button", { name: /Huwaa/ })).toBeTruthy();
  expect(screen.getByRole("button", { name: /Fami/ })).toBeTruthy();
  expect(screen.queryByRole("button", { name: /Aslan/ })).toBeNull();
  expect(screen.queryByRole("button", { name: /Yumi/ })).toBeNull();
  expect(fetchSocialBiFunnel).toHaveBeenCalledWith(expect.objectContaining({ appCodes: ["lalu", "huwaa", "fami"] }));

  expect(screen.getByText("主路径转化")).toBeTruthy();
  expect(screen.getByText("事件明细")).toBeTruthy();
  expect(screen.getByText("D1 Cohort")).toBeTruthy();
  expect(screen.getByText("login_start")).toBeTruthy();
  expect(screen.getByText("room_join_fail")).toBeTruthy();
  expect(screen.getByText("US")).toBeTruthy();
  expect(screen.getAllByText("100.0%").length).toBeGreaterThan(0);
  expect(screen.getByTestId("databi-chart")).toBeTruthy();
});

async function flushEffects() {
  await act(async () => {
    for (let index = 0; index < 10; index += 1) {
      await Promise.resolve();
    }
  });
}
