import test from "node:test";
import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createAccountLoginAttempts } from "./account-login-attempts.js";
import { assertCanAddExecutionAccount } from "./entitlement-policy.js";

test("free and Pro capacity is checked before a new login starts", async (t) => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-capacity-"));
  t.after(() => rm(root, { recursive: true, force: true }));
  const freeConfig = {
    codexCliPath: "codex.cmd",
    dataDir: join(root, "free", "data"),
    accounts: [
      { id: "free-account", label: "Free", codexHome: "free-home" },
      { id: "history", label: "History", codexHome: "history-home", historyOnly: true },
    ],
  };
  let freeLoginStarts = 0;
  const freeAttempts = await createAccountLoginAttempts(freeConfig, join(root, "free-config.json"), {
    assertCanAddExecutionAccount: () => assertCanAddExecutionAccount({
      accounts: freeConfig.accounts,
      entitlement: { tier: "free", maxAccounts: 1 },
    }),
    startLogin: async () => { freeLoginStarts += 1; },
  });
  await assert.rejects(freeAttempts.start(), (error) => error?.status === 402 && error?.code === "pro_required");
  assert.equal(freeLoginStarts, 0);

  const proConfig = {
    codexCliPath: "codex.cmd",
    dataDir: join(root, "pro", "data"),
    accounts: Array.from({ length: 4 }, (_, index) => ({ id: `pro-${index}`, label: "Pro", codexHome: `home-${index}` })),
  };
  const proAttempts = await createAccountLoginAttempts(proConfig, join(root, "pro-config.json"), {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    assertCanAddExecutionAccount: () => assertCanAddExecutionAccount({
      accounts: proConfig.accounts,
      entitlement: { tier: "pro", maxAccounts: 5 },
    }),
    startLogin: async () => ({
      loginId: "login-1",
      verificationUrl: "https://example.com/device",
      userCode: "ABCD-EFGH",
      stop() {},
    }),
    scheduleExpiry: () => 1,
    cancelExpiry() {},
  });
  const allowed = await proAttempts.start();
  await proAttempts.cancel(allowed.attemptId);
  proConfig.accounts.push({ id: "pro-5", label: "Pro", codexHome: "home-5" });
  await assert.rejects(proAttempts.start(), (error) => error?.status === 409 && error?.code === "account_limit_reached");
});

test("new account login stays out of config until authentication succeeds", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const configPath = join(root, "config.local.json");
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  await writeFile(configPath, JSON.stringify(config));
  let draftHome;
  const attached = [];

  const attempts = await createAccountLoginAttempts(config, configPath, {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    startLogin: async ({ codexHome }) => {
      draftHome = codexHome;
      return {
        loginId: "login-1",
        verificationUrl: "https://example.com/device",
        userCode: "ABCD-EFGH",
        stop() {}
      };
    },
    requester: async () => ({ account: { type: "chatgpt", email: "person@example.com", planType: "plus" } }),
    tokenUsageStore: { attach: async (...args) => attached.push(args) },
    scheduleExpiry: () => 1,
    cancelExpiry() {}
  });

  const started = await attempts.start();
  assert.deepEqual(config.accounts, []);
  assert.deepEqual(JSON.parse(await readFile(configPath, "utf8")).accounts, []);
  assert.equal(Object.hasOwn(started, "codexHome"), false);

  assert.deepEqual(await attempts.complete(started.attemptId), { connected: false, accounts: [] });
  await writeFile(join(draftHome, "auth.json"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));

  const completed = await attempts.complete(started.attemptId);
  assert.equal(completed.connected, true);
  assert.equal(completed.account.email, "person@example.com");
  assert.deepEqual(config.accounts.map((account) => account.id), ["new-account-abcdef12"]);
  assert.deepEqual(attached, [["new-account-abcdef12", "person@example.com"]]);
  assert.equal(JSON.stringify(completed).includes(draftHome), false);
});

test("canceling or expiring an unfinished login stops it and removes only its draft home", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  let stopped = 0;
  let expire;
  const attempts = await createAccountLoginAttempts(config, join(root, "config.local.json"), {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    startLogin: async () => ({
      loginId: "login-1",
      verificationUrl: "https://example.com/device",
      userCode: "ABCD-EFGH",
      stop() { stopped += 1; }
    }),
    scheduleExpiry: (callback) => { expire = callback; return 1; },
    cancelExpiry() {}
  });

  const first = await attempts.start();
  assert.deepEqual(await attempts.cancel(first.attemptId), { canceled: true });
  assert.equal(stopped, 1);
  assert.deepEqual(config.accounts, []);

  await attempts.start();
  await expire();
  assert.equal(stopped, 2);
  assert.deepEqual(config.accounts, []);
});

test("startup continues when Windows still locks a stale login draft", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  let warnings = 0;

  const attempts = await createAccountLoginAttempts(config, join(root, "config.local.json"), {
    removeDirectory: async () => {
      const error = new Error("directory_locked");
      error.code = "EBUSY";
      throw error;
    },
    makeDirectory: async () => {},
    logger: { warn() { warnings += 1; } }
  });

  assert.equal(typeof attempts.start, "function");
  assert.equal(warnings, 1);
});

test("repeated starts reuse the active device code until it is canceled", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  let loginStarts = 0;
  let uuidCalls = 0;
  let expiryDelay;
  const attempts = await createAccountLoginAttempts(config, join(root, "config.local.json"), {
    randomUUID: () => `${++uuidCalls}`.padStart(32, "a"),
    startLogin: async () => {
      loginStarts += 1;
      return {
        loginId: `login-${loginStarts}`,
        verificationUrl: "https://example.com/device",
        userCode: `CODE-${loginStarts}`,
        stop() {}
      };
    },
    scheduleExpiry: (_callback, delay) => { expiryDelay = delay; return 1; },
    cancelExpiry() {}
  });

  const [first, repeated, concurrent] = await Promise.all([
    attempts.start(),
    attempts.start(),
    attempts.start()
  ]);

  assert.deepEqual(repeated, first);
  assert.deepEqual(concurrent, first);
  assert.equal(loginStarts, 1);
  assert.equal(uuidCalls, 1);
  assert.equal(expiryDelay, 3 * 60 * 1000);

  await attempts.cancel(first.attemptId);
  const replacement = await attempts.start();
  assert.notEqual(replacement.attemptId, first.attemptId);
  assert.equal(loginStarts, 2);
  assert.equal(uuidCalls, 2);
});

test("a Windows-locked authenticated draft is copied into its final account home", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const configPath = join(root, "config.local.json");
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  await writeFile(configPath, JSON.stringify(config));
  let draftHome;
  const attempts = await createAccountLoginAttempts(config, configPath, {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    startLogin: async ({ codexHome }) => {
      draftHome = codexHome;
      return { loginId: "login-1", verificationUrl: "https://example.com/device", userCode: "ABCD-EFGH", stop() {} };
    },
    requester: async () => ({ account: { type: "chatgpt", email: "person@example.com", planType: "plus" } }),
    moveDirectory: async () => {
      const error = new Error("directory_locked");
      error.code = "EPERM";
      throw error;
    },
    scheduleExpiry: () => 1,
    cancelExpiry() {}
  });

  const started = await attempts.start();
  await writeFile(join(draftHome, "auth.json"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));
  const [completed, repeatedCompletion] = await Promise.all([
    attempts.complete(started.attemptId),
    attempts.complete(started.attemptId)
  ]);

  assert.equal(completed.connected, true);
  assert.deepEqual(repeatedCompletion, completed);
  assert.equal(config.accounts.length, 1);
  assert.equal(await readFile(join(config.accounts[0].codexHome, "auth.json"), "utf8"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));
});

test("logging into an existing identity discards the draft without creating another account", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const existingHome = join(root, "existing-home");
  await writeFile(join(root, "config.local.json"), JSON.stringify({ accounts: [] }));
  await mkdir(existingHome, { recursive: true });
  await writeFile(join(existingHome, "auth.json"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));
  const config = {
    codexCliPath: "codex.cmd",
    dataDir: join(root, "data"),
    accounts: [{ id: "desktop-codex", label: "Desktop Codex", codexHome: existingHome }]
  };
  let draftHome;
  let capacityChecks = 0;
  const attempts = await createAccountLoginAttempts(config, join(root, "config.local.json"), {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    startLogin: async ({ codexHome }) => {
      draftHome = codexHome;
      return { loginId: "login-1", verificationUrl: "https://example.com/device", userCode: "ABCD-EFGH", stop() {} };
    },
    requester: async () => ({ account: { type: "chatgpt", email: "person@example.com", planType: "plus" } }),
    assertCanAddExecutionAccount: () => {
      capacityChecks += 1;
      if (capacityChecks > 1) throw new Error("duplicate_consumed_a_slot");
    },
    scheduleExpiry: () => 1,
    cancelExpiry() {}
  });

  const started = await attempts.start();
  await writeFile(join(draftHome, "auth.json"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));
  const completed = await attempts.complete(started.attemptId);

  assert.deepEqual(completed, {
    connected: true,
    deduplicated: true,
    existingAccountId: "desktop-codex",
    accounts: [{ id: "desktop-codex", label: "Desktop Codex", email: "person@example.com", displayName: "person@example.com", planType: "plus", status: "logged_in" }]
  });
  assert.deepEqual(config.accounts.map((account) => account.id), ["desktop-codex"]);
  assert.equal(capacityChecks, 1);
  await assert.rejects(attempts.complete(started.attemptId), (error) => error.message === "account_login_attempt_not_found");
});

test("a failed promotion remains scheduled for draft cleanup", async () => {
  const root = await mkdtemp(join(tmpdir(), "crm-account-attempt-"));
  const config = { codexCliPath: "codex.cmd", dataDir: join(root, "data"), accounts: [] };
  let draftHome;
  let nextTimer = 0;
  const timers = new Map();
  const attempts = await createAccountLoginAttempts(config, join(root, "config.local.json"), {
    randomUUID: () => "abcdef12-3456-7890-abcd-ef1234567890",
    startLogin: async ({ codexHome }) => {
      draftHome = codexHome;
      return { loginId: "login-1", verificationUrl: "https://example.com/device", userCode: "ABCD-EFGH", stop() {} };
    },
    requester: async () => ({ account: { type: "chatgpt", email: "person@example.com", planType: "plus" } }),
    moveDirectory: async () => { throw new Error("move_failed"); },
    scheduleExpiry: (callback) => { const id = ++nextTimer; timers.set(id, callback); return id; },
    cancelExpiry: (id) => timers.delete(id)
  });

  const started = await attempts.start();
  await writeFile(join(draftHome, "auth.json"), JSON.stringify({ OPENAI_ACCOUNT_EMAIL: "person@example.com" }));
  await assert.rejects(attempts.complete(started.attemptId), /move_failed/);

  assert.equal(timers.size, 1);
  await [...timers.values()][0]();
  assert.deepEqual(config.accounts, []);
});
