import { describe, expect, it } from "vitest";
import { hashPassword, verifyPassword } from "../src/lib/passwords.js";
import { generateInviteCode, sha256 } from "../src/lib/helpers.js";
import { parse, ApiError } from "../src/lib/errors.js";
import { registerInputSchema, confirmScanInputSchema } from "@app/validation";

describe("lösenordshantering (scrypt)", () => {
  it("hashar och verifierar", async () => {
    const hash = await hashPassword("korrekt häst batteri 99");
    expect(hash.startsWith("scrypt$")).toBe(true);
    expect(await verifyPassword("korrekt häst batteri 99", hash)).toBe(true);
    expect(await verifyPassword("fel lösenord", hash)).toBe(false);
  });
  it("unika salter ger olika hashar", async () => {
    const a = await hashPassword("samma lösenord!");
    const b = await hashPassword("samma lösenord!");
    expect(a).not.toBe(b);
    expect(await verifyPassword("samma lösenord!", a)).toBe(true);
    expect(await verifyPassword("samma lösenord!", b)).toBe(true);
  });
  it("hanterar trasig lagrad hash utan att kasta", async () => {
    expect(await verifyPassword("x", "inte-en-hash")).toBe(false);
    expect(await verifyPassword("x", "scrypt$abc$8$1$AA$BB")).toBe(false);
  });
});

describe("hjälpare", () => {
  it("inbjudningskoder: 8 tecken utan förväxlingsbara", () => {
    for (let i = 0; i < 20; i++) {
      const code = generateInviteCode();
      expect(code).toHaveLength(8);
      expect(code).not.toMatch(/[01IO]/);
    }
  });
  it("sha256 är deterministisk", () => {
    expect(sha256("abc")).toBe(sha256("abc"));
    expect(sha256("abc")).toHaveLength(64);
  });
});

describe("validering via parse()", () => {
  it("registrering: normaliserar e-post, kräver 10 teckens lösenord", () => {
    const result = parse(registerInputSchema, {
      email: "  Johan@Example.COM",
      password: "supersäkert lösen",
      displayName: "Johan",
    });
    expect(result.email).toBe("johan@example.com");
    expect(() =>
      parse(registerInputSchema, { email: "a@b.se", password: "kort", displayName: "X" }),
    ).toThrowError(ApiError);
  });
  it("scan-bekräftelse: avvisar ogiltiga enheter", () => {
    expect(() =>
      parse(confirmScanInputSchema, {
        items: [{ action: "accept", displayName: "Mjölk", quantity: 1, unit: "gallon" }],
      }),
    ).toThrowError(ApiError);
  });
  it("ApiError bär status och kod", () => {
    try {
      parse(registerInputSchema, {});
    } catch (err) {
      expect(err).toBeInstanceOf(ApiError);
      expect((err as ApiError).statusCode).toBe(400);
      expect((err as ApiError).code).toBe("VALIDATION_ERROR");
    }
  });
});

describe("lösenordsåterställning (mejlmallar + tokensäkerhet)", () => {
  it("renderar återställningsmejl per språk med brand ur konfig", async () => {
    const { renderMail } = await import("../src/lib/mailer.js");
    const sv = renderMail("auth.password_reset", "sv-SE", {
      email: "a@b.se",
      link: "app://reset-password?token=x",
    });
    const en = renderMail("auth.password_reset", "en-US", {
      email: "a@b.se",
      link: "app://reset-password?token=x",
    });
    expect(sv.subject).toContain("Återställ");
    expect(en.subject).toContain("Reset");
    expect(sv.text).toContain("30 minuter");
    expect(en.text).toContain("30 minutes");
    // Namnet kommer ur brand.config.json – aldrig hårdkodat i mallen.
    const { BRAND } = await import("@app/shared-types");
    expect(sv.subject).toContain(BRAND.name);
  });

  it("alla tolv språk har mallar; okänt språk faller till svenska; okänd mall kastar", async () => {
    const { renderMail } = await import("../src/lib/mailer.js");
    const subjects = {
      "sv-SE": "Återställ",
      "en-US": "Reset",
      "es-ES": "Restablece",
      "it-IT": "Reimposta",
      "de-DE": "Setze",
      "fr-FR": "Réinitialisez",
      "da-DK": "Nulstil",
      "nb-NO": "Tilbakestill",
      "fi-FI": "Nollaa",
      "nl-NL": "wachtwoord opnieuw",
      "pl-PL": "Zresetuj",
      "pt-PT": "Reponha",
    };
    for (const [tag, word] of Object.entries(subjects)) {
      expect(renderMail("auth.password_reset", tag, { email: "x", link: "y" }).subject).toContain(
        word,
      );
    }
    const unknown = renderMail("auth.password_reset", "el-GR", { email: "x", link: "y" });
    expect(unknown.subject).toContain("Återställ"); // ostött språk -> sv
    expect(() => renderMail("finns.inte", "sv", {})).toThrow();
  });

  it("återställningstoken lagras aldrig i klartext (sha256 är envägs)", () => {
    const token = "a".repeat(64);
    const hash = sha256(token);
    expect(hash).not.toBe(token);
    expect(hash).toHaveLength(64);
    expect(sha256(token)).toBe(hash); // deterministisk – uppslag fungerar
    expect(sha256("b".repeat(64))).not.toBe(hash);
  });

  it("log-mailern skickar aldrig på riktigt; okonfigurerad SMTP vägrar högljutt", async () => {
    const { createMailer } = await import("../src/lib/mailer.js");
    await expect(
      createMailer("log").send({ to: "x@y.se", subject: "t", text: "b" }),
    ).resolves.toBeUndefined();
    // smtp utan inställningar/host/avsändare -> tydligt fel direkt (aldrig tyst tappade mejl)
    expect(() => createMailer("smtp")).toThrow(/SMTP/);
    expect(() =>
      createMailer("smtp", {
        host: "",
        port: 587,
        secure: false,
        user: "",
        pass: "",
        from: "x@y.se",
      }),
    ).toThrow(/SMTP_HOST/);
    expect(() =>
      createMailer("smtp", {
        host: "smtp.x.se",
        port: 587,
        secure: false,
        user: "",
        pass: "",
        from: "",
      }),
    ).toThrow(/MAIL_FROM/);
  });
});

describe("TOTP (RFC 6238) – admin-2FA utan externa beroenden", () => {
  it("klarar RFC 6238-testvektorn (SHA1, 6 siffror)", async () => {
    const { base32Encode, totpCode } = await import("../src/lib/totp.js");
    // RFC 6238 bilaga B: secret "12345678901234567890", T=59 s → 6 sista siffrorna av 94287082.
    const secret = base32Encode(Buffer.from("12345678901234567890", "ascii"));
    expect(totpCode(secret, 59_000)).toBe("287082");
    expect(totpCode(secret, 1_111_111_109_000)).toBe("081804");
  });

  it("verifierar med ±1 stegs fönster men inte längre bort", async () => {
    const { generateTotpSecret, totpCode, verifyTotp } = await import("../src/lib/totp.js");
    const secret = generateTotpSecret();
    const now = 1_700_000_000_000;
    const code = totpCode(secret, now);
    expect(verifyTotp(secret, code, now)).not.toBeNull();
    expect(verifyTotp(secret, code, now + 30_000)).not.toBeNull(); // föregående steg ok
    expect(verifyTotp(secret, code, now + 90_000)).toBeNull(); // för gammalt
    expect(verifyTotp(secret, "000000", now)).toBeNull();
    expect(verifyTotp(secret, "12345", now)).toBeNull(); // fel format
  });

  it("base32 rundtur + otpauth-URL utan hårdkodat namn", async () => {
    const { base32Decode, base32Encode, otpauthUrl } = await import("../src/lib/totp.js");
    const buf = Buffer.from("hemlig-nyckel-123", "ascii");
    expect(base32Decode(base32Encode(buf)).equals(buf)).toBe(true);
    const { BRAND } = await import("@app/shared-types");
    const url = otpauthUrl("ABC234", "admin@example.com", `${BRAND.name} Admin`);
    expect(url.startsWith("otpauth://totp/")).toBe(true);
    expect(url).toContain("secret=ABC234");
    expect(url).toContain(encodeURIComponent(BRAND.name));
  });
});
