import { TallerParametrosService } from "./taller-parametros.service";

function createService(overrides: Record<string, any> = {}) {
  const filas = new Map<string, any>();

  const repo = {
    findOne: jest.fn(async ({ where }: any) => {
      const key = `${where.clave}:${where.tiendaId}:${where.destino}`;
      return filas.get(key) ?? null;
    }),
    create: jest.fn((data: any) => data),
    save: jest.fn(async (data: any) => {
      const saved = { id: filas.size + 1, ...data };
      filas.set(`${data.clave}:${data.tiendaId}:${data.destino}`, saved);
      return saved;
    }),
    update: jest.fn(async (id: number, data: any) => {
      for (const [key, fila] of filas.entries()) {
        if (fila.id === id) filas.set(key, { ...fila, ...data });
      }
    }),
    ...overrides.repo,
  };

  // Repo SEPARADO del de arriba a propósito, aunque esta tarea todavía no lo
  // usa (llega en Task 5): repositoryFor() diferencia por entidad desde el
  // principio para que agregar historial más adelante no pise silenciosamente
  // las filas de `repo` -- ambos usan la misma forma de clave interna
  // (clave:tiendaId:destino) y compartir un solo mock los mezclaría.
  const historialRepo = {
    create: jest.fn((data: any) => data),
    save: jest.fn(async (data: any) => ({ id: 1, ...data })),
    ...overrides.historialRepo,
  };

  const tenantAccessor = {
    getCurrentTenantId: jest.fn(() => 7),
    repositoryFor: jest.fn((entity: any) =>
      entity?.name === "TallerParametroHistorial" ? historialRepo : repo,
    ),
    ...overrides.tenantAccessor,
  };

  const credentialsCrypto = {
    encrypt: jest.fn((plain: string) => `ENC(${plain})`),
    decrypt: jest.fn((blob: string) => blob.replace(/^ENC\(|\)$/g, "")),
    ...overrides.credentialsCrypto,
  };

  // Default: mismo default que TenantConfigService.getTogglesOperativos()
  // usa cuando el tenant todavia no tiene fila en tenant_config (todo true,
  // igual que el default del catalogo) -- asi las pruebas de Tasks 3-5 que no
  // conocen el puente de Task 6 siguen viendo el mismo comportamiento
  // efectivo para las 3 claves-puente sin tener que mockear esto una por una.
  const tenantConfigService = {
    getTogglesOperativos: jest.fn(async () => ({
      permiteTiendas: true,
      permiteProductosPropios: true,
      facturaHabilitada: true,
    })),
    ...overrides.tenantConfigService,
  };

  const service = new TallerParametrosService(
    tenantAccessor as any,
    credentialsCrypto as any,
    tenantConfigService as any,
    (overrides.tenantConnectionRegistry as any) ?? (undefined as any), // ver Task 4
  );

  return {
    service,
    repo,
    historialRepo,
    tenantAccessor,
    credentialsCrypto,
    filas,
  };
}

describe("TallerParametrosService — núcleo ambiental", () => {
  it("get() devuelve el default del catálogo cuando no hay fila", async () => {
    const { service } = createService();
    await expect(service.get("facturacion.usa_facturacion")).resolves.toBe(
      true,
    );
    await expect(
      service.get("notificaciones.whatsapp_api_version"),
    ).resolves.toBe("v25.0");
  });

  it("get() castea boolean/number/json correctamente desde texto guardado", async () => {
    const { service } = createService();
    await service.set("productos.permite_crear", false, 1);
    await expect(service.get("productos.permite_crear")).resolves.toBe(false);
  });

  it("getParaTienda() castea number", async () => {
    const { service } = createService();
    await service.setParaTienda(9, "facturacion_pos.precio_decimales", 3, 1);
    await expect(
      service.getParaTienda(9, "facturacion_pos.precio_decimales"),
    ).resolves.toBe(3);
  });

  it("claves sensibles se cifran al escribir y se descifran al leer, nunca en texto plano en `valor`", async () => {
    const { service, filas } = createService();
    await service.setParaTienda(9, "facturacion_pos.password", "secreta123", 1);
    const fila = filas.get("facturacion_pos.password:9:");
    expect(fila.valor).toBeNull();
    expect(fila.valorCifrado).toBe("ENC(secreta123)");
    await expect(
      service.getParaTienda(9, "facturacion_pos.password"),
    ).resolves.toBe("secreta123");
  });

  it("set() invalida la caché del tenant: una lectura posterior refleja el nuevo valor sin volver a golpear el repo si no cambió, y SÍ refleja el cambio tras escribir", async () => {
    const { service, repo } = createService();
    await service.get("facturacion.usa_facturacion"); // primera lectura, popula caché
    await service.get("facturacion.usa_facturacion"); // segunda lectura, debería venir de caché
    // 2, no 1: "facturacion.usa_facturacion" es una clave-puente (Task 6) --
    // la primera lectura, sin fila, hace su propio findOne (1) y además
    // dispara el respaldo fire-and-forget hacia taller_parametros, cuyo
    // escribirFila hace SU findOne para decidir insert-vs-update (2). Lo que
    // esta prueba valida sigue intacto: la SEGUNDA lectura no agrega un
    // tercer findOne porque viene de caché.
    expect(repo.findOne).toHaveBeenCalledTimes(2);

    await service.set("facturacion.usa_facturacion", false, 1);
    await expect(service.get("facturacion.usa_facturacion")).resolves.toBe(
      false,
    );
  });
});

function createRepoFake() {
  const filas = new Map<string, any>();
  return {
    filas,
    findOne: jest.fn(async ({ where }: any) => filas.get(`${where.clave}:${where.tiendaId}:${where.destino}`) ?? null),
    create: jest.fn((d: any) => d),
    save: jest.fn(async (d: any) => {
      const saved = { id: filas.size + 1, ...d };
      filas.set(`${d.clave}:${d.tiendaId}:${d.destino}`, saved);
      return saved;
    }),
    update: jest.fn(async (id: number, data: any) => {
      for (const [k, f] of filas.entries()) if (f.id === id) filas.set(k, { ...f, ...data });
    }),
  };
}

describe("TallerParametrosService — superficie explícita", () => {
  it("getExplicit() usa TenantConnectionRegistry.withTenantDataSourceExplicit, no el contexto ambiental", async () => {
    const parametrosRepo = createRepoFake();
    const historialRepo = createRepoFake();
    const dataSource = {
      getRepository: jest.fn((entity: any) => (entity?.name === "TallerParametroHistorial" ? historialRepo : parametrosRepo)),
    };
    const tenantConnectionRegistry = {
      withTenantDataSourceExplicit: jest.fn(async (_tenantId: number, fn: any) => fn(dataSource)),
      repositoryForDataSource: jest.fn((ds: any, entity: any) => ds.getRepository(entity)),
    };
    const { service, tenantAccessor } = createService({ tenantConnectionRegistry });

    await service.getExplicit(55, "tiendas.permite_crear");

    expect(tenantConnectionRegistry.withTenantDataSourceExplicit).toHaveBeenCalledWith(
      55,
      expect.any(Function),
      undefined,
    );
    expect(tenantAccessor.repositoryFor).not.toHaveBeenCalled();
  });

  it("setExplicit() invalida la caché de ESE tenantId, no la del contexto ambiental", async () => {
    const parametrosRepo = createRepoFake();
    const historialRepo = createRepoFake();
    const dataSource = {
      getRepository: jest.fn((entity: any) => (entity?.name === "TallerParametroHistorial" ? historialRepo : parametrosRepo)),
    };
    const tenantConnectionRegistry = {
      withTenantDataSourceExplicit: jest.fn(async (_tenantId: number, fn: any) => fn(dataSource)),
      repositoryForDataSource: jest.fn((ds: any, entity: any) => ds.getRepository(entity)),
    };
    const { service } = createService({ tenantConnectionRegistry });

    await service.setExplicit(55, "tiendas.permite_crear", false, 1);
    await expect(service.getExplicit(55, "tiendas.permite_crear")).resolves.toBe(false);
  });
});

describe("TallerParametrosService — requeridoSi e historial", () => {
  it("rechaza activar facturacion_pos.habilitada sin los campos dependientes", async () => {
    const { service } = createService();
    await expect(
      service.setGrupoParaTienda(9, { "facturacion_pos.habilitada": true }, 1),
    ).rejects.toThrow(/es obligatorio porque/);
  });

  it("acepta activar facturacion_pos.habilitada cuando el grupo completo viene en la misma escritura", async () => {
    const { service } = createService();
    await expect(
      service.setGrupoParaTienda(
        9,
        {
          "facturacion_pos.habilitada": true,
          "facturacion_pos.url": "http://postouch/create_ordenes_edngt",
          "facturacion_pos.username": "22",
          "facturacion_pos.password": "secreta",
          "facturacion_pos.computadora": "5.5",
          "facturacion_pos.precio_decimales": 2,
        },
        1,
      ),
    ).resolves.toBeUndefined();
  });

  it("acepta reenviar solo un campo (ej. computadora) de una tienda YA configurada, sin repetir password", async () => {
    const { service } = createService();
    await service.setGrupoParaTienda(
      9,
      {
        "facturacion_pos.habilitada": true,
        "facturacion_pos.url": "http://postouch/create_ordenes_edngt",
        "facturacion_pos.username": "22",
        "facturacion_pos.password": "secreta",
        "facturacion_pos.computadora": "5.5",
        "facturacion_pos.precio_decimales": 2,
      },
      1,
    );

    await expect(
      service.setGrupoParaTienda(9, { "facturacion_pos.computadora": "9.1" }, 1),
    ).resolves.toBeUndefined();
    await expect(service.getParaTienda(9, "facturacion_pos.computadora")).resolves.toBe("9.1");
    await expect(service.getParaTienda(9, "facturacion_pos.password")).resolves.toBe("secreta");
  });

  it("desactivar facturacion_pos.habilitada NO exige los demás campos", async () => {
    const { service } = createService();
    await expect(
      service.setGrupoParaTienda(9, { "facturacion_pos.habilitada": false }, 1),
    ).resolves.toBeUndefined();
  });

  it("registra historial con valor anterior/nuevo para una clave NO sensible", async () => {
    const { service, historialRepo } = createService();
    await service.set("productos.permite_crear", false, 1);
    expect(historialRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ clave: "productos.permite_crear", valorAnterior: "true", valorNuevo: "false", usuarioId: 1 }),
    );
  });

  it("NUNCA registra el valor real de una clave sensible en el historial", async () => {
    const { service, historialRepo } = createService();
    await service.setParaTienda(9, "facturacion_pos.password", "secreta123", 1);
    expect(historialRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ clave: "facturacion_pos.password", valorAnterior: null, valorNuevo: null, usuarioId: 1 }),
    );
  });
});

describe("TallerParametrosService — puente de tenant_config para los 3 toggles", () => {
  it("get() de una clave-puente sin fila usa tenant_config y la sirve de inmediato", async () => {
    const tenantConfigService = {
      getTogglesOperativos: jest.fn(async () => ({
        permiteTiendas: false,
        permiteProductosPropios: true,
        facturaHabilitada: true,
      })),
    };
    const { service } = createService({ tenantConfigService });

    await expect(service.get("tiendas.permite_crear")).resolves.toBe(false);
    expect(tenantConfigService.getTogglesOperativos).toHaveBeenCalledWith(7);
  });

  it("intenta persistir la copia hacia taller_parametros, sin bloquear la respuesta", async () => {
    const tenantConfigService = {
      getTogglesOperativos: jest.fn(async () => ({
        permiteTiendas: false,
        permiteProductosPropios: true,
        facturaHabilitada: true,
      })),
    };
    const { service, repo } = createService({ tenantConfigService });

    await service.get("tiendas.permite_crear");
    await new Promise((resolve) => setImmediate(resolve)); // deja correr el fire-and-forget

    expect(repo.save).toHaveBeenCalled();
  });

  it("si la persistencia de respaldo falla, igual devuelve el valor correcto y no rompe la promesa", async () => {
    const tenantConfigService = {
      getTogglesOperativos: jest.fn(async () => ({
        permiteTiendas: true,
        permiteProductosPropios: true,
        facturaHabilitada: true,
      })),
    };
    const repoOverride = {
      save: jest.fn(async () => {
        throw new Error("DB caída");
      }),
    };
    const { service } = createService({ tenantConfigService, repo: repoOverride });

    await expect(service.get("tiendas.permite_crear")).resolves.toBe(true);
    await new Promise((resolve) => setImmediate(resolve)); // deja correr el .catch() interno
  });

  it("una segunda lectura del mismo tenant NO vuelve a consultar tenant_config -- sirve desde caché", async () => {
    const tenantConfigService = {
      getTogglesOperativos: jest.fn(async () => ({
        permiteTiendas: false,
        permiteProductosPropios: true,
        facturaHabilitada: true,
      })),
    };
    const { service } = createService({ tenantConfigService });

    await service.get("tiendas.permite_crear");
    await service.get("tiendas.permite_crear");

    expect(tenantConfigService.getTogglesOperativos).toHaveBeenCalledTimes(1);
  });

  it("una clave-puente CON fila ya guardada nunca toca tenant_config", async () => {
    const tenantConfigService = { getTogglesOperativos: jest.fn() };
    const { service } = createService({ tenantConfigService });

    await service.set("tiendas.permite_crear", true, 1);
    tenantConfigService.getTogglesOperativos.mockClear();

    await expect(service.get("tiendas.permite_crear")).resolves.toBe(true);
    expect(tenantConfigService.getTogglesOperativos).not.toHaveBeenCalled();
  });
});

describe("TallerParametrosService.getResumenTenant", () => {
  it("enmascara claves sensibles como {configurado:boolean}, nunca el valor real", async () => {
    const { service, repo, tenantAccessor } = createService();
    const tiendaRepo = {
      find: jest.fn(async () => [{ id: 3, nombre: "Tienda 3" }]),
    };
    tenantAccessor.repositoryFor.mockImplementation((entity: any) =>
      entity?.name === "Tienda" ? tiendaRepo : entity?.name === "TallerParametroHistorial" ? { create: jest.fn(), save: jest.fn() } : repo,
    );

    await service.setParaTienda(3, "facturacion_pos.password", "secreta", 1);

    const resumen = await service.getResumenTenant();

    expect(resumen.tiendas).toEqual([
      expect.objectContaining({
        tiendaId: 3,
        nombre: "Tienda 3",
        parametros: expect.objectContaining({ "facturacion_pos.password": { configurado: true } }),
      }),
    ]);
  });

  it("incluye todas las claves tenant-scope con su valor (o default) tipado", async () => {
    const { service, tenantAccessor } = createService();
    // Captura la implementacion ORIGINAL antes de reemplazarla -- llamar
    // getMockImplementation() DENTRO de la nueva implementacion se
    // autorreferencia (devuelve la funcion que se esta definiendo, no la de
    // createService()) y recursiona infinito para cualquier entidad que no
    // sea Tienda.
    const original = tenantAccessor.repositoryFor.getMockImplementation()!;
    tenantAccessor.repositoryFor.mockImplementation((entity: any) =>
      entity?.name === "Tienda" ? { find: jest.fn(async () => []) } : original(entity),
    );

    const resumen = await service.getResumenTenant();

    expect(resumen.tenant["facturacion.usa_facturacion"]).toBe(true);
    expect(resumen.tenant["notificaciones.usa_whatsapp"]).toBe(false);
  });
});

describe("TallerParametrosService — comportamiento ante fallo", () => {
  it("getBooleanConFallback devuelve false (bloquear) para un gate que falla y bloquea por default", async () => {
    const repoQueFalla = {
      findOne: jest.fn(async () => {
        throw new Error("DB caída");
      }),
    };
    const { service } = createService({ repo: repoQueFalla });
    await expect(service.getBooleanConFallback("tiendas.permite_crear")).resolves.toBe(false);
  });

  it("getBooleanConFallback devuelve true (permitir) para un gate que falla y permite por default", async () => {
    const repoQueFalla = {
      findOne: jest.fn(async () => {
        throw new Error("DB caída");
      }),
    };
    const { service } = createService({ repo: repoQueFalla });
    await expect(service.getBooleanConFallback("productos.permite_crear")).resolves.toBe(true);
  });

  it("getFlags() nunca lanza -- cada clave aplica su propio comportamientoAnteFallo si falla", async () => {
    const repoQueFalla = {
      findOne: jest.fn(async () => {
        throw new Error("DB caída");
      }),
    };
    const { service } = createService({ repo: repoQueFalla });

    await expect(service.getFlags()).resolves.toEqual({
      usaFacturacion: false,
      permiteTiendas: false,
      permiteProductosPropios: true,
      usaWhatsapp: false,
    });
  });

  it("getFlags() en condiciones normales refleja los valores guardados", async () => {
    const { service } = createService();
    await service.set("facturacion.usa_facturacion", false, 1);

    await expect(service.getFlags()).resolves.toEqual({
      usaFacturacion: false,
      permiteTiendas: true,
      permiteProductosPropios: true,
      usaWhatsapp: false,
    });
  });

  it("getTogglesOperativosAdmin devuelve null por clave (no tumba las otras) si la tabla no existe todavía", async () => {
    const repoQueFalla = {
      findOne: jest.fn(async () => {
        throw new Error("Table 'taller_parametros' doesn't exist");
      }),
    };
    const dataSource = { getRepository: jest.fn(() => repoQueFalla) };
    const tenantConnectionRegistry = {
      withTenantDataSourceExplicit: jest.fn(async (_tenantId: number, fn: any) => fn(dataSource)),
    };
    const { service } = createService({ tenantConnectionRegistry });

    await expect(service.getTogglesOperativosAdmin(99)).resolves.toEqual({
      permiteTiendas: null,
      permiteProductosPropios: null,
      facturaHabilitada: null,
    });
  });

  it("getTogglesOperativosAdmin pide allowOnboarding:true", async () => {
    const repoOk = { findOne: jest.fn(async () => null) };
    const dataSource = { getRepository: jest.fn(() => repoOk) };
    const tenantConnectionRegistry = {
      withTenantDataSourceExplicit: jest.fn(async (_tenantId: number, fn: any) => fn(dataSource)),
    };
    const { service } = createService({ tenantConnectionRegistry });

    await service.getTogglesOperativosAdmin(99);

    expect(tenantConnectionRegistry.withTenantDataSourceExplicit).toHaveBeenCalledWith(99, expect.any(Function), {
      allowOnboarding: true,
    });
  });
});
