/**
 * 共享步骤：取 captcha、登录玩家、登录超管、获取/重置玩家状态。
 */

import { test, request as pwRequest, type APIRequestContext, expect } from '@playwright/test';
import { adminLogin, playerLogin, playerCtx, adminCtx, fetchCurrentDrawNo, e2e } from '../fixtures';

export const E2E_TAG = '@e2e';

export async function fetchPlayerCaptcha(): Promise<{ captcha_key: string; image_svg: string }> {
  const ctx = await pwRequest.newContext();
  const resp = await ctx.get('/api/v1/player/auth/captcha');
  expect(resp.ok(), `GET /player/auth/captcha failed: ${resp.status()}`).toBeTruthy();
  const body = (await resp.json()) as { data: { captcha_key: string; image_svg: string; image_base64: string } };
  await ctx.dispose();
  return { captcha_key: body.data.captcha_key, image_svg: body.data.image_svg };
}

export async function fetchAdminCaptcha(): Promise<{ captcha_key: string }> {
  const ctx = await pwRequest.newContext();
  const resp = await ctx.get('/api/v1/admin/auth/captcha');
  expect(resp.ok(), `GET /admin/auth/captcha failed: ${resp.status()}`).toBeTruthy();
  const body = (await resp.json()) as { data: { captcha_key: string } };
  await ctx.dispose();
  return { captcha_key: body.data.captcha_key };
}

/** 用 LOTTERY_E2E_BYPASS 登录（管理端 captcha 旁路） */
export async function adminLoginViaBypass(): Promise<ReturnType<typeof adminLogin>> {
  const captcha = await fetchAdminCaptcha();
  const ctx = await pwRequest.newContext();
  const resp = await ctx.post('/api/v1/admin/auth/login', {
    data: {
      account: process.env.E2E_ADMIN_USERNAME ?? 'admin',
      password: process.env.E2E_ADMIN_PASSWORD ?? '12345678',
      captcha_key: captcha.captcha_key,
      captcha_code: 'LOTTERY_E2E_BYPASS',
    },
  });
  expect(resp.ok(), `admin login failed: ${resp.status()}`).toBeTruthy();
  const body = (await resp.json()) as { data: any };
  await ctx.dispose();
  if (!body.data?.token) throw new Error('admin login returned no token: ' + JSON.stringify(body));
  return {
    accessToken: body.data.token,
    tokenType: body.data.token_type ?? 'Bearer',
    expiresIn: 0,
    admin: body.data.admin,
  };
}

/** 用 LOTTERY_E2E_BYPASS 登录（玩家端 captcha 旁路） */
export async function playerLoginViaBypass(creds?: {
  site_code?: string;
  username?: string;
  password?: string;
}): Promise<ReturnType<typeof playerLogin>> {
  const captcha = await fetchPlayerCaptcha();
  const ctx = await pwRequest.newContext();
  const resp = await ctx.post('/api/v1/player/auth/login', {
    data: {
      site_code: creds?.site_code ?? process.env.E2E_PLAYER_SITE_CODE ?? 'demo',
      username: creds?.username ?? process.env.E2E_PLAYER_USERNAME ?? 'demo_player',
      password: creds?.password ?? process.env.E2E_PLAYER_PASSWORD ?? '12345678',
      captcha_key: captcha.captcha_key,
      captcha_code: 'LOTTERY_E2E_BYPASS',
    },
  });
  expect(resp.ok(), `player login failed: ${resp.status()}`).toBeTruthy();
  const body = (await resp.json()) as { data: any };
  await ctx.dispose();
  if (!body.data?.access_token) throw new Error('player login returned no token: ' + JSON.stringify(body));
  return body.data;
}

/** 重置 e2e 玩家（清失败计数、解除锁定、恢复初始余额） */
export async function resetE2EPlayer(): Promise<void> {
  await e2e('POST', '/player/reset', {});
}

/** 把期号 close_time 改到过去，status=closed */
export async function forceCloseDraw(drawNo: string): Promise<any> {
  return e2e('POST', `/draw/${encodeURIComponent(drawNo)}/close-now`, {});
}

export async function finishDrawCooldown(drawNo: string): Promise<any> {
  return e2e('POST', `/draw/${encodeURIComponent(drawNo)}/finish-cooldown`, {});
}

export async function tickDraws(): Promise<any> {
  return e2e('POST', '/draw/tick', {});
}

export async function inspectDraw(drawNo: string): Promise<any> {
  return e2e('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`, {});
}

export async function waitForDrawStatus(
  drawNo: string,
  targets: string[],
  maxAttempts = 20,
  pauseMs = 500,
): Promise<any> {
  for (let i = 0; i < maxAttempts; i++) {
    const insp = await inspectDraw(drawNo);
    const status = String(insp.data?.status ?? insp.status ?? '');
    if (targets.includes(status)) {
      return insp;
    }
    await tickDraws();
    await sleep(pauseMs);
  }
  const last = await inspectDraw(drawNo);
  throw new Error(
    `draw ${drawNo} not reached ${targets.join('|')} after ${maxAttempts} ticks; last=${JSON.stringify(last).slice(0, 400)}`,
  );
}

export function sleep(ms: number): Promise<void> {
  return new Promise((r) => setTimeout(r, ms));
}

export async function adminCtxOf(token: string): Promise<APIRequestContext> {
  return pwRequest.newContext({
    extraHTTPHeaders: { Authorization: `Bearer ${token}` },
  });
}

export async function playerCtxOf(token: string): Promise<APIRequestContext> {
  return pwRequest.newContext({
    extraHTTPHeaders: { Authorization: `Bearer ${token}` },
  });
}

export async function setupCreditPlayer(opts?: { credit_limit?: number; username?: string }): Promise<{
  player_id: number;
  username: string;
  password: string;
  site_code: string;
  admin_site_id: number;
  agent_node_id: number;
}> {
  const data = await e2e<any>('POST', '/credit-player/setup', opts ?? {});
  return data.data;
}

export async function setupSiteOperator(opts?: {
  role_slug?: string;
  username?: string;
  password?: string;
}): Promise<{
  admin_user_id: number;
  username: string;
  password: string;
  role_slug: string;
  admin_site_id: number;
  site_code: string;
}> {
  const data = await e2e<any>('POST', '/site-operator/setup', opts ?? {});
  return data.data;
}

export async function adminLoginWithAccount(
  account: string,
  password: string,
): Promise<ReturnType<typeof adminLogin>> {
  const captcha = await fetchAdminCaptcha();
  const ctx = await pwRequest.newContext();
  const resp = await ctx.post('/api/v1/admin/auth/login', {
    data: {
      account,
      password,
      captcha_key: captcha.captcha_key,
      captcha_code: 'LOTTERY_E2E_BYPASS',
    },
  });
  expect(resp.ok(), `admin login failed: ${resp.status()}`).toBeTruthy();
  const body = (await resp.json()) as { data: any };
  await ctx.dispose();
  if (!body.data?.token) throw new Error('admin login returned no token: ' + JSON.stringify(body));
  return {
    accessToken: body.data.token,
    tokenType: body.data.token_type ?? 'Bearer',
    expiresIn: 0,
    admin: body.data.admin,
  };
}

/** 以当前时间为中心的 2h 账期窗口；开账前会清掉同站历史账期避免重叠。 */
export function periodWindowIso(): { start: string; end: string } {
  const start = new Date(Date.now() - 3600_000).toISOString().replace('T', ' ').slice(0, 19);
  const end = new Date(Date.now() + 3600_000).toISOString().replace('T', ' ').slice(0, 19);
  return { start, end };
}

export async function resetSiteSettlement(siteCode?: string): Promise<void> {
  await e2e('POST', '/settlement/reset', { site_code: siteCode ?? process.env.E2E_PLAYER_SITE_CODE ?? 'demo' });
}

export async function fetchBillCreditLedgerRows(
  playerId: number,
  billId: number,
  reason: 'settlement_confirm' | 'settlement_payout',
): Promise<Array<{ id: number; amount: number; reason: string; ref_type: string; ref_id: number }>> {
  const ledger = await e2e<any>('GET', `/inspect/credit-ledger?player_id=${playerId}&limit=50`);
  return (ledger.data.rows as any[]).filter(
    (row) =>
      row.reason === reason &&
      row.ref_type === 'settlement_bill' &&
      Number(row.ref_id) === billId,
  );
}

export async function openSettlementPeriod(
  admin: APIRequestContext,
  adminSiteId: number,
  siteCode?: string,
): Promise<number> {
  await resetSiteSettlement(siteCode);
  const { start, end } = periodWindowIso();
  const open = await admin.post('/api/v1/admin/settlement-periods', {
    data: {
      admin_site_id: adminSiteId,
      period_start: start,
      period_end: end,
    },
  });
  expect(open.ok(), `open period status=${open.status()}`).toBeTruthy();
  const openBody = (await open.json()) as any;
  expect(openBody.code).toBe(0);
  return Number(openBody.data.id);
}

export async function mintSsoJwt(sitePlayerId?: string): Promise<{ jwt: string; site_player_id: string }> {
  const data = await e2e<any>('POST', '/sso/mint-jwt', {
    site_player_id: sitePlayerId ?? `e2e-sso-${Date.now()}`,
  });
  return data.data;
}

export async function configureWalletMock(baseUrl: string): Promise<void> {
  await e2e('POST', '/site/wallet-api', { base_url: baseUrl, wallet_api_key: 'e2e-mock-key' });
}

export async function resetWalletMock(): Promise<void> {
  await e2e('POST', '/site/wallet-api/reset', {});
}

export async function setMockWalletMode(mode: 'success' | '504' | 'reject'): Promise<void> {
  const port = process.env.E2E_MOCK_WALLET_PORT ?? '5555';
  const ctx = await pwRequest.newContext({ baseURL: `http://127.0.0.1:${port}` });
  const resp = await ctx.post('/_e2e/mode', { data: { mode } });
  expect(resp.ok(), `mock wallet mode=${mode} failed`).toBeTruthy();
  await ctx.dispose();
}

/** 造 23 个 slot：first=winNumber，其余填占位号 */
export function buildAllResultItems(winNumber: string): any[] {
  const items: any[] = [];
  items.push({ prize_type: 'first', prize_index: 0, number_4d: winNumber });
  items.push({ prize_type: 'second', prize_index: 0, number_4d: '5678' });
  items.push({ prize_type: 'third', prize_index: 0, number_4d: '0123' });
  for (let i = 0; i < 10; i++) {
    items.push({
      prize_type: 'starter',
      prize_index: i,
      number_4d: String(1000 + i).padStart(4, '0'),
    });
  }
  for (let i = 0; i < 10; i++) {
    items.push({
      prize_type: 'consolation',
      prize_index: i,
      number_4d: String(2000 + i).padStart(4, '0'),
    });
  }
  return items;
}

export async function resolveDrawId(admin: APIRequestContext, drawNo: string): Promise<number> {
  const drawsList = await admin.get(`/api/v1/admin/draws?keyword=${encodeURIComponent(drawNo)}&size=10`);
  const dl = (await drawsList.json()) as any;
  const drawId =
    dl.data.items.find((it: any) => it.draw_no === drawNo)?.id ?? dl.data.items[0]?.id;
  expect(drawId, `draw id for ${drawNo}`).toBeTruthy();
  return Number(drawId);
}

export async function fetchOpenDrawNo(): Promise<string> {
  const current = await fetchCurrentDrawNo();
  if (!current?.draw_no) {
    throw new Error('draw/current returned no draw_no');
  }
  const drawNo = current.draw_no;
  const insp = await e2e<any>('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`);
  const status = String(insp.data?.status ?? '');
  if (status !== 'open') {
    await e2e('POST', `/draw/${encodeURIComponent(drawNo)}/reopen`, {});
    const after = await e2e<any>('GET', `/draw/${encodeURIComponent(drawNo)}/inspect`);
    expect(String(after.data?.status), `draw ${drawNo} reopen`).toBe('open');
  }
  return drawNo;
}

export { playerLogin, playerCtx, adminLogin, adminCtx, fetchCurrentDrawNo, e2e };
