const { test, expect } = require("@playwright/test");
const fs = require("fs");
const path = require("path");

const SHOT_DIR = path.resolve(__dirname, "../../../verify-shots");
fs.mkdirSync(SHOT_DIR, { recursive: true });

function loginPayload(user) {
  return {
    ok: true,
    authenticated: true,
    csrf_token: "portal-csrf",
    user,
  };
}

async function mockLoginPortal(page, options = {}) {
  const accounts = options.accounts || [];
  let currentUserId = options.currentUserId ?? null;
  await page.route("**/api/**", async (route) => {
    const url = new URL(route.request().url());
    const method = route.request().method();
    if (url.pathname === "/api/auth/accounts") {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ ok: true, accounts, current_user_id: currentUserId }),
      });
      return;
    }
    if (url.pathname === "/api/auth/switch" && method === "POST") {
      const body = route.request().postDataJSON() || {};
      const account = accounts.find((item) => Number(item.user_id) === Number(body.user_id));
      if (!account || options.switchFails) {
        await route.fulfill({
          status: 401,
          contentType: "application/json",
          body: JSON.stringify({ error: "该账号需重新验证" }),
        });
        return;
      }
      currentUserId = account.user_id;
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(loginPayload(account)),
      });
      return;
    }
    if (url.pathname === "/api/auth/forget" && method === "POST") {
      const body = route.request().postDataJSON() || {};
      const index = accounts.findIndex((item) => Number(item.user_id) === Number(body.user_id));
      if (index >= 0) accounts.splice(index, 1);
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ ok: true }),
      });
      return;
    }
    if ((url.pathname === "/api/auth/login" || url.pathname === "/api/auth/register") && method === "POST") {
      if (options.loginDelay) await new Promise((resolve) => setTimeout(resolve, options.loginDelay));
      if (options.loginFails) {
        await route.fulfill({
          status: 401,
          contentType: "application/json",
          body: JSON.stringify({ error: "账号名或密码不正确，请重新输入。" }),
        });
        return;
      }
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(loginPayload({
          id: 9,
          username: "new_user",
          role: "user",
          membership: { active: false, subscribed: false, is_admin: false },
        })),
      });
      return;
    }
    if (url.pathname === "/api/auth/me") {
      const current = accounts.find((item) => Number(item.user_id) === Number(currentUserId)) || null;
      const authenticated = Boolean(current) && !options.sessionExpired;
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          ok: true,
          authenticated,
          csrf_token: "portal-csrf",
          user: authenticated ? {
            id: current.user_id,
            username: current.username,
            role: current.role,
            membership: current.membership,
          } : null,
        }),
      });
      return;
    }
    if (url.pathname === "/api/dashboard") {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          ok: true,
          meta: {
            trade_date: "2026-07-22",
            requested_date: "2026-07-22",
            source: "tushare",
            realtime: false,
            cached: true,
            market_status: "closed",
            updated_at: "2026-07-22T15:00:00+08:00",
          },
          overview: {
            up_count: 2100,
            down_count: 2800,
            limit_up_count: 42,
            limit_down_count: 8,
            broken_count: 17,
            seal_rate: 71.2,
            amount_billion: 12600,
            sentiment_score: 48,
          },
          limits: [],
          broken: [],
          down_limits: [],
          yesterday_limits: [],
          limit_performance: [],
          ladders: [],
          sectors: [],
          sector_rotation: [],
        }),
      });
      return;
    }
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({ ok: true }),
    });
  });
}

const SAVED_ACCOUNTS = [
  {
    user_id: 1,
    username: "alpha_user",
    role: "admin",
    membership: { active: true, subscribed: true, is_admin: true },
    last_used_at: "2026-08-29T01:00:00+00:00",
  },
  {
    user_id: 2,
    username: "beta_user",
    role: "user",
    membership: { active: false, subscribed: false, is_admin: false },
    last_used_at: "2026-08-28T01:00:00+00:00",
  },
];

test("first-time login portal asks for a password and hides environment copy", async ({ page }) => {
  await mockLoginPortal(page, { accounts: [] });
  await page.goto("/login/");
  await expect(page.locator(".login-card-title")).toHaveText("欢迎回来");
  await expect(page.locator("#loginUsername")).toBeVisible();
  await expect(page.locator(".login-submit")).toHaveText("登录");
  await expect(page.locator("body")).not.toContainText("内网个人版");
  await expect(page.locator("body")).not.toContainText("192.168.200.11");
});

test("saved accounts can switch directly and show a re-auth message on failure", async ({ page }) => {
  await mockLoginPortal(page, { accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })) });
  await page.goto("/login/");
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
  await expect(page.locator(".login-account-row")).toHaveCount(2);
  const switched = page.waitForRequest((request) => (
    request.url().includes("/api/auth/switch") && request.method() === "POST"
  ));
  await page.locator('[data-switch-id="2"]').click();
  const request = await switched;
  expect(JSON.parse(request.postData() || "{}")).toEqual({ user_id: 2 });
});

test("failed account switch stays on the portal with the original copy", async ({ page }) => {
  await mockLoginPortal(page, {
    accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })),
    switchFails: true,
  });
  await page.goto("/login/");
  await page.locator('[data-switch-id="2"]').click();
  await expect(page.locator(".login-error")).toHaveText("该账号需重新验证");
  await expect(page).toHaveURL(/\/login\/?/);
});

test("managing accounts removes a local record after inline confirmation", async ({ page }) => {
  await mockLoginPortal(page, { accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })) });
  await page.goto("/login/");
  await page.locator('[data-login-action="manage"]').click();
  await expect(page.locator(".login-card-title")).toHaveText("管理账号记录");
  await page.locator('[data-confirm-id="2"]').click();
  await expect(page.locator(".login-confirm-copy")).toContainText("beta_user");
  await page.locator('[data-forget-id="2"]').click();
  await expect(page.locator(".login-account-row")).toHaveCount(1);
  await expect(page.locator(".login-account-row")).toContainText("alpha_user");
});

async function assertConfirmedSkeleton(page, { width, height }) {
  await expect(page.locator(".login-brand-title")).toHaveText("看懂情绪周期，把复盘变成下一次的先手。");
  await expect(page.locator(".login-brand-header")).toBeVisible();
  await expect(page.locator(".login-brand-chart")).toBeVisible();
  await expect(page.locator(".login-brand-stats")).toBeVisible();
  await expect(page.locator(".login-brand-kicker")).toHaveText("收盘之后 · 复盘开始");
  const brand = await page.locator(".login-brand").boundingBox();
  const header = await page.locator(".login-brand-header").boundingBox();
  const mark = await page.locator(".login-brand-mark").boundingBox();
  const name = await page.locator(".login-brand-name").boundingBox();
  const title = await page.locator(".login-brand-title").boundingBox();
  const stats = await page.locator(".login-brand-stats").boundingBox();
  const chart = await page.locator(".login-brand-chart").boundingBox();
  const card = await page.locator(".login-card").boundingBox();
  expect(brand).toBeTruthy();
  expect(header.y - brand.y).toBeLessThan(48);
  expect(Math.abs(mark.y - name.y)).toBeLessThan(16);
  expect(title.y).toBeGreaterThan(height * 0.28);
  expect(title.y).toBeLessThan(height * 0.72);
  expect(stats.y).toBeGreaterThan(height * 0.55);
  expect(chart.height).toBeGreaterThan(80);
  await expect(page.locator("#loginMascots")).toBeVisible();
  expect(card.width).toBeGreaterThan(380);
  expect(card.width).toBeLessThan(450);
  if (width === 1440) {
    expect(brand.width).toBeGreaterThan(470);
    expect(brand.width).toBeLessThan(520);
    expect(brand.height).toBe(height);
    const mascots = await page.locator("#loginMascots").boundingBox();
    const kicker = await page.locator(".login-brand-kicker").boundingBox();
    expect(mascots).toBeTruthy();
    expect(kicker).toBeTruthy();
    expect(mascots.y).toBeGreaterThan(header.y + header.height - 4);
    expect(mascots.y + mascots.height).toBeLessThan(kicker.y + 8);
    const gapTop = header.y + header.height;
    const gapBottom = kicker.y;
    const mid = (gapTop + gapBottom) / 2;
    const mascotMid = mascots.y + mascots.height / 2;
    expect(Math.abs(mascotMid - mid)).toBeLessThan(48);
    expect(title.y).toBeGreaterThan(470);
    expect(title.y).toBeLessThan(580);
  } else if (width === 1920) {
    expect(brand.width).toBeGreaterThan(540);
    expect(brand.width).toBeLessThan(580);
  } else {
    expect(brand.width).toBeGreaterThan(560);
  }
}

async function openPortal(page, { theme, width, height, accounts, currentUserId, loginFails, loginDelay }) {
  await page.addInitScript((nextTheme) => {
    localStorage.setItem("xiaobaiTheme", nextTheme);
  }, theme);
  await page.setViewportSize({ width, height });
  await mockLoginPortal(page, { accounts, currentUserId, loginFails, loginDelay });
  await page.goto("/login/");
}

for (const theme of ["light", "dark"]) {
  for (const [width, height] of [[1440, 900], [1920, 1080]]) {
    test(`confirmed skeleton ${theme} ${width}x${height}`, async ({ page }) => {
      await openPortal(page, { theme, width, height, accounts: [] });
      await assertConfirmedSkeleton(page, { width, height });
      await expect(page.locator("#loginThemeToggle")).toHaveText(theme === "dark" ? "☀ 日间" : "🌙 夜间");
      await page.screenshot({ path: path.join(SHOT_DIR, `first-${theme}-${width}.png`), fullPage: true });
    });
  }
}

test("ultrawide keeps the left brand from collapsing into a strip", async ({ page }) => {
  await openPortal(page, { theme: "dark", width: 2560, height: 1080, accounts: [] });
  await assertConfirmedSkeleton(page, { width: 2560, height: 1080 });
});

test("picker add remove error and loading share the same desktop skeleton", async ({ page }) => {
  const accounts = SAVED_ACCOUNTS.map((item) => ({ ...item }));
  await openPortal(page, {
    theme: "dark",
    width: 1440,
    height: 900,
    accounts,
    currentUserId: 1,
  });
  await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
  await expect(page.locator(".login-avatar")).toHaveCount(2);
  await expect(page.locator(".login-add")).toBeVisible();
  await page.screenshot({ path: path.join(SHOT_DIR, "picker-dark-1440.png"), fullPage: true });

  await page.locator('[data-login-action="add"]').click();
  await expect(page.locator(".login-card-title")).toHaveText("添加账号");
  await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
  await page.screenshot({ path: path.join(SHOT_DIR, "add-dark-1440.png"), fullPage: true });

  await page.locator('[data-login-action="picker"]').click();
  await page.locator('[data-login-action="manage"]').click();
  await expect(page.locator(".login-card-title")).toHaveText("管理账号记录");
  await page.locator('[data-confirm-id="2"]').click();
  await expect(page.locator(".login-confirm-copy")).toContainText("beta_user");
  await page.screenshot({ path: path.join(SHOT_DIR, "remove-dark-1440.png"), fullPage: true });
});

test("login failure and loading keep the confirmed first-login skeleton", async ({ page }) => {
  await openPortal(page, {
    theme: "light",
    width: 1440,
    height: 900,
    accounts: [],
    loginFails: true,
  });
  await page.locator("#loginUsername").fill("baiqizhi");
  await page.locator("#loginPassword").fill("wrong-password");
  await page.locator(".login-submit").click();
  await expect(page.locator(".login-error")).toContainText("账号名或密码不正确");
  await expect(page.locator("#loginPassword")).toHaveClass(/is-invalid/);
  await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
  await page.screenshot({ path: path.join(SHOT_DIR, "error-light-1440.png"), fullPage: true });
});

test("loading button appears on the confirmed first-login skeleton", async ({ page }) => {
  await openPortal(page, {
    theme: "dark",
    width: 1440,
    height: 900,
    accounts: [],
    loginDelay: 2500,
  });
  await page.evaluate(() => {
    window.location.replace = () => {};
  });
  await page.locator("#loginUsername").fill("baiqizhi");
  await page.locator("#loginPassword").fill("password12");
  const submit = page.locator(".login-submit").click();
  await expect(page.locator(".login-submit")).toContainText("正在登录...");
  await expect(page.locator(".login-spinner")).toBeVisible();
  await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
  await page.screenshot({ path: path.join(SHOT_DIR, "loading-dark-1440.png"), fullPage: true });
  await submit;
});

async function openPicker(page, options = {}) {
  const accounts = options.accounts || SAVED_ACCOUNTS.map((item) => ({ ...item }));
  const currentUserId = options.currentUserId ?? 1;
  const next = options.next || "/index.html?view=sentimentCycleView";
  await page.unroute("**/api/**").catch(() => {});
  await mockLoginPortal(page, {
    accounts,
    currentUserId,
    sessionExpired: options.sessionExpired,
    switchFails: options.switchFails,
  });
  await page.goto(`/login/?next=${encodeURIComponent(next)}`);
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
}

test("clicking the current account from two workspace pages returns without switching", async ({ page }) => {
  const views = ["sentimentCycleView", "ladderView"];
  for (const viewId of views) {
    const next = `/index.html?view=${viewId}`;
    const switchCalls = [];
    const onRequest = (request) => {
      if (request.url().includes("/api/auth/switch") && request.method() === "POST") {
        switchCalls.push(request);
      }
    };
    page.on("request", onRequest);
    await openPicker(page, { next });
    await expect(page.locator('[data-resume-id="1"]')).toContainText("继续使用");
    await expect(page.locator('[data-resume-id="1"]')).toContainText("当前");
    await page.locator('[data-resume-id="1"]').click();
    await expect(page).toHaveURL(new RegExp(`[?&]view=${viewId}\\b`));
    expect(switchCalls).toEqual([]);
    page.off("request", onRequest);
  }
});

test("a lone current account can return from the picker instead of dead-ending", async ({ page }) => {
  await openPicker(page, {
    accounts: [SAVED_ACCOUNTS[0]],
    currentUserId: 1,
    next: "/index.html?view=reviewWorkspaceView",
  });
  await expect(page.locator(".login-account-row")).toHaveCount(1);
  await expect(page.locator('[data-switch-id]')).toHaveCount(0);
  await page.locator('[data-resume-id="1"]').click();
  await expect(page).toHaveURL(/view=reviewWorkspaceView/);
});

test("the return control also restores the originating workspace page", async ({ page }) => {
  await openPicker(page, { next: "/index.html?view=ladderView" });
  await page.locator('[data-login-action="resume"]').click();
  await expect(page).toHaveURL(/view=ladderView/);
});

test("refreshing the picker still returns to the originating page", async ({ page }) => {
  await openPicker(page, { next: "/index.html?view=sentimentCycleView" });
  await page.reload();
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
  await page.locator('[data-resume-id="1"]').click();
  await expect(page).toHaveURL(/view=sentimentCycleView/);
});

test("an expired current session asks for login instead of pretending to return", async ({ page }) => {
  await openPicker(page, {
    next: "/index.html?view=sentimentCycleView",
    sessionExpired: true,
  });
  await page.locator('[data-resume-id="1"]').click();
  await expect(page.locator(".login-error")).toHaveText("当前会话已失效，请重新登录");
  await expect(page).toHaveURL(/\/login\/?/);
});

test("other saved accounts still switch while the current row only resumes", async ({ page }) => {
  await openPicker(page, { next: "/index.html?view=auctionView" });
  const switched = page.waitForRequest((request) => (
    request.url().includes("/api/auth/switch") && request.method() === "POST"
  ));
  await page.locator('[data-switch-id="2"]').click();
  const request = await switched;
  expect(JSON.parse(request.postData() || "{}")).toEqual({ user_id: 2 });
});

test("workspace switch-account menu carries the current page back to the picker", async ({ page }) => {
  await mockLoginPortal(page, {
    accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })),
    currentUserId: 1,
  });
  await page.goto("/index.html?view=sentimentCycleView");
  await expect(page.locator("#accountButton")).toBeVisible();
  await page.locator("#accountButton").click();
  await page.locator("#switchAccountMenuButton").click();
  await expect(page).toHaveURL(/\/login\/\?next=/);
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
  await page.locator('[data-resume-id="1"]').click();
  await expect(page).toHaveURL(/view=sentimentCycleView/);
});

test("workspace switch-account from a second page also returns to that page", async ({ page }) => {
  await mockLoginPortal(page, {
    accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })),
    currentUserId: 1,
  });
  await page.goto("/index.html?view=ladderView");
  await expect(page.locator("#accountButton")).toBeVisible();
  await page.locator("#accountButton").click();
  await page.locator("#switchAccountMenuButton").click();
  await expect(page.locator(".login-card-title")).toHaveText("选择账号");
  await page.locator('[data-resume-id="1"]').click();
  await expect(page).toHaveURL(/view=ladderView/);
});

test("desktop mascots react to account focus, password, toggle, loading, success and failure", async ({ page }) => {
  await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
  const mascots = page.locator("#loginMascots");
  await expect(mascots).toHaveAttribute("data-mood", "idle");
  await page.locator("#loginUsername").focus();
  await expect(mascots).toHaveAttribute("data-mood", "account");
  await page.locator("#loginPassword").focus();
  await expect(mascots).toHaveAttribute("data-mood", "password");
  await expect.poll(async () => (
    page.locator(".login-mascot.is-red .login-mascot-hand.is-left").evaluate((node) => getComputedStyle(node).opacity)
  )).toBe("1");
  await page.locator(".login-password-toggle").click();
  await expect(page.locator("#loginPassword")).toHaveAttribute("type", "text");
  await expect(mascots).toHaveAttribute("data-mood", "password");
  await page.locator(".login-password-toggle").click();
  await expect(page.locator("#loginPassword")).toHaveAttribute("type", "password");
  await expect(mascots).toHaveAttribute("data-mood", "password");
  await page.screenshot({ path: path.join(SHOT_DIR, "mascots-password-light-1440.png"), fullPage: true });
});

test("login loading and failure drive short mascot feedback", async ({ page }) => {
  await openPortal(page, {
    theme: "dark",
    width: 1440,
    height: 900,
    accounts: [],
    loginFails: true,
    loginDelay: 800,
  });
  await page.locator("#loginUsername").fill("baiqizhi");
  await page.locator("#loginPassword").fill("wrong-password");
  const submit = page.locator(".login-submit").click();
  await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "busy");
  await expect(page.locator(".login-error")).toContainText("账号名或密码不正确");
  await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "fail");
  await page.screenshot({ path: path.join(SHOT_DIR, "mascots-fail-dark-1440.png"), fullPage: true });
  await submit;
});

test("login success plays a hop before leaving the portal", async ({ page }) => {
  await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
  await page.evaluate(() => {
    window.location.replace = () => {};
  });
  await page.locator("#loginUsername").fill("baiqizhi");
  await page.locator("#loginPassword").fill("password12");
  const submit = page.locator(".login-submit").click();
  await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", /busy|success/);
  await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "success", { timeout: 4000 });
  await page.screenshot({ path: path.join(SHOT_DIR, "mascots-success-light-1440.png"), fullPage: true });
  await submit;
});

test("reduced motion keeps static mascots without mouse tracking", async ({ page }) => {
  await page.emulateMedia({ reducedMotion: "reduce" });
  await openPortal(page, { theme: "dark", width: 1440, height: 900, accounts: [] });
  const mascots = page.locator("#loginMascots");
  await expect(mascots).toBeVisible();
  await expect(mascots).toHaveAttribute("data-mood", "idle");
  const before = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
  await page.mouse.move(1200, 120);
  await page.waitForTimeout(120);
  const after = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
  expect(after).toBe(before);
  await page.locator("#loginPassword").focus();
  await expect(mascots).toHaveAttribute("data-mood", "password");
  await page.screenshot({ path: path.join(SHOT_DIR, "mascots-reduced-dark-1440.png"), fullPage: true });
});

test("mouse follow updates mascot pupils on a fine pointer", async ({ page }) => {
  await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
  const mascots = page.locator("#loginMascots");
  await page.mouse.move(80, 160);
  await page.waitForTimeout(180);
  const left = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
  await page.mouse.move(1280, 200);
  await page.waitForTimeout(180);
  const right = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
  expect(Number.parseFloat(right)).toBeGreaterThan(Number.parseFloat(left));
});

test("narrow screens hide mascots without moving the login card", async ({ page }) => {
  await openPortal(page, { theme: "light", width: 800, height: 900, accounts: [] });
  await expect(page.locator("#loginMascots")).toBeHidden();
  await expect(page.locator(".login-card-title")).toHaveText("欢迎回来");
  await expect(page.locator(".login-brand-title")).toBeHidden();
});

test("theme toggle keeps mascots in the brand gap", async ({ page }) => {
  await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
  await page.locator("#loginThemeToggle").click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
  await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
  await page.screenshot({ path: path.join(SHOT_DIR, "mascots-idle-dark-1440.png"), fullPage: true });
});
