import { jobProgress } from "./ops.service";

/** Shape run_all.py emits, with the shell trace lines it interleaves. */
const line = (i: number, n: number, name: string) =>
  `[paso ${i}/${n}] ${name}\n+ /repo/migration/.venv/bin/python /repo/migration/${name} --env prod\n[${name}] target env: prod\n  validation: OK`;

describe("jobProgress", () => {
  it("returns null before any step marker appears", () => {
    // The safety backup runs before run_all.py, so this is the real state for
    // the first stretch of every REIMPORT.
    expect(jobProgress("== Respaldo de seguridad previo ==\ntablas capturadas: 39", "RUNNING")).toBeNull();
  });

  it("returns null for jobs that have no steps at all", () => {
    // BACKUP/RESTORE are a single mysqldump; a fabricated percentage would be
    // worse than none.
    expect(jobProgress("mysqldump ... done", "SUCCESS")).toBeNull();
  });

  it("tracks the most recent marker, not the first", () => {
    const log = [line(1, 9, "transform_customers.py"), line(2, 9, "transform_properties.py")].join("\n");
    const p = jobProgress(log, "RUNNING");
    expect(p).toMatchObject({ step: 2, total: 9, name: "transform_properties.py" });
  });

  /**
   * The point of the whole feature. While RUNNING, step i is IN PROGRESS, so
   * only i-1 are done. Counting i as complete would show 100% while the final
   * and slowest step (blob_extract) is still working.
   */
  it("does not claim a running step is finished", () => {
    expect(jobProgress(line(1, 9, "transform_customers.py"), "RUNNING")?.percent).toBe(0);
    expect(jobProgress(line(9, 9, "blob_extract.py"), "RUNNING")?.percent).toBe(88);
  });

  it("reaches 100 only once the job is no longer running", () => {
    expect(jobProgress(line(9, 9, "blob_extract.py"), "SUCCESS")?.percent).toBe(100);
  });

  /** A job that died mid-way must report where it died, not 100%. */
  it("reports the failed step rather than completion", () => {
    const p = jobProgress(line(5, 9, "transform_transactions.py"), "FAILED");
    expect(p).toMatchObject({ step: 5, total: 9 });
    expect(p!.percent).toBe(55);
  });

  it("handles the 8-step SYNC list as well as the 9-step REIMPORT one", () => {
    expect(jobProgress(line(8, 8, "transform_bank.py"), "SUCCESS")?.percent).toBe(100);
    expect(jobProgress(line(4, 8, "transform_policies.py"), "RUNNING")?.percent).toBe(37);
  });

  /**
   * Captured verbatim from `run_all.run(..., step=8, total=9)`. This is the
   * contract between the Python and this parser; if run_all.py's format
   * changes, this fails rather than the panel silently showing no progress.
   */
  it("parses the exact line run_all.py emits", () => {
    const real =
      "[paso 8/9] transform_bank.py\n+ /repo/migration/.venv/bin/python /repo/migration/transform_bank.py --env prod";
    expect(jobProgress(real, "RUNNING")).toMatchObject({
      step: 8,
      total: 9,
      name: "transform_bank.py",
      percent: 77,
    });
  });

  it("ignores a malformed marker instead of reporting NaN", () => {
    expect(jobProgress("[paso 3/0] x.py", "RUNNING")).toBeNull();
  });

  /** The marker must be at line start so log text quoting it cannot spoof it. */
  it("does not match a marker embedded mid-line", () => {
    expect(jobProgress("some output mentioning [paso 4/9] fake.py", "RUNNING")).toBeNull();
  });
});
