import test from "node:test";
import assert from "node:assert/strict";
import { parseEnvironment } from "../src/config/environment.ts";
import { hashPassword, passwordHashNeedsUpgrade, verifyPassword } from "../src/modules/identity/passwords.ts";
import { SessionService, verifyCsrfToken, type SessionRecord, type SessionRepository } from "../src/modules/identity/sessions.ts";
import { normalizeDisplayName, normalizeEmail } from "../src/modules/identity/users.ts";

class MemorySessions implements SessionRepository {
  records = new Map<string, SessionRecord>();
  async insert(record: SessionRecord): Promise<void> { this.records.set(record.tokenHash, record); }
  async findByTokenHash(hash: string): Promise<SessionRecord | null> { return this.records.get(hash) ?? null; }
  async revoke(id: string, now: Date): Promise<void> {
    for (const record of this.records.values()) if (record.id === id) record.revokedAt = now;
  }
}

test("password hashes are salted, verifiable and do not expose plaintext", async () => {
  const first = await hashPassword("correct horse battery staple");
  const second = await hashPassword("correct horse battery staple");
  assert.notEqual(first, second);
  assert.equal(first.includes("correct horse"), false);
  assert.equal(await verifyPassword("correct horse battery staple", first), true);
  assert.equal(await verifyPassword("incorrect password", first), false);
  assert.equal(passwordHashNeedsUpgrade(first), false);
});

test("malformed password hashes fail closed", async () => {
  assert.equal(await verifyPassword("anything at all", "scrypt$999999999$8$1$x$y"), false);
  assert.equal(await verifyPassword("anything at all", "legacy-hash"), false);
});

test("sessions use opaque tokens, expire, rotate and revoke the old session", async () => {
  const repository = new MemorySessions();
  const service = new SessionService(repository, 1_000);
  const start = new Date("2026-09-13T00:00:00Z");
  const issued = await service.issue("user-1", start);
  assert.equal(issued.session.tokenHash.includes(issued.token), false);
  assert.equal((await service.authenticate(issued.token, new Date(start.getTime() + 500))).userId, "user-1");
  const rotated = await service.rotate(issued.token, new Date(start.getTime() + 600));
  await assert.rejects(service.authenticate(issued.token, new Date(start.getTime() + 700)), { code: "UNAUTHENTICATED" });
  assert.equal((await service.authenticate(rotated.token, new Date(start.getTime() + 700))).userId, "user-1");
  await assert.rejects(service.authenticate(rotated.token, new Date(start.getTime() + 1_601)), { code: "UNAUTHENTICATED" });
});

test("CSRF tokens require constant-time comparable exact values", () => {
  assert.doesNotThrow(() => verifyCsrfToken("fixed-secret", "fixed-secret"));
  assert.throws(() => verifyCsrfToken("wrong", "fixed-secret"), { code: "CSRF_INVALID" });
  assert.throws(() => verifyCsrfToken("", ""), { code: "CSRF_INVALID" });
});

test("user input is normalized and bounded", () => {
  assert.equal(normalizeEmail("  OWNER@Crakey.COM "), "owner@crakey.com");
  assert.equal(normalizeDisplayName(" Jay-ar   Crakey "), "Jay-ar Crakey");
  assert.throws(() => normalizeEmail("not-an-email"), { code: "INVALID_EMAIL" });
});

test("production configuration fails closed", () => {
  assert.throws(() => parseEnvironment({ NODE_ENV: "production", APP_ORIGIN: "http://crakey.com", SESSION_SECRET: "x".repeat(40), KYC_ENCRYPTION_KEY: "y".repeat(40) }), { code: "UNSAFE_PRODUCTION_ENV" });
  assert.throws(() => parseEnvironment({ NODE_ENV: "production", APP_ORIGIN: "https://crakey.com", SESSION_SECRET: "x".repeat(40), KYC_ENCRYPTION_KEY: "y".repeat(40), PAYPAL_MODE: "live" }), { code: "UNSAFE_PRODUCTION_ENV" });
  const safe = parseEnvironment({ NODE_ENV: "production", APP_ORIGIN: "https://crakey.com", SESSION_SECRET: "x".repeat(40), KYC_ENCRYPTION_KEY: "y".repeat(40), PAYPAL_MODE: "disabled" });
  assert.equal(safe.paypalMode, "disabled");
});
