import { SetTenantConfigTogglesDefaultOff1781300000000 } from "../../control-plane/migrations/1781300000000-SetTenantConfigTogglesDefaultOff";
import { AddPagoConfirmadoToOrdenTrabajo1781100000000 } from "../1781100000000-AddPagoConfirmadoToOrdenTrabajo";
import { SeedParametrosOperativosEnTenantsExistentes1781200000000 } from "../1781200000000-SeedParametrosOperativosEnTenantsExistentes";
import { AddDatosEntregaToOrdenTrabajo1781400000000 } from "../1781400000000-AddDatosEntregaToOrdenTrabajo";
import { CrearPluGenericosTaller1781500000000, PLU_GENERICOS_TALLER } from "../1781500000000-CrearPluGenericosTaller";
import { TALLER_PARAMETROS_CATALOGO } from "../../taller-parametros/taller-parametros.catalogo";

// Vive en verification/ por la misma razon que migrations-shape.spec.ts: un
// .spec.ts directo en src/migrations/ lo cargaria el glob del CLI como si
// fuera una migracion.

function fakeQueryRunner(opciones: {
  tablas?: string[];
  columnas?: Record<string, string[]>;
  filasOts?: number;
}) {
  const tablas = new Set(opciones.tablas ?? []);
  const queries: Array<{ sql: string; params?: any[] }> = [];
  const addColumn = jest.fn(async () => undefined);
  const dropColumn = jest.fn(async () => undefined);
  const qr = {
    queries,
    addColumn,
    dropColumn,
    hasTable: jest.fn(async (t: string) => tablas.has(t)),
    getTable: jest.fn(async (t: string) =>
      tablas.has(t)
        ? {
            findColumnByName: (c: string) =>
              ((opciones.columnas ?? {})[t] ?? []).includes(c)
                ? { name: c }
                : undefined,
          }
        : undefined,
    ),
    query: jest.fn(async (sql: string, params?: any[]) => {
      queries.push({ sql, params });
      if (/FROM taller_ordenes_trabajo/i.test(sql)) {
        return Array.from(
          { length: Math.min(opciones.filasOts ?? 0, 1) },
          () => ({ hay: 1 }),
        );
      }
      return [];
    }),
  };
  return qr;
}

const inserts = (qr: ReturnType<typeof fakeQueryRunner>) =>
  qr.queries.filter((q) => /INSERT IGNORE/i.test(q.sql));

describe("AddPagoConfirmadoToOrdenTrabajo", () => {
  const migracion = new AddPagoConfirmadoToOrdenTrabajo1781100000000();

  it("agrega las dos columnas nullable cuando faltan", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_ordenes_trabajo"],
      columnas: { taller_ordenes_trabajo: [] },
    });
    await migracion.up(qr as any);
    const columnas = qr.addColumn.mock.calls.map((c: any[]) => c[1]);
    expect(columnas.map((c: any) => [c.name, c.type, c.isNullable])).toEqual([
      ["pago_confirmado_at", "datetime", true],
      ["pago_confirmado_por", "int", true],
    ]);
  });

  it("es idempotente: no agrega lo que ya existe", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_ordenes_trabajo"],
      columnas: {
        taller_ordenes_trabajo: ["pago_confirmado_at", "pago_confirmado_por"],
      },
    });
    await migracion.up(qr as any);
    expect(qr.addColumn).not.toHaveBeenCalled();
  });

  it("no falla si la tabla no existe", async () => {
    const qr = fakeQueryRunner({});
    await expect(migracion.up(qr as any)).resolves.toBeUndefined();
    await expect(migracion.down(qr as any)).resolves.toBeUndefined();
  });

  it("down quita solo las columnas presentes", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_ordenes_trabajo"],
      columnas: {
        taller_ordenes_trabajo: ["pago_confirmado_at", "pago_confirmado_por"],
      },
    });
    await migracion.down(qr as any);
    expect(qr.dropColumn.mock.calls.map((c: any[]) => c[1])).toEqual([
      "pago_confirmado_por",
      "pago_confirmado_at",
    ]);
  });
});

describe("AddDatosEntregaToOrdenTrabajo", () => {
  const migracion = new AddDatosEntregaToOrdenTrabajo1781400000000();
  const tablas = ["taller_ordenes_trabajo", "taller_ot_firmas"];

  it("agrega las columnas de entrega nullable en la OT y en las firmas", async () => {
    const qr = fakeQueryRunner({
      tablas,
      columnas: { taller_ordenes_trabajo: [], taller_ot_firmas: [] },
    });
    await migracion.up(qr as any);
    const agregadas = (qr.addColumn.mock.calls as any[][]).map((c) => [
      c[0],
      c[1].name,
      c[1].isNullable,
    ]);
    expect(agregadas).toEqual([
      ["taller_ordenes_trabajo", "km_salida", true],
      ["taller_ordenes_trabajo", "nivel_gasolina_salida", true],
      ["taller_ordenes_trabajo", "recomendaciones_entrega", true],
      ["taller_ot_firmas", "firma_entrega", true],
      ["taller_ot_firmas", "nombre_firma_entrega", true],
      ["taller_ot_firmas", "fecha_firma_entrega", true],
    ]);
  });

  it("es idempotente: no agrega lo que ya existe", async () => {
    const qr = fakeQueryRunner({
      tablas,
      columnas: {
        taller_ordenes_trabajo: [
          "km_salida",
          "nivel_gasolina_salida",
          "recomendaciones_entrega",
        ],
        taller_ot_firmas: [
          "firma_entrega",
          "nombre_firma_entrega",
          "fecha_firma_entrega",
        ],
      },
    });
    await migracion.up(qr as any);
    expect(qr.addColumn).not.toHaveBeenCalled();
  });

  it("no falla si las tablas no existen", async () => {
    const qr = fakeQueryRunner({});
    await expect(migracion.up(qr as any)).resolves.toBeUndefined();
    await expect(migracion.down(qr as any)).resolves.toBeUndefined();
  });

  it("down quita solo las columnas presentes", async () => {
    const qr = fakeQueryRunner({
      tablas,
      columnas: {
        taller_ordenes_trabajo: ["km_salida"],
        taller_ot_firmas: ["firma_entrega", "fecha_firma_entrega"],
      },
    });
    await migracion.down(qr as any);
    expect((qr.dropColumn.mock.calls as any[][]).map((c) => c[1])).toEqual([
      "fecha_firma_entrega",
      "firma_entrega",
      "km_salida",
    ]);
  });
});

describe("CrearPluGenericosTaller", () => {
  const migracion = new CrearPluGenericosTaller1781500000000();
  const columnasMaeplu = [
    "plu", "desclarga", "precio", "costo", "iva", "pagaiva", "es_servicio", "usainventario", "es_taller", "es_producto",
  ];
  const columnasDetuni = ["plu", "unidad", "precio", "costo", "sicosto", "sicostoue"];
  const conErp = () =>
    fakeQueryRunner({
      tablas: ["maeplu", "detuni"],
      columnas: { maeplu: columnasMaeplu, detuni: columnasDetuni },
    });
  const insertsEn = (qr: ReturnType<typeof fakeQueryRunner>, tabla: string) =>
    qr.queries.filter((q) => new RegExp(`INSERT INTO ${tabla}\\b`, "i").test(q.sql));

  it("crea los dos PLU genericos en maeplu y detuni, solo si no existen", async () => {
    const qr = conErp();
    await migracion.up(qr as any);

    const enMaeplu = insertsEn(qr, "maeplu");
    const enDetuni = insertsEn(qr, "detuni");
    expect(enMaeplu).toHaveLength(2);
    expect(enDetuni).toHaveLength(2);
    // Idempotente por construccion: cada INSERT lleva su NOT EXISTS.
    for (const q of [...enMaeplu, ...enDetuni]) {
      expect(q.sql).toMatch(/WHERE NOT EXISTS/i);
    }
    expect(enMaeplu.map((q) => q.params![0])).toEqual(["TALLER-PROD", "TALLER-SERV"]);
    expect(enDetuni.map((q) => q.params![0])).toEqual(["TALLER-PROD", "TALLER-SERV"]);
  });

  it("el producto no es servicio y lleva costo simbolico; el servicio es servicio; ninguno usa inventario", async () => {
    const qr = conErp();
    await migracion.up(qr as any);

    const [prod, serv] = insertsEn(qr, "maeplu").map((q) => q.params as any[]);
    // columnas: plu, desclarga, precio, costo, iva, pagaiva, es_servicio, usainventario, ...
    expect(prod.slice(0, 8)).toEqual(["TALLER-PROD", "PRODUCTOS VARIOS", 0, 0.01, 12, "S", "N", "N"]);
    expect(serv.slice(0, 8)).toEqual(["TALLER-SERV", "SERVICIOS VARIOS", 0, 0, 12, "S", "S", "N"]);
  });

  it("sin es_taller/es_producto no los incluye en el insert", async () => {
    const qr = fakeQueryRunner({
      tablas: ["maeplu", "detuni"],
      columnas: { maeplu: columnasMaeplu.filter((c) => !c.startsWith("es_t") && c !== "es_producto"), detuni: columnasDetuni },
    });
    await migracion.up(qr as any);

    expect(insertsEn(qr, "maeplu")).toHaveLength(2);
    expect(insertsEn(qr, "maeplu")[0].sql).not.toMatch(/es_taller|es_producto/);
  });

  it("sin tablas del ERP, o con columnas faltantes, no hace nada", async () => {
    for (const qr of [
      fakeQueryRunner({}),
      fakeQueryRunner({ tablas: ["maeplu"], columnas: { maeplu: columnasMaeplu } }),
      fakeQueryRunner({ tablas: ["maeplu", "detuni"], columnas: { maeplu: columnasMaeplu, detuni: ["plu", "unidad"] } }),
    ]) {
      await expect(migracion.up(qr as any)).resolves.toBeUndefined();
      expect(qr.queries.filter((q) => /INSERT/i.test(q.sql))).toHaveLength(0);
    }
  });

  it("down no borra nada (pueden estar facturados contra esos PLU)", async () => {
    const qr = conErp();
    await migracion.down();
    expect(qr.queries).toHaveLength(0);
  });

  it("los codigos coinciden con el valor por defecto de los parametros de la tienda", () => {
    const [prod, serv] = PLU_GENERICOS_TALLER;
    expect(TALLER_PARAMETROS_CATALOGO["facturacion_pos.plu_productos_taller"].default).toBe(prod.plu);
    expect(TALLER_PARAMETROS_CATALOGO["facturacion_pos.plu_servicios_taller"].default).toBe(serv.plu);
  });
});

describe("SeedParametrosOperativosEnTenantsExistentes", () => {
  const migracion =
    new SeedParametrosOperativosEnTenantsExistentes1781200000000();

  it("siembra 'true' en las 22 claves nuevas cuando la base ya tiene OTs", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_parametros", "taller_ordenes_trabajo"],
      filasOts: 3,
    });
    await migracion.up(qr as any);

    const filas = inserts(qr);
    expect(filas).toHaveLength(22);
    const claves = filas.map((q) => q.params![0]);
    expect(new Set(claves).size).toBe(22);
    expect(claves).toEqual(
      expect.arrayContaining(["ot.usa_fotos", "facturacion.exige_pago_para_entrega"]),
    );
    for (const q of filas) {
      expect(q.sql).toMatch(/INSERT IGNORE INTO taller_parametros/);
      expect(q.sql).toMatch(/'tenant', 0, '', 'boolean', 'true'/);
    }
  });

  it("NUNCA siembra las 3 claves puente: pisaria lo que un operador apago en tenant_config (p. ej. encenderia la facturacion de un cliente que la tenia apagada)", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_parametros", "taller_ordenes_trabajo"],
      filasOts: 3,
    });
    await migracion.up(qr as any);

    const claves = inserts(qr).map((q) => q.params![0]);
    expect(claves).not.toContain("tiendas.permite_crear");
    expect(claves).not.toContain("productos.permite_crear");
    expect(claves).not.toContain("facturacion.usa_facturacion");
  });

  it("no siembra nada en una base sin OTs (sitio nuevo)", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_parametros", "taller_ordenes_trabajo"],
      filasOts: 0,
    });
    await migracion.up(qr as any);
    expect(inserts(qr)).toHaveLength(0);
  });

  it("no hace nada si falta taller_parametros o taller_ordenes_trabajo", async () => {
    const sinParametros = fakeQueryRunner({
      tablas: ["taller_ordenes_trabajo"],
      filasOts: 3,
    });
    await migracion.up(sinParametros as any);
    expect(sinParametros.queries).toHaveLength(0);

    const sinOts = fakeQueryRunner({ tablas: ["taller_parametros"] });
    await migracion.up(sinOts as any);
    expect(inserts(sinOts)).toHaveLength(0);
  });

  it("down borra solo las 22 claves nuevas sembradas (sin historial) y no las 3 puente", async () => {
    const qr = fakeQueryRunner({
      tablas: ["taller_parametros", "taller_parametros_historial"],
    });
    await migracion.down(qr as any);

    expect(qr.queries).toHaveLength(1);
    const { sql, params } = qr.queries[0];
    expect(sql).toMatch(/DELETE FROM taller_parametros/);
    expect(sql).toMatch(/NOT EXISTS/);
    expect(params).toHaveLength(22);
    expect(params).not.toContain("tiendas.permite_crear");
    expect(params).not.toContain("facturacion.usa_facturacion");
  });
});

describe("SetTenantConfigTogglesDefaultOff", () => {
  const migracion = new SetTenantConfigTogglesDefaultOff1781300000000();
  const columnas = [
    "permite_tiendas",
    "permite_productos_propios",
    "factura_habilitada",
  ];

  it("cambia solo el DEFAULT a 0, sin tocar valores guardados", async () => {
    const qr = fakeQueryRunner({
      tablas: ["tenant_config"],
      columnas: { tenant_config: columnas },
    });
    await migracion.up(qr as any);
    expect(qr.queries).toHaveLength(3);
    for (const q of qr.queries) {
      expect(q.sql).toMatch(
        /ALTER TABLE tenant_config ALTER COLUMN \w+ SET DEFAULT 0/,
      );
      expect(q.sql).not.toMatch(/UPDATE/i);
    }
  });

  it("down restaura el DEFAULT 1", async () => {
    const qr = fakeQueryRunner({
      tablas: ["tenant_config"],
      columnas: { tenant_config: columnas },
    });
    await migracion.down(qr as any);
    expect(qr.queries.every((q) => /SET DEFAULT 1/.test(q.sql))).toBe(true);
  });

  it("no falla si tenant_config no existe", async () => {
    await expect(
      migracion.up(fakeQueryRunner({}) as any),
    ).resolves.toBeUndefined();
  });

  describe("respaldo de tenants existentes sin fila (conservan 'todo encendido')", () => {
    const conTenants = () =>
      fakeQueryRunner({
        tablas: ["tenant_config", "tenants"],
        columnas: { tenant_config: columnas },
      });

    it("crea fila con todo en 1 SOLO para tenants que no tienen fila, y ANTES de bajar el default", async () => {
      const qr = conTenants();
      await migracion.up(qr as any);

      const [primera, ...resto] = qr.queries;
      expect(primera.sql).toMatch(/INSERT INTO tenant_config/);
      expect(primera.sql).toMatch(/SELECT t\.id, 1, 1, 1 FROM tenants t/);
      expect(primera.sql).toMatch(/WHERE NOT EXISTS \(SELECT 1 FROM tenant_config c WHERE c\.tenant_id = t\.id\)/);
      expect(resto).toHaveLength(3);
      expect(resto.every((q) => /SET DEFAULT 0/.test(q.sql))).toBe(true);
    });

    it("nunca modifica filas existentes (ni UPDATE ni REPLACE)", async () => {
      const qr = conTenants();
      await migracion.up(qr as any);

      expect(qr.queries.some((q) => /UPDATE|REPLACE|ON DUPLICATE/i.test(q.sql))).toBe(false);
    });

    it("sin tabla tenants o sin las columnas no inserta nada", async () => {
      const sinTenants = fakeQueryRunner({
        tablas: ["tenant_config"],
        columnas: { tenant_config: columnas },
      });
      await migracion.up(sinTenants as any);
      expect(sinTenants.queries.some((q) => /INSERT/i.test(q.sql))).toBe(false);

      const sinColumnas = fakeQueryRunner({
        tablas: ["tenant_config", "tenants"],
        columnas: { tenant_config: [] },
      });
      await migracion.up(sinColumnas as any);
      expect(sinColumnas.queries.some((q) => /INSERT/i.test(q.sql))).toBe(false);
    });

    it("el down no borra filas (indistinguibles de las creadas a mano)", async () => {
      const qr = conTenants();
      await migracion.down(qr as any);

      expect(qr.queries.some((q) => /DELETE/i.test(q.sql))).toBe(false);
    });
  });
});
