import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test, { after } from "node:test";

const dataDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "qiyouxue-auth-"));
process.env.CMS_DATA_DIR = dataDirectory;
process.env.CMS_PASSWORD = "environment-master-password";
process.env.CMS_SECRET = "auth-test-session-secret";
process.env.CMS_API_KEY = "auth-test-api-key";

const [{ handleCmsApi }, { createConsultation, createConsultationTeacher }] = await Promise.all([
  import("../src/lib/cms-api"),
  import("../src/lib/consultation-store"),
]);
let requestNumber = 0;

after(async () => {
  await fs.rm(dataDirectory, { recursive: true, force: true });
});

function cookieFrom(response: Response, name: string): string {
  const header = response.headers.get("set-cookie") || "";
  const match = header.match(new RegExp(`(?:^|,\\s*)${name}=([^;]*)`));
  assert.ok(match, `missing ${name} cookie in ${header}`);
  return `${name}=${match[1]}`;
}

async function login(password: string): Promise<{ response: Response; cookie?: string }> {
  requestNumber += 1;
  const captchaResponse = await handleCmsApi(
    new Request("http://localhost/api/cms/captcha"),
    "captcha",
    `captcha-${requestNumber}`,
  );
  const code = [...(await captchaResponse.text()).matchAll(/<text\b[^>]*>([^<])<\/text>/g)]
    .map((match) => match[1])
    .join("");
  assert.equal(code.length, 4);
  const response = await handleCmsApi(new Request("http://localhost/api/cms/session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Cookie: cookieFrom(captchaResponse, "cms_captcha"),
    },
    body: JSON.stringify({ password, captcha: code, captchaToken: captchaResponse.headers.get("X-Captcha-Token") }),
  }), "session", `login-${requestNumber}`);
  return {
    response,
    cookie: response.ok ? cookieFrom(response, "cms_session") : undefined,
  };
}

async function staffLogin(username: string, password: string): Promise<{ response: Response; cookie?: string }> {
  requestNumber += 1;
  const captchaResponse = await handleCmsApi(
    new Request("http://localhost/api/cms/captcha"),
    "captcha",
    `staff-captcha-${requestNumber}`,
  );
  const code = [...(await captchaResponse.text()).matchAll(/<text\b[^>]*>([^<])<\/text>/g)]
    .map((match) => match[1])
    .join("");
  const response = await handleCmsApi(new Request("http://localhost/api/cms/session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Cookie: cookieFrom(captchaResponse, "cms_captcha"),
    },
    body: JSON.stringify({ username, password, captcha: code, captchaToken: captchaResponse.headers.get("X-Captcha-Token") }),
  }), "session", `staff-login-${requestNumber}`);
  return {
    response,
    cookie: response.ok ? cookieFrom(response, "cms_teacher_session") : undefined,
  };
}

async function sessionStatus(cookie: string): Promise<boolean> {
  const response = await handleCmsApi(new Request("http://localhost/api/cms/session", {
    headers: { Cookie: cookie },
  }), "session");
  return Boolean((await response.json()).authed);
}

async function changePassword(cookie: string, currentPassword: string, newPassword: string): Promise<Response> {
  return handleCmsApi(new Request("http://localhost/api/cms/account/password", {
    method: "PUT",
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: JSON.stringify({ currentPassword, newPassword }),
  }), "account/password");
}

test("the displayed captcha remains valid when a later image request replaces the captcha cookie", async () => {
  const displayedCaptcha = await handleCmsApi(
    new Request("http://localhost/api/cms/captcha"),
    "captcha",
    "captcha-race-displayed",
  );
  const displayedCode = [...(await displayedCaptcha.text()).matchAll(/<text\b[^>]*>([^<])<\/text>/g)]
    .map((match) => match[1])
    .join("");
  const displayedToken = displayedCaptcha.headers.get("X-Captcha-Token");
  assert.equal(displayedCode.length, 4);
  assert.ok(displayedToken);

  const laterCaptcha = await handleCmsApi(
    new Request("http://localhost/api/cms/captcha"),
    "captcha",
    "captcha-race-later",
  );
  const response = await handleCmsApi(new Request("http://localhost/api/cms/session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Cookie: cookieFrom(laterCaptcha, "cms_captcha"),
    },
    body: JSON.stringify({
      password: "environment-master-password",
      captcha: displayedCode,
      captchaToken: displayedToken,
    }),
  }), "session", "captcha-race-login");

  assert.equal(response.status, 200);
});

test("users can change the persisted password while the .env password remains valid", async () => {
  const initialLogin = await login("environment-master-password");
  assert.equal(initialLogin.response.status, 200);
  assert.ok(initialLogin.cookie);

  const tooShort = await changePassword(initialLogin.cookie, "environment-master-password", "abcd");
  assert.equal(tooShort.status, 400);
  assert.match((await tooShort.json()).error, /至少需要 5 个字符/);
  assert.equal(await sessionStatus(initialLogin.cookie), true);

  const firstPassword = "first-user-password";
  const changed = await changePassword(initialLogin.cookie, "environment-master-password", firstPassword);
  assert.equal(changed.status, 200);
  assert.match(changed.headers.get("set-cookie") || "", /cms_session=;/);
  assert.equal((await changed.json()).requiresLogin, true);
  assert.equal(await sessionStatus(initialLogin.cookie), false);

  const stored = await fs.readFile(path.join(dataDirectory, "auth", "password.json"), "utf8");
  assert.doesNotMatch(stored, new RegExp(firstPassword));

  const userLogin = await login(firstPassword);
  assert.equal(userLogin.response.status, 200);
  assert.ok(userLogin.cookie);
  assert.equal((await login("environment-master-password")).response.status, 200);

  const secondPassword = "abcde";
  const changedAgain = await changePassword(userLogin.cookie, firstPassword, secondPassword);
  assert.equal(changedAgain.status, 200);
  assert.equal(await sessionStatus(userLogin.cookie), false);
  assert.equal((await login(firstPassword)).response.status, 401);
  assert.equal((await login(secondPassword)).response.status, 200);
  assert.equal((await login("environment-master-password")).response.status, 200);
});

test("an API key alone cannot change the browser login password", async () => {
  const response = await handleCmsApi(new Request("http://localhost/api/cms/account/password", {
    method: "PUT",
    headers: {
      Authorization: "Bearer auth-test-api-key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      currentPassword: "environment-master-password",
      newPassword: "api-key-must-not-change-this",
    }),
  }), "account/password");
  assert.equal(response.status, 401);
});

test("customer service and operator accounts are restricted to their own work areas", async () => {
  const serviceAccount = await createConsultationTeacher({
    role: "customer_service",
    name: "测试客服",
    username: "service_test",
    password: "service-password",
    title: "学情客服",
    qrCodeUrl: "/uploads/service-test.png",
    sort: 1,
    enabled: true,
  });
  const originalFetch = globalThis.fetch;
  globalThis.fetch = (async () => new Response(JSON.stringify({ code: 0, data: { province: "山西", city: "运城", sp: "移动" } }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
  try {
    const generatedConsultation = await createConsultation({
      name: "测试用户",
      phone: "13800138000",
      teacherId: serviceAccount.id,
      status: "unprocessed",
      callCount: 0,
      note: "第一行备注\n第二行备注",
    });
    assert.equal(generatedConsultation.location, "山西省 运城市 中国移动");
    assert.equal(generatedConsultation.note, "第一行备注\n第二行备注");
  } finally {
    globalThis.fetch = originalFetch;
  }
  await createConsultationTeacher({
    role: "operator",
    name: "测试运营",
    username: "operator_test",
    password: "operator-password",
    enabled: true,
  });

  const serviceLogin = await staffLogin("service_test", "service-password");
  assert.equal(serviceLogin.response.status, 200);
  assert.equal((await serviceLogin.response.json()).user.role, "customer_service");
  assert.ok(serviceLogin.cookie);
  const serviceConsultations = await handleCmsApi(new Request("http://localhost/api/cms/consultations", {
    headers: { Cookie: serviceLogin.cookie },
  }), "consultations");
  assert.equal(serviceConsultations.status, 200);
  const serviceArticles = await handleCmsApi(new Request("http://localhost/api/cms/articles", {
    headers: { Cookie: serviceLogin.cookie },
  }), "articles");
  assert.equal(serviceArticles.status, 403);

  const operatorLogin = await staffLogin("operator_test", "operator-password");
  assert.equal(operatorLogin.response.status, 200);
  assert.equal((await operatorLogin.response.json()).user.role, "operator");
  assert.ok(operatorLogin.cookie);
  const operatorArticles = await handleCmsApi(new Request("http://localhost/api/cms/articles", {
    headers: { Cookie: operatorLogin.cookie },
  }), "articles");
  assert.equal(operatorArticles.status, 200);
  const operatorConsultations = await handleCmsApi(new Request("http://localhost/api/cms/consultations", {
    headers: { Cookie: operatorLogin.cookie },
  }), "consultations");
  assert.equal(operatorConsultations.status, 403);
});
