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 } = await import("../src/lib/cms-api");
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 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);
});
