import { ForbiddenException } from "@nestjs/common";
import { WhatsappService } from "./whatsapp.service";
import { EstadoOT } from "../ots/entities/orden-trabajo.entity";

function createService(overrides: Record<string, any> = {}) {
  const clienteRepository = {
    findOne: jest.fn(async () => ({
      nombre: "Juan Perez",
      celular: "12345678",
      telefono: null,
    })),
    ...overrides.clienteRepository,
  };

  const tenantAccessor = {
    repositoryFor: jest.fn(() => clienteRepository),
    getCurrentTenantId: jest.fn(() => 42),
    ...overrides.tenantAccessor,
  };

  const otsService = {
    findById: jest.fn(async () => ({
      id: 7,
      numeroOt: "OT-1234",
      estado: EstadoOT.EN_PROCESO,
      clienteTarjeta: "2031",
      vehiculo: { placa: "P123ABC" },
    })),
    logEvento: jest.fn(async () => undefined),
    ...overrides.otsService,
  };

  const cotizacionesService = { ...overrides.cotizacionesService };

  const configService = {
    get: jest.fn((_key: string, def?: any) => def),
    ...overrides.configService,
  };

  const valores: Record<string, any> = {
    "notificaciones.usa_whatsapp": true,
    "notificaciones.whatsapp_access_token": "token-real",
    "notificaciones.whatsapp_phone_number_id": "1234567890",
    "notificaciones.whatsapp_api_version": "v25.0",
    "notificaciones.whatsapp_default_country_code": "502",
    "notificaciones.whatsapp_template_estado": "",
    "notificaciones.whatsapp_template_cotizacion": "",
    "notificaciones.whatsapp_template_language": "es_GT",
    ...overrides.valores,
  };
  const tallerParametros = {
    get: jest.fn(async (clave: string) => valores[clave]),
    // Funciones apagables: encendidas salvo que el test las apague.
    getBooleanConFallback: jest.fn(async (clave: string) => (overrides.flags ?? {})[clave] ?? true),
    exigirHabilitado: jest.fn(async (clave: string) => {
      if (!((overrides.flags ?? {})[clave] ?? true)) {
        throw new ForbiddenException(`${clave} apagado`);
      }
    }),
    ...overrides.tallerParametros,
  };

  const service = new WhatsappService(
    configService as any,
    cotizacionesService as any,
    otsService as any,
    tenantAccessor as any,
    tallerParametros as any,
  );

  return {
    service,
    clienteRepository,
    tenantAccessor,
    otsService,
    configService,
    tallerParametros,
  };
}

describe("WhatsappService.sendEstadoOt — configuración por tenant, sin fallback a env", () => {
  it("lanza un error claro si notificaciones.usa_whatsapp está apagado, sin llamar a fetch", async () => {
    const fetchMock = jest.fn();
    (global as any).fetch = fetchMock;
    const { service } = createService({
      valores: { "notificaciones.usa_whatsapp": false },
    });

    await expect(service.sendEstadoOt(7, { id: 1 })).rejects.toThrow(
      /WhatsApp no está activado/,
    );
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it("lanza un error claro si falta access token o phone number id, no un error de 'undefined'", async () => {
    const fetchMock = jest.fn();
    (global as any).fetch = fetchMock;
    const { service } = createService({
      valores: { "notificaciones.whatsapp_access_token": "" },
    });

    await expect(service.sendEstadoOt(7, { id: 1 })).rejects.toThrow(
      /WhatsApp no está configurado/,
    );
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it("usa el código de país del TENANT (no una constante fija) para números locales de 8 dígitos", async () => {
    const fetchMock = jest.fn(async () => ({
      ok: true,
      text: async () => JSON.stringify({ messages: [{ id: "wamid.1" }] }),
    }));
    (global as any).fetch = fetchMock;
    const { service } = createService({
      valores: { "notificaciones.whatsapp_default_country_code": "1" },
    });

    await service.sendEstadoOt(7, { id: 1 });

    // Cast necesario: jest.fn(async () => ({...})) sin parámetros tipados se
    // infiere como Mock<..., []>, así que TS ve mock.calls[0] como tupla
    // vacía. El cast preserva exactamente el comportamiento/aserciones del
    // brief, solo evita el error de compilación de ts-jest (TS2493).
    const [, requestInit] = fetchMock.mock.calls[0] as any[];
    const body = JSON.parse((requestInit as any).body);
    expect(body.to).toBe("112345678");
  });

  it("envía con éxito cuando la configuración del tenant está completa", async () => {
    const fetchMock = jest.fn(async () => ({
      ok: true,
      text: async () => JSON.stringify({ messages: [{ id: "wamid.1" }] }),
    }));
    (global as any).fetch = fetchMock;
    const { service } = createService();

    const result = await service.sendEstadoOt(7, { id: 1 });

    expect(result.ok).toBe(true);
    expect(fetchMock).toHaveBeenCalledWith(
      "https://graph.facebook.com/v25.0/1234567890/messages",
      expect.objectContaining({
        headers: expect.objectContaining({
          Authorization: "Bearer token-real",
        }),
      }),
    );
  });
});

describe("WhatsappService — funciones apagables", () => {
  it("sendCotizacion con WhatsApp apagado NO crea el link ni cambia estados (antes generaba el link, pasaba la cotizacion a ENVIADA y recien despues lanzaba 400)", async () => {
    const createPublicLink = jest.fn();
    const { service } = createService({
      valores: { "notificaciones.usa_whatsapp": false },
      cotizacionesService: { createPublicLink },
    });

    await expect(service.sendCotizacion(5, { id: 1 })).rejects.toThrow(/WhatsApp no está activado/);

    expect(createPublicLink).not.toHaveBeenCalled();
  });

  it("sendCotizacion con el link publico apagado tampoco crea el link", async () => {
    const createPublicLink = jest.fn();
    const { service } = createService({
      flags: { "cotizacion.usa_link_publico": false },
      cotizacionesService: { createPublicLink },
    });

    await expect(service.sendCotizacion(5, { id: 1 })).rejects.toThrow("cotizacion.usa_link_publico");

    expect(createPublicLink).not.toHaveBeenCalled();
  });

  it("sendEstadoOt con el estado publico apagado rechaza antes de buscar la OT o mandar nada", async () => {
    const fetchMock = jest.fn();
    (global as any).fetch = fetchMock;
    const { service, otsService } = createService({ flags: { "ot.usa_estado_publico": false } });

    await expect(service.sendEstadoOt(7, { id: 1 })).rejects.toThrow("ot.usa_estado_publico");

    expect(otsService.findById).not.toHaveBeenCalled();
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it("sendCotizacion sin mecanicos no menciona al mecanico en el mensaje al cliente", async () => {
    const mensajes: string[] = [];
    (global as any).fetch = jest.fn(async (_url: string, init: any) => {
      mensajes.push(JSON.parse(init.body).text?.body ?? "");
      return { ok: true, text: async () => JSON.stringify({ messages: [{ id: "wamid.1" }] }) };
    });
    const armar = (flags: Record<string, boolean>) =>
      createService({
        flags,
        cotizacionesService: {
          createPublicLink: jest.fn(async () => ({ publicPath: "/cotizacion/1.abc" })),
          findById: jest.fn(async () => ({ id: 5, otId: 7, numeroCotizacion: "COT-1", total: 100 })),
        },
        otsService: {
          findById: jest.fn(async () => ({
            id: 7,
            numeroOt: "OT-1",
            clienteTarjeta: "2031",
            vehiculo: { placa: "P123" },
            mecanico: { nombre: "Luis" },
          })),
        },
      }).service;

    await armar({ "ot.usa_mecanicos": false }).sendCotizacion(5, { id: 1 });
    await armar({}).sendCotizacion(5, { id: 1 });

    expect(mensajes[0]).not.toMatch(/Mecanico/i);
    expect(mensajes[1]).toMatch(/Mecanico asignado: Luis/);
  });
});
