import test from "node:test";
import assert from "node:assert/strict";
import { AppError, AuthService, CatalogService, FakeClock, MemAudit, MemEmail, MemProducts, MemRateLimiter, MemSearchIndex, MemSessions, MemTokens, MemUsers, SeqIds, base32Decode, base32Encode, buildSearchBody, decryptSecret, defaultAuthConfig, encryptSecret, facetSourceFromAggs, hashPassword, hotp, needsRehash, signJwt, totpCounter, verifyJwt, verifyPassword, verifyTotp } from "../src/core";
import { blank, filterModelFrom, fromQuery } from "@sheikbras/domain";

const FAST = { N: 1024, r: 8, p: 1 };
const KEY = Buffer.alloc(32, 7).toString("base64");
const JWT = { secrets: { k1: "segredo-um-bem-longo-0123456789", k2: "segredo-dois-bem-longo-0123456789" }, kid: "k1", issuer: "sheikbras", audience: "sheikbras-app" };
const code = async (p: Promise<unknown>) => { try { await p; return "ok"; } catch (e) { return e instanceof AppError ? e.code : "ERRO:" + (e as Error).message; } };
function setup() {
  const clock = new FakeClock(), d = { users: new MemUsers(), sessions: new MemSessions(), limiter: new MemRateLimiter(clock), audit: new MemAudit(), clock, ids: new SeqIds(), tokens: new MemTokens(), email: new MemEmail() };
  const cfg = defaultAuthConfig({ jwt: JWT, pepper: "pimenta", encKeyB64: KEY, hash: FAST });
  return { d, clock, auth: new AuthService(d, cfg) };
}
const NEW = { nome: "Maria Souza", email: "Maria@Email.com", senha: "Senha1234!", aceite: true };
const secretOf = (d: ReturnType<typeof setup>["d"], id: string) => decryptSecret(d.users.m.get(id)!.mfa!.secretEnc, KEY);

test("senha: hash com scrypt, sal único, verificação e pepper", async () => {
  const a = await hashPassword("Abc12345", "p", FAST), b = await hashPassword("Abc12345", "p", FAST);
  assert.notEqual(a, b); assert.ok(a.startsWith("scrypt$1024$8$1$"));
  assert.equal(await verifyPassword("Abc12345", a, "p"), true); assert.equal(await verifyPassword("abc12345", a, "p"), false); assert.equal(await verifyPassword("Abc12345", a, "outro"), false);
  assert.equal(needsRehash(a), true);
});
test("segredo em repouso: AES-GCM ida e volta e detecta adulteração", () => {
  const c = encryptSecret("JBSWY3DPEHPK3PXP", KEY); assert.equal(decryptSecret(c, KEY), "JBSWY3DPEHPK3PXP");
  const [iv, enc, tag] = c.split("."); assert.throws(() => decryptSecret([iv, enc!.slice(0, -2) + "AA", tag].join("."), KEY));
});
test("JWT: válido, expirado, chave errada, alg none e rotação de chave", () => {
  const now = Date.parse("2026-10-09T12:00:00Z"), t = signJwt({ sub: "u1", role: "customer", sid: "s1" }, JWT, 900, now);
  assert.equal(verifyJwt(t, JWT, now + 1000).sub, "u1");
  assert.throws(() => verifyJwt(t, JWT, now + 901_000)); assert.throws(() => verifyJwt(t, { ...JWT, audience: "x" }, now));
  assert.throws(() => verifyJwt(t.slice(0, -2) + "xx", JWT, now));
  const none = Buffer.from(JSON.stringify({ alg: "none", kid: "k1" })).toString("base64url") + "." + t.split(".")[1] + ".";
  assert.throws(() => verifyJwt(none, JWT, now));
  const rotated = { ...JWT, kid: "k2" }; assert.equal(verifyJwt(t, rotated, now).sub, "u1"); // token antigo (k1) ainda vale após rotacionar
});
test("TOTP: vetores do RFC 6238 e janela de ±1", () => {
  const s = Buffer.from("12345678901234567890");
  assert.equal(hotp(s, Math.floor(59 / 30)), "287082"); assert.equal(hotp(s, Math.floor(1111111109 / 30)), "081804"); assert.equal(hotp(s, Math.floor(1234567890 / 30)), "005924");
  const b32 = base32Encode(s); assert.equal(base32Decode(b32).toString(), "12345678901234567890");
  assert.equal(verifyTotp(b32, "287082", 59_000), 1); assert.equal(verifyTotp(b32, "287082", 59_000 + 30_000), 1); assert.equal(verifyTotp(b32, "287082", 59_000 + 90_000), null); assert.equal(verifyTotp(b32, "12ab", 59_000), null);
});
test("cadastro: não expõe hash, normaliza e-mail, bloqueia duplicado e senha fraca", async () => {
  const { auth, d } = setup(); const r = await auth.register(NEW, { ip: "1.1.1.1" });
  assert.equal(r.user.email, "maria@email.com"); assert.ok(!JSON.stringify(r).includes("scrypt")); assert.equal(r.user.role, "customer"); assert.ok(r.tokens.accessToken && r.tokens.refreshToken);
  assert.equal(d.users.m.get(r.user.id)!.consent.termsVersion, "2026-10");
  assert.equal(await code(auth.register({ ...NEW, email: "maria@email.com" })), "EMAIL_TAKEN");
  assert.equal(await code(auth.register({ ...NEW, email: "x@y.com", senha: "abc" })), "VALIDATION");
  assert.equal(await code(auth.register({ ...NEW, email: "z@y.com", aceite: false })), "VALIDATION");
});
test("login: erro igual para conta inexistente e senha errada; bloqueio após 5 falhas", async () => {
  const { auth } = setup(); await auth.register(NEW);
  assert.equal(await code(auth.login({ email: "naoexiste@x.com", senha: "Senha1234!" })), "INVALID_CREDENTIALS");
  const e1 = await auth.login({ email: "maria@email.com", senha: "errada123" }).catch((e) => e), e2 = await auth.login({ email: "naoexiste@x.com", senha: "errada123" }).catch((e) => e);
  assert.equal(e1.message, e2.message);
  for (let i = 0; i < 4; i++) await code(auth.login({ email: "maria@email.com", senha: "errada123" })); // com a tentativa acima, são 5 falhas
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!" })), "RATE_LIMITED"); // a 6ª tentativa na janela é bloqueada, mesmo com a senha certa
});
test("login ok zera o contador da conta", async () => {
  const { auth } = setup(); await auth.register(NEW);
  for (let i = 0; i < 3; i++) await code(auth.login({ email: "maria@email.com", senha: "errada123" }));
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!" })), "ok");
  for (let i = 0; i < 4; i++) assert.equal(await code(auth.login({ email: "maria@email.com", senha: "errada123" })), "INVALID_CREDENTIALS");
});
test("refresh: rotação, reuso derruba a família, logout invalida o access token", async () => {
  const { auth, clock } = setup(); const { tokens: t1 } = await auth.register(NEW);
  const t2 = await auth.refresh(t1.refreshToken); assert.notEqual(t2.refreshToken, t1.refreshToken);
  assert.equal(await code(auth.refresh(t1.refreshToken)), "TOKEN_REUSED"); // reuso do antigo
  assert.equal(await code(auth.refresh(t2.refreshToken)), "TOKEN_REUSED"); // família inteira revogada
  const s = setup(); const r = await s.auth.register(NEW); assert.equal((await s.auth.authenticate(r.tokens.accessToken)).role, "customer");
  await s.auth.logout(r.tokens.refreshToken); assert.equal(await code(s.auth.authenticate(r.tokens.accessToken)), "UNAUTHORIZED");
  const x = setup(); const rr = await x.auth.register(NEW); x.clock.advance(60 * 60 * 24 * 31); assert.equal(await code(x.auth.refresh(rr.tokens.refreshToken)), "UNAUTHORIZED"); void clock;
});
test("verificação em duas etapas: configurar, exigir código, bloquear reuso, código de recuperação", async () => {
  const { auth, d, clock } = setup(); const r = await auth.register(NEW); const id = r.user.id;
  const { secret, otpauthUrl } = await auth.setupMfa(id); assert.ok(otpauthUrl.startsWith("otpauth://totp/")); assert.equal(secretOf(d, id), secret);
  const codeNow = () => hotp(base32Decode(secret), totpCounter(clock.now().getTime()));
  assert.equal(await code(auth.confirmMfa(id, "000000")), "MFA_INVALID");
  const { recoveryCodes } = await auth.confirmMfa(id, codeNow()); assert.equal(recoveryCodes.length, 8); assert.equal((await auth.me(id)).mfaEnabled, true);
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!" })), "MFA_REQUIRED");
  clock.advance(30); const c1 = codeNow(); assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!", mfaCode: c1 })), "ok");
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!", mfaCode: c1 })), "MFA_INVALID"); // mesmo código não vale duas vezes
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!", mfaCode: recoveryCodes[0]! })), "ok");
  assert.equal(await code(auth.login({ email: "maria@email.com", senha: "Senha1234!", mfaCode: recoveryCodes[0]! })), "MFA_INVALID"); // recuperação é de uso único
});
/* ---------------- catálogo ---------------- */
function catalog() { const clock = new FakeClock(), index = new MemSearchIndex(); return { svc: new CatalogService({ products: new MemProducts(), index, clock }), index }; }
const seller = { userId: "s1", role: "seller" as const }, other = { userId: "s2", role: "seller" as const }, admin = { userId: "a1", role: "admin" as const }, customer = { userId: "c1", role: "customer" as const };
const PHONE = { title: "Smartphone 6,7 pol. 256 GB 5G", priceCents: 189900, originalPriceCents: 229900, installments: 12, category: "Celulares" as const, brand: "Vento", freeShipping: true, eta: "chega amanhã", stock: 5, attrs: { ram: 8, storage: 256, tela: 6.7, g5: true } };
test("catálogo: permissões, validação por categoria e ciclo rascunho → ativo → arquivado", async () => {
  const { svc, index } = catalog();
  assert.equal(await code(svc.create(customer, PHONE)), "FORBIDDEN");
  const err = await svc.create(seller, { ...PHONE, attrs: { ram: "oito", storage: 256 } as never }).catch((e) => e); assert.equal(err.code, "VALIDATION"); assert.ok(err.details["attrs.ram"] && err.details["attrs.tela"] && err.details["attrs.g5"]);
  assert.equal(await code(svc.create(seller, { ...PHONE, originalPriceCents: 100 })), "VALIDATION");
  const p = await svc.create(seller, PHONE); assert.equal(p.status, "draft"); assert.equal(p.brand, "vento"); assert.equal(index.m.size, 0);
  assert.equal(await code(svc.get(null, p.id)), "NOT_FOUND"); assert.equal((await svc.get(seller, p.id)).id, p.id);
  assert.equal(await code(svc.update(other, p.id, { priceCents: 1 })), "NOT_FOUND"); assert.equal((await svc.update(admin, p.id, { priceCents: 179900 })).priceCents, 179900);
  assert.equal(await code(svc.publish(seller, p.id)), "ok"); assert.equal(index.m.size, 1); assert.equal((await svc.get(null, p.id)).status, "active");
  assert.equal((await svc.update(seller, p.id, { priceCents: 169900 })).priceCents, 169900); assert.equal(index.m.get(p.id)!.priceCents, 169900);
  await svc.archive(seller, p.id); assert.equal(index.m.size, 0); assert.equal(await code(svc.get(null, p.id)), "NOT_FOUND");
});
test("catálogo: não publica sem estoque", async () => { const { svc } = catalog(); const p = await svc.create(seller, { ...PHONE, stock: 0 }); assert.equal(await code(svc.publish(seller, p.id)), "VALIDATION"); });
test("busca no índice reaproveita a lógica do domínio (intenção, filtros e facetas)", async () => {
  const { svc } = catalog(); for (const [brand, storage] of [["Vento", 256], ["Oca", 128], ["Vento", 128]] as const) { const p = await svc.create(seller, { ...PHONE, brand, attrs: { ...PHONE.attrs, storage } }); await svc.publish(seller, p.id); }
  const draft = await svc.create(seller, PHONE); void draft;
  const all = await svc.search({ ...blank(), c: "Celulares" }); assert.equal(all.total, 3);
  const r = await svc.search(fromQuery("celular vento 256gb")); assert.equal(r.total, 1);
  const brandGroup = all.filters.find((g) => g.id === "brand")!; assert.ok(brandGroup.type === "check"); assert.deepEqual((brandGroup as { options: { value: string; count?: number }[] }).options.map((o) => [o.value, o.count]), [["vento", 2], ["oca", 1]]);
});
test("OpenSearch: consulta com post_filter e agregações que ignoram o próprio filtro", () => {
  const s = { ...fromQuery("celular vento 256gb"), sort: "pa" as const }; const b = buildSearchBody(s) as any;
  assert.deepEqual(b.query.bool.filter[1], { term: { cat: "Celulares" } }); assert.equal(b.query.bool.must.length, 1); assert.equal(b.query.bool.must[0].multi_match.query, "smartphone");
  const post = JSON.stringify(b.post_filter); assert.ok(post.includes('"brand":["vento"]') && post.includes('"attrs.storage":[256]'));
  assert.ok(!JSON.stringify(b.aggs.brand.filter).includes('"brand":["vento"]')); assert.ok(JSON.stringify(b.aggs.brand.filter).includes("attrs.storage"));
  assert.ok(!JSON.stringify(b.aggs["a:storage"].filter).includes("attrs.storage")); assert.deepEqual(b.sort, [{ priceCents: "asc" }]);
  assert.ok(b.aggs["a:ram"] && b.aggs["a:g5"]);
  const f = facetSourceFromAggs({ hits: { total: { value: 0 }, hits: [] }, aggregations: { brand: { v: { buckets: [{ key: "vento", doc_count: 2 }] } }, "a:g5": { v: { buckets: [{ key: 1, key_as_string: "true", doc_count: 3 }] } }, cat: { v: { buckets: [] } } } });
  const g = filterModelFrom({ ...blank(), c: "Celulares" }, f); assert.deepEqual((g.find((x) => x.id === "ag5") as any).options[0], { field: "a_g5", value: "true", label: "Sim", count: 3, checked: false });
});
