import { CredentialsCryptoService } from "./credentials-crypto.service";

function createService(keysEnvValue: string, useSingular = false) {
  const configService = {
    get: (key: string) => {
      if (useSingular && key === "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY") {
        return keysEnvValue;
      }
      if (!useSingular && key === "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEYS") {
        return keysEnvValue;
      }
      return undefined;
    },
  } as any;

  const service = new CredentialsCryptoService(configService);
  service.onModuleInit();
  return service;
}

describe("CredentialsCryptoService", () => {
  it("cifra y descifra ida y vuelta correctamente", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const plaintext = "Polaris$202521";

    const encrypted = service.encrypt(plaintext);
    expect(encrypted).not.toContain(plaintext);

    const decrypted = service.decrypt(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  it("genera un IV distinto en cada operacion, incluso para el mismo texto", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const plaintext = "misma-contraseña";

    const first = service.encrypt(plaintext);
    const second = service.encrypt(plaintext);

    expect(first).not.toBe(second);

    // El IV son los bytes [2, 2+12) del blob decodificado (formatVersion +
    // keyVersion = 2 bytes de cabecera antes del IV).
    const ivOf = (blob: string) => Buffer.from(blob, "base64").subarray(2, 14);
    expect(ivOf(first).equals(ivOf(second))).toBe(false);

    // Ambos igual siguen descifrando al mismo texto.
    expect(service.decrypt(first)).toBe(plaintext);
    expect(service.decrypt(second)).toBe(plaintext);
  });

  it("el IV y el auth tag quedan guardados junto con el cifrado, no se pierden", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const encrypted = service.encrypt("dato");
    const raw = Buffer.from(encrypted, "base64");

    // formatVersion(1) + keyVersion(1) + iv(12) + authTag(16) + al menos 1
    // byte de ciphertext
    expect(raw.length).toBeGreaterThanOrEqual(2 + 12 + 16 + 1);
  });

  it("rechaza un blob alterado — el auth tag de GCM detecta la manipulacion", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const encrypted = service.encrypt("Polaris$202521");

    const raw = Buffer.from(encrypted, "base64");
    // Alteramos un byte del ciphertext (despues de version+iv+authTag).
    raw[raw.length - 1] = raw[raw.length - 1] ^ 0xff;
    const tampered = raw.toString("base64");

    expect(() => service.decrypt(tampered)).toThrow();
  });

  it("rechaza un blob con el auth tag alterado directamente", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const encrypted = service.encrypt("Polaris$202521");

    const raw = Buffer.from(encrypted, "base64");
    // El auth tag son los bytes [14, 30) (2 bytes de cabecera + 12 de IV).
    raw[14] = raw[14] ^ 0xff;
    const tampered = raw.toString("base64");

    expect(() => service.decrypt(tampered)).toThrow();
  });

  it("rechaza un blob con una version de formato desconocida", () => {
    const service = createService("llave-de-prueba-suficientemente-larga");
    const encrypted = service.encrypt("dato");

    const raw = Buffer.from(encrypted, "base64");
    raw[0] = 99; // formatVersion inexistente
    const tampered = raw.toString("base64");

    expect(() => service.decrypt(tampered)).toThrow(
      /Version de formato de cifrado no soportada/,
    );
  });

  it("acepta CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY (singular) como lista de una sola llave", () => {
    const service = createService("llave-unica-de-prueba-larga", true);
    const encrypted = service.encrypt("valor");
    expect(service.decrypt(encrypted)).toBe("valor");
  });

  it("ROTACION: lo cifrado con la llave vieja sigue descifrando despues de rotar", () => {
    const OLD_KEY = "llave-vieja-de-produccion-2026";
    const NEW_KEY = "llave-nueva-rotada-2027";

    // Estado antes de rotar: solo la llave vieja.
    const before = createService(OLD_KEY);
    const encryptedWithOldKey = before.encrypt("Polaris$202521");

    // Rotacion: se agrega la llave nueva AL FINAL, la vieja se conserva.
    const after = createService(`${OLD_KEY},${NEW_KEY}`);

    // Lo cifrado con la llave vieja (version 0) sigue descifrando.
    expect(after.decrypt(encryptedWithOldKey)).toBe("Polaris$202521");

    // Lo nuevo se cifra con la llave activa (la ultima = keyVersion 1).
    // El formatVersion (byte 0) no cambia por rotar llaves — son cosas
    // independientes.
    const encryptedWithNewKey = after.encrypt("otra-contraseña");
    const rawNew = Buffer.from(encryptedWithNewKey, "base64");
    expect(rawNew[0]).toBe(1); // formatVersion, sin cambios
    expect(rawNew[1]).toBe(1); // keyVersion, la llave nueva

    const rawOld = Buffer.from(encryptedWithOldKey, "base64");
    expect(rawOld[0]).toBe(1); // mismo formatVersion
    expect(rawOld[1]).toBe(0); // keyVersion 0, la llave vieja

    // Un proceso que todavia no cargo la llave nueva (rollback a mitad de
    // rotacion) no puede descifrar lo cifrado con la llave nueva — y el
    // error dice por que, no es un fallo silencioso.
    expect(() => before.decrypt(encryptedWithNewKey)).toThrow(
      /No hay llave cargada para keyVersion/,
    );
  });

  it("falla al iniciar si no hay ninguna llave configurada", () => {
    const configService = { get: () => undefined } as any;
    const service = new CredentialsCryptoService(configService);
    expect(() => service.onModuleInit()).toThrow(
      /CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY/,
    );
  });

  it("fingerprint() no revela la llave y es estable para la misma llave", () => {
    const a = createService("llave-de-prueba-suficientemente-larga");
    const b = createService("llave-de-prueba-suficientemente-larga");
    const c = createService("otra-llave-completamente-distinta");

    expect(a.fingerprint()).toBe(b.fingerprint());
    expect(a.fingerprint()).not.toBe(c.fingerprint());
    expect(a.fingerprint()).not.toContain("llave-de-prueba-suficientemente-larga");
  });
});
