import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
  antiDetectPause: vi.fn(async () => {}),
  ensureSlideVerification: vi.fn(async () => false),
  humanClick: vi.fn(async (_page, locator) => {
    await locator.click?.();
  }),
  randomBetween: vi.fn(() => 1),
  waitForManualVerificationIfNeeded: vi.fn(async () => false),
}));
const clock = vi.hoisted(() => ({
  nowMs: 0,
}));

vi.mock("./human.js", () => ({
  antiDetectPause: mocks.antiDetectPause,
  ensureSlideVerification: mocks.ensureSlideVerification,
  humanClick: mocks.humanClick,
  randomBetween: mocks.randomBetween,
  waitForManualVerificationIfNeeded: mocks.waitForManualVerificationIfNeeded,
}));

vi.mock("node:timers/promises", () => ({
  setTimeout: vi.fn(async (ms?: number) => {
    clock.nowMs += Number(ms ?? 0);
  }),
  default: {
    setTimeout: vi.fn(async (ms?: number) => {
      clock.nowMs += Number(ms ?? 0);
    }),
  },
}));

import { ensure1688LoggedIn } from "./ensure-1688-login.js";

class FakeLocator {
  constructor(
    private readonly page: FakePage,
    private readonly kind: "body" | "passwordTab" | "loginButton" | "input" | "missing",
  ) {}

  first() {
    return this;
  }

  async waitFor() {
    if (this.kind === "missing") {
      throw new Error("locator.waitFor: Timeout 15000ms exceeded.");
    }
  }

  async click() {
    if (this.kind === "loginButton") {
      this.page.afterLoginClicked = true;
      this.page.bodyText = this.page.loginClickBodyText;
    }
  }

  async isVisible() {
    return this.kind !== "missing";
  }

  async count() {
    return this.kind === "missing" ? 0 : 1;
  }

  async innerText() {
    if (this.page.closed) {
      throw new Error("Target page, context or browser has been closed");
    }
    return this.page.bodyText;
  }
}

class FakePage {
  currentUrl = "about:blank";
  bodyText = "";
  loginClickBodyText = "安全验证 手机短信验证码 验证码已发送 请输入验证码";
  afterLoginClicked = false;
  closed = false;
  keyboard = {
    press: vi.fn(async () => {}),
    type: vi.fn(async () => {}),
  };

  constructor(private readonly contextRef: FakeContext) {}

  isClosed() {
    return this.closed;
  }

  url() {
    if (this.afterLoginClicked && clock.nowMs >= 6500) {
      return "https://i.taobao.com/my_itaobao";
    }
    return this.currentUrl;
  }

  async goto(url: string) {
    if (this.closed) {
      throw new Error("Target page, context or browser has been closed");
    }
    this.currentUrl = url;
    this.bodyText = url.includes("login.taobao.com") ? "密码登录" : "1688 首页";
  }

  async waitForLoadState() {}

  async bringToFront() {}

  context() {
    return this.contextRef;
  }

  locator(selector: string) {
    if (selector === "body") return new FakeLocator(this, "body");
    if (selector === "#fm-login-id" || selector === "#fm-login-password") return new FakeLocator(this, "input");
    if (selector === ".password-login" || selector.includes("submit")) return new FakeLocator(this, "loginButton");
    return new FakeLocator(this, "missing");
  }

  getByText(text: string) {
    return new FakeLocator(this, text === "密码登录" ? "passwordTab" : "missing");
  }

  getByRole(_role: string, options: { name?: RegExp }) {
    return new FakeLocator(this, options.name?.test("登录") ? "loginButton" : "missing");
  }

  async evaluate() {
    if (this.closed) {
      throw new Error("Target page, context or browser has been closed");
    }
    return this.bodyText;
  }
}

class FakeContext {
  page = new FakePage(this);
  cookieChecks = 0;
  readonly cookieSuccessAt: number | null;

  constructor(params: { cookieSuccessAt?: number | null; loginClickBodyText?: string } = {}) {
    this.cookieSuccessAt = params.cookieSuccessAt === undefined ? 3 : params.cookieSuccessAt;
    if (params.loginClickBodyText) {
      this.page.loginClickBodyText = params.loginClickBodyText;
    }
  }

  pages() {
    return [this.page];
  }

  async newPage() {
    return this.page;
  }

  async cookies() {
    this.cookieChecks += 1;
    if (this.cookieSuccessAt != null && this.cookieChecks >= Math.max(1, this.cookieSuccessAt - 1)) {
      this.page.currentUrl = "https://i.taobao.com/my_itaobao";
    }
    return this.cookieSuccessAt != null && this.cookieChecks >= this.cookieSuccessAt
      ? [{ name: "cookie2", value: "ok" }]
      : [];
  }
}

describe("ensure1688LoggedIn", () => {
  beforeEach(() => {
    clock.nowMs = 0;
    vi.spyOn(Date, "now").mockImplementation(() => clock.nowMs);
    mocks.antiDetectPause.mockClear();
    mocks.ensureSlideVerification.mockReset();
    mocks.humanClick.mockClear();
    mocks.waitForManualVerificationIfNeeded.mockReset();
    mocks.waitForManualVerificationIfNeeded.mockResolvedValue(false);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("keeps waiting for login cookies after SMS verification consumes the original timeout", async () => {
    const context = new FakeContext();
    mocks.ensureSlideVerification.mockImplementation(async (page: FakePage) => {
      if (page.afterLoginClicked && page.bodyText.includes("短信验证码")) {
        clock.nowMs = 5000;
        page.bodyText = "密码登录";
        page.currentUrl = "https://login.taobao.com/member/login.jhtml";
        return true;
      }
      return false;
    });

    const pending = ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
    });

    await expect(pending).resolves.toMatchObject({
      ok: true,
      status: "logged_in",
    });
  });

  it("returns manual verification required quickly for SMS verification in headless mode", async () => {
    const context = new FakeContext({ cookieSuccessAt: null });

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 90000,
      headless: true,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "manual_verification_required",
      reason: "搜索前登录态预热暂停：当前页面需要手机短信验证码",
    });
    expect(clock.nowMs).toBeLessThan(90000);
  });

  it("returns manual verification required quickly for captcha verification in headless mode", async () => {
    const context = new FakeContext({
      cookieSuccessAt: null,
      loginClickBodyText: "安全验证 验证失败 请按住滑块拖动到最右边",
    });

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 90000,
      headless: true,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "manual_verification_required",
      reason: "搜索前登录态预热失败：当前页面需要验证码或二次验证",
    });
    expect(clock.nowMs).toBeLessThan(90000);
  });

  it("returns a clear headed-mode error when the verification page is closed", async () => {
    const context = new FakeContext({
      cookieSuccessAt: null,
      loginClickBodyText: "安全验证 验证失败 请按住滑块拖动到最右边",
    });
    mocks.ensureSlideVerification.mockImplementation(async (page: FakePage) => {
      if (page.afterLoginClicked) {
        page.closed = true;
      }
      return false;
    });

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
      headless: false,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "login_failed",
      reason: "有头模式人工验证未完成：浏览器页面已关闭",
    });
  });

  it("returns a clear headed-mode error when verification remains unresolved", async () => {
    const context = new FakeContext({
      cookieSuccessAt: null,
      loginClickBodyText: "安全验证 验证失败 请按住滑块拖动到最右边",
    });
    mocks.ensureSlideVerification.mockImplementation(async () => false);
    mocks.waitForManualVerificationIfNeeded.mockImplementation(async () => {
      clock.nowMs = Math.max(clock.nowMs, 5000);
      return false;
    });

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
      headless: false,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "login_failed",
      reason: "有头模式人工验证未完成：当前页面仍需要验证码或二次验证",
    });
  });

  it("keeps credential errors as login failures instead of manual verification", async () => {
    const context = new FakeContext({
      cookieSuccessAt: null,
      loginClickBodyText: "账号或密码错误 登录失败",
    });

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "bad-pass",
      timeoutMs: 3000,
      headless: true,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "login_failed",
      reason: "搜索前登录态预热失败：请检查 1688 账户名/邮箱/手机号或密码是否正确",
    });
  });

  it("returns manual verification required instead of raw locator timeout when headless password fields are missing", async () => {
    const context = new FakeContext({
      cookieSuccessAt: null,
    });

    context.page.locator = (selector: string) => {
      if (selector === "body") return new FakeLocator(context.page, "body");
      if (selector === "#fm-login-id" || selector === "#fm-login-password") return new FakeLocator(context.page, "missing");
      if (selector === ".password-login" || selector.includes("submit")) return new FakeLocator(context.page, "loginButton");
      return new FakeLocator(context.page, "missing");
    };

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
      headless: true,
    });

    expect(result).toMatchObject({
      ok: false,
      status: "manual_verification_required",
      reason: "搜索前登录态预热暂停：登录表单未显示，请在可视化窗口完成登录或验证",
    });
  });

  it("accepts a trusted post-login redirect after confirming the 1688 home page is reachable", async () => {
    const context = new FakeContext({ cookieSuccessAt: null });
    const visitedUrls: string[] = [];
    context.page.goto = async (url: string) => {
      visitedUrls.push(url);
      if (url.includes("login.taobao.com")) {
        context.page.currentUrl = "https://i.taobao.com/my_itaobao";
        context.page.bodyText = "我的淘宝";
        return;
      }
      context.page.currentUrl = url;
      context.page.bodyText = "1688 首页";
    };
    context.page.locator = (selector: string) => {
      if (selector === "body") return new FakeLocator(context.page, "body");
      if (selector === "#fm-login-id" || selector === "#fm-login-password") return new FakeLocator(context.page, "missing");
      if (selector === ".password-login" || selector.includes("submit")) return new FakeLocator(context.page, "loginButton");
      return new FakeLocator(context.page, "missing");
    };

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
      headless: false,
    });

    expect(result).toMatchObject({
      ok: true,
      status: "trusted_post_login",
    });
    expect(visitedUrls).toContain("https://www.1688.com/");
  });

  it("uses an existing browser session when valid cookies are already present", async () => {
    const context = new FakeContext({ cookieSuccessAt: 1 });
    context.page.currentUrl = "https://www.1688.com/";
    context.page.bodyText = "1688 首页";

    const result = await ensure1688LoggedIn(context as never, {
      accountName: "user",
      password: "pass",
      timeoutMs: 3000,
      headless: true,
    });

    expect(result).toMatchObject({
      ok: true,
      status: "existing_session",
    });
  });
});
