import { expect, test } from '@playwright/test';

const regularUser = { id: 8, username: 'demo_operator', roleCode: 'OPERATIONS', pagePermissions: { overview: 'EDIT' } };
const developerUser = { id: 1, username: 'Jeddy', roleCode: 'DEVELOPER', pagePermissions: { overview: 'EDIT' } };

/** Plain purpose: provide an API envelope matching the production backend without using any real credentials. Related files: auth-api-client.js, AuthController.java. Flow: Playwright route -> fetch response -> auth store/router. */
function envelope(data, message = 'success') { return JSON.stringify({ code: 200, message, data }); }

/** Plain purpose: provide the shared unauthenticated session response used before a login scenario begins. Related files: auth-store.js, router/index.js. Flow: protected route -> GET auth/me -> login route. */
async function mockUnauthenticatedSession(page) {
  await page.route('**/api/auth/me', route => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ code: 401, message: '请先登录', data: null }) }));
  await page.route('**/api/auth/csrf', route => route.fulfill({ contentType: 'application/json', body: envelope(null) }));
}

/** Plain purpose: submit one complete credential form so each scenario exercises the real login view bindings. Related files: LoginView.js, auth-store.js. Flow: input values -> submit -> auth API mock -> route/message. */
async function submitLogin(page, username = 'demo_operator', password = 'a-safe-test-password') {
  await page.getByRole('textbox', { name: '账号' }).fill(username);
  await typePassword(page, password);
  await page.getByRole('button', { name: '登录' }).click();
}

/** Code purpose (plain language): enter a test password through the visible native field and keyboard path that a person uses, without reading any password value. Related files: LoginView.js, app.css. Flow: native input click -> keyboard input -> v-model -> native browser password presentation. */
async function typePassword(page, password) {
  const passwordInput = page.getByRole('textbox', { name: '密码' });
  await passwordInput.click();
  await passwordInput.pressSequentially(password);
}

/** Plain purpose: confirm a route guard protects business pages when no valid session exists. Related files: router/index.js, App.js. Flow: browser hash -> auth/me 401 -> login route -> no application shell. */
test('redirects an unauthenticated protected route to login without the sidebar', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.goto('/asset/#/overview');
  await expect(page).toHaveURL(/#\/login$/);
  await expect(page.locator('.sidebar')).toHaveCount(0);
});

/** Plain purpose: confirm an ordinary account reaches the overview without the developer-only greeting. Related files: LoginView.js, auth-store.js. Flow: login response -> user state -> overview route. */
test('logs in a regular account without the developer greeting', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.route('**/api/auth/login', route => route.fulfill({ contentType: 'application/json', body: envelope(regularUser) }));
  await page.goto('/asset/#/login');
  await submitLogin(page);
  await expect(page).toHaveURL(/#\/overview$/);
  await expect(page.getByText('欢迎系统开发者-BOSS：Jeddy 上线')).toHaveCount(0);
});

/** Plain purpose: confirm only the fixed developer identity receives its one-time welcome message. Related files: LoginView.js, auth-store.js. Flow: developer login response -> identity condition -> success message. */
test('shows the developer greeting after Jeddy logs in', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.route('**/api/auth/login', route => route.fulfill({ contentType: 'application/json', body: envelope(developerUser) }));
  await page.goto('/asset/#/login');
  await submitLogin(page, 'Jeddy');
  await expect(page).toHaveURL(/#\/overview$/);
  await expect(page.getByText('欢迎系统开发者-BOSS：Jeddy 上线')).toBeVisible();
});

/** Plain purpose: keep a rejected login distinct from an expired authenticated session. Related files: auth-api-client.js, auth-store.js. Flow: login 401 -> generic form error only -> no session-expired browser event. */
test('keeps a rejected login on the form without emitting a session-expired event', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.route('**/api/auth/login', route => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ code: 401, message: '账号或密码错误', data: null }) }));
  await page.goto('/asset/#/login');
  await page.evaluate(() => { window.__loginExpiredEvents = 0; window.addEventListener('xyw-auth-expired', () => { window.__loginExpiredEvents += 1; }); });
  await submitLogin(page);
  await expect(page).toHaveURL(/#\/login$/);
  await expect(page.getByText('账号或密码错误，请重试')).toBeVisible();
  await expect.poll(() => page.evaluate(() => window.__loginExpiredEvents)).toBe(0);
  await expect(page.getByRole('button', { name: '登录' })).toBeEnabled();
});

/** Plain purpose: ensure the client always returns to the public login route even when server logout fails. Related files: App.js, auth-store.js. Flow: sidebar logout -> API failure -> finally route change -> login page. */
test('returns to login when the logout endpoint fails', async ({ page }) => {
  await page.route('**/api/auth/me', route => route.fulfill({ contentType: 'application/json', body: envelope(developerUser) }));
  await page.route('**/api/auth/logout', route => route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ code: 500, message: 'logout failed', data: null }) }));
  await page.goto('/asset/#/overview');
  await page.getByRole('button', { name: '退出登录' }).click();
  await expect(page).toHaveURL(/#\/login$/);
});

/** 代码作用（白话）：验证登录页恢复 Cookie 会话期间不抢先渲染后台外壳，避免未登录用户刷新时看见侧栏和业务页面闪屏。关联文件：App.js、router/index.js、auth-store.js。关联逻辑（调用链/数据流）：刷新登录路由 -> /api/auth/me 等待 -> authState.ready=false -> 仅加载占位 -> 401 后显示登录页。 */
test('does not render the application shell while the login session check is pending', async ({ page }) => {
  let releaseSessionResponse;
  let markSessionRequested;
  const sessionRequested = new Promise(resolve => { markSessionRequested = resolve; });
  const sessionResponseReleased = new Promise(resolve => { releaseSessionResponse = resolve; });
  /** 代码作用（白话）：把当前用户接口暂时挂起，稳定复现身份尚未恢复的页面时间窗口。关联文件：auth-api-client.js、auth-store.js。关联逻辑（调用链/数据流）：路由守卫 -> /api/auth/me 拦截 -> 等待断言 -> 返回未登录响应。 */
  await page.route('**/api/auth/me', async route => { markSessionRequested(); await sessionResponseReleased; await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ code: 401, message: '请先登录', data: null }) }); });

  const pageLoad = page.goto('/asset/#/login');
  await sessionRequested;
  await expect(page.locator('.app-shell')).toHaveCount(0);
  releaseSessionResponse();
  await pageLoad;
  await expect(page.locator('.login-page')).toBeVisible();
});

/** 代码作用（白话）：验证未登录用户刷新受保护页面时，认证响应尚未返回也不会看见后台侧栏、卡片或登录表单。关联文件：index.html、main.js、router/index.js。关联逻辑（调用链/数据流）：浏览器刷新后台 Hash 路由 -> 中性启动底图 -> 路由守卫请求 /api/auth/me -> 401 后进入登录页。 */
test('does not show an admin interface before an unauthenticated protected refresh redirects', async ({ page }) => {
  let releaseSessionResponse;
  let markSessionRequested;
  const sessionRequested = new Promise(resolve => { markSessionRequested = resolve; });
  const sessionResponseReleased = new Promise(resolve => { releaseSessionResponse = resolve; });
  /** 代码作用（白话）：暂停真实路由守卫发出的当前用户请求，制造刷新期间可观察的认证等待窗口。关联文件：auth-api-client.js、auth-store.js、router/index.js。关联逻辑（调用链/数据流）：受保护地址 -> 路由守卫 -> /api/auth/me 拦截 -> 中性底图断言 -> 返回未登录响应。 */
  await page.route('**/api/auth/me', async route => { markSessionRequested(); await sessionResponseReleased; await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ code: 401, message: '请先登录', data: null }) }); });

  const pageLoad = page.goto('/asset/#/overview');
  await sessionRequested;
  await expect(page.locator('.app-boot-splash')).toBeVisible();
  await expect(page.locator('.login-page, .app-shell, .app-boot-shell')).toHaveCount(0);
  releaseSessionResponse();
  await pageLoad;
  await expect(page).toHaveURL(/#\/login$/);
  await expect(page.locator('.login-page')).toBeVisible();
});

/** 代码作用（白话）：验证后端长时间无响应时，用户仍停留在原受保护地址并能看到网络重试入口，而不会被误跳到登录页。关联文件：auth-store.js、router/index.js、App.js。关联逻辑（调用链/数据流）：/api/auth/me 无响应 -> 会话检查超时 -> authState 网络错误状态 -> App 网络提示与重试按钮。 */
test('shows a recoverable timeout state when the session check does not finish', async ({ page }) => {
  await page.addInitScript(() => { window.__XYW_AUTH_CHECK_TIMEOUT_MS = 20; });
  /** 代码作用（白话）：模拟接口不返回的网络故障，触发前端会话检查超时分支。关联文件：auth-api-client.js、auth-store.js。关联逻辑（调用链/数据流）：请求拦截 -> 保持挂起 -> 超时状态。 */
  await page.route('**/api/auth/me', () => new Promise(() => {}));

  await page.goto('/asset/#/overview');
  await expect(page).toHaveURL(/#\/overview$/);
  await expect(page.locator('.auth-network-error')).toBeVisible();
  await expect(page.locator('.login-page, .app-shell')).toHaveCount(0);
});

/** Plain purpose: make the image-backed login page observable so an accidental plain-color fallback is caught. Related files: LoginView.js, black_hole.png. Flow: Vite asset import -> CSS custom property -> login background. */
test('renders the login page with its Vite-resolved black-hole background', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.goto('/asset/#/login');
  const backgroundImage = await page.locator('.login-page').evaluate(element => getComputedStyle(element).getPropertyValue('--login-background-image'));
  expect(backgroundImage).toContain('url(');
  expect(backgroundImage).toContain('black_hole');
});

/** Code purpose (plain language): protect the browser-owned password presentation by ensuring the login page adds no custom dots while keeping the native field and standard eye icon usable. Related files: LoginView.js, app.css. Flow: login render -> native input value -> browser password dots and Element Plus suffix icon. */
test('uses browser-native password presentation and the standard eye icon', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.goto('/asset/#/login');
  const field = page.locator('.login-password-field');
  const nativeInput = field.locator('input');

  await expect(nativeInput).toBeVisible();
  await typePassword(page, 'eye-icon-test');
  await expect(field.locator('.login-password-mask')).toHaveCount(0);
  await expect(field.locator('.el-input__password')).toBeVisible();
});

/** Code purpose (plain language): keep the password field as wide as the account field after the custom-dot wrapper is removed. Related files: LoginView.js, app.css. Flow: login form render -> wrapper width -> native input width -> visual layout regression check. */
test('keeps the password field the same width as the account field', async ({ page }) => {
  await mockUnauthenticatedSession(page);
  await page.goto('/asset/#/login');
  const accountWidth = await page.getByRole('textbox', { name: '账号' }).locator('xpath=..').evaluate(wrapper => wrapper.getBoundingClientRect().width);
  const passwordWidth = await page.getByRole('textbox', { name: '密码' }).locator('xpath=..').evaluate(wrapper => wrapper.getBoundingClientRect().width);

  expect(passwordWidth).toBeCloseTo(accountWidth, 0);
});
