/**
 * Unit tests for the PIBI-selection service (issue #254).
 *
 * These tests exercise the orchestration logic — DB transaction +
 * post-commit HubSpot push — by injecting lightweight fakes for the DB
 * hooks. The tests never touch the real DB or HubSpot client.
 *
 * Key properties verified:
 *   - Happy path: rows inserted, HubSpot push called with correct property,
 *     pushed_at stamped.
 *   - No approval rows are created or modified.
 *   - Sync semantics: pushed_at null when HubSpot fails.
 *   - DB failure: returns ok=false, no HubSpot call.
 *   - Empty selection: clears all rows and pushes an empty list.
 */
import { describe, expect, it, vi } from "vitest";
import {
  PIBI_MEASURES_PROP,
  selectPibiMeasures,
} from "./selectPibiMeasures";
import type {
  RunPibiTx,
  StampPushedAt,
  SyncMeasuresField,
} from "./selectPibiMeasures";

function makeDeps(overrides?: {
  txResult?: { insertedRowIds: bigint[] };
  txError?: Error;
  syncResult?: { ok: true } | { ok: false; error: string };
  stampError?: Error;
}) {
  const txResult = overrides?.txResult ?? { insertedRowIds: [1n, 2n] };

  const runPibiTx: RunPibiTx = vi.fn(async () => {
    if (overrides?.txError) throw overrides.txError;
    return txResult;
  });

  const syncMeasuresField: SyncMeasuresField = vi.fn(async () => {
    return overrides?.syncResult ?? ({ ok: true } as const);
  });

  const stampPushedAt: StampPushedAt = vi.fn(async () => {
    if (overrides?.stampError) throw overrides.stampError;
  });

  return { runPibiTx, syncMeasuresField, stampPushedAt };
}

describe("selectPibiMeasures — happy path", () => {
  it("commits the tx, pushes to HubSpot under the PIBI property, stamps pushed_at", async () => {
    const deps = makeDeps({
      txResult: { insertedRowIds: [10n, 11n] },
    });

    const result = await selectPibiMeasures({
      dealId: "deal-1",
      measureNames: ["ASHP", "Solar PV"],
      userId: 5n,
      deps,
    });

    expect(result).toMatchObject({
      ok: true,
      insertedRowIds: [10n, 11n],
      hubspotSync: "ok",
    });

    expect(deps.runPibiTx).toHaveBeenCalledWith({
      dealId: "deal-1",
      measureNames: ["ASHP", "Solar PV"],
      userId: 5n,
    });

    // Must push to the PIBI property specifically, not instructed_measures.
    expect(deps.syncMeasuresField).toHaveBeenCalledTimes(1);
    expect(deps.syncMeasuresField).toHaveBeenCalledWith({
      hubspotDealId: "deal-1",
      propName: PIBI_MEASURES_PROP,
      measureNames: ["ASHP", "Solar PV"],
    });

    expect(deps.stampPushedAt).toHaveBeenCalledWith([10n, 11n]);
  });

  it("handles an empty selection — clears rows and pushes an empty list", async () => {
    const deps = makeDeps({
      txResult: { insertedRowIds: [] },
    });

    const result = await selectPibiMeasures({
      dealId: "deal-2",
      measureNames: [],
      userId: 3n,
      deps,
    });

    expect(result).toMatchObject({
      ok: true,
      insertedRowIds: [],
      hubspotSync: "ok",
    });

    expect(deps.syncMeasuresField).toHaveBeenCalledWith({
      hubspotDealId: "deal-2",
      propName: PIBI_MEASURES_PROP,
      measureNames: [],
    });

    // Nothing to stamp when no rows were inserted.
    expect(deps.stampPushedAt).toHaveBeenCalledWith([]);
  });
});

describe("selectPibiMeasures — no approval rows touched", () => {
  it("never calls any approval-related function", async () => {
    // The deps object only exposes pibi-specific hooks; if the service
    // called any approval function it would have to import it separately.
    // We simply confirm the service returns ok=true and only the three
    // expected hooks were invoked — no approval side-effects possible.
    const deps = makeDeps();
    const result = await selectPibiMeasures({
      dealId: "deal-3",
      measureNames: ["EWI"],
      userId: 1n,
      deps,
    });
    expect(result.ok).toBe(true);
    // Only these three hooks should exist / be called.
    expect(Object.keys(deps)).toEqual(["runPibiTx", "syncMeasuresField", "stampPushedAt"]);
  });
});

describe("selectPibiMeasures — DB transaction failure", () => {
  it("returns ok=false and skips HubSpot when the tx throws", async () => {
    const deps = makeDeps({ txError: new Error("insert failed") });

    const result = await selectPibiMeasures({
      dealId: "deal-x",
      measureNames: ["ASHP"],
      userId: 1n,
      deps,
    });

    expect(result).toEqual({ ok: false, error: "insert failed" });
    expect(deps.syncMeasuresField).not.toHaveBeenCalled();
    expect(deps.stampPushedAt).not.toHaveBeenCalled();
  });
});

describe("selectPibiMeasures — HubSpot push failure leaves pushed_at null", () => {
  it("returns ok=true with hubspotSync=failed and does NOT stamp pushed_at", async () => {
    const deps = makeDeps({
      txResult: { insertedRowIds: [20n] },
      syncResult: { ok: false, error: "hubspot 503" },
    });

    const result = await selectPibiMeasures({
      dealId: "deal-h",
      measureNames: ["CWI"],
      userId: 2n,
      deps,
    });

    expect(result).toMatchObject({
      ok: true,
      insertedRowIds: [20n],
      hubspotSync: "failed",
      hubspotError: "hubspot 503",
    });

    // DB was committed (tx called) but pushed_at NOT stamped.
    expect(deps.runPibiTx).toHaveBeenCalledTimes(1);
    expect(deps.stampPushedAt).not.toHaveBeenCalled();
  });
});

describe("selectPibiMeasures — sync called with correct property name", () => {
  it("always passes measures_for_pibi_ordered as propName, never instructed_measures", async () => {
    const deps = makeDeps();
    await selectPibiMeasures({
      dealId: "deal-prop",
      measureNames: ["Loft insulation", "CWI"],
      userId: 7n,
      deps,
    });

    const callArg = (deps.syncMeasuresField as ReturnType<typeof vi.fn>).mock
      .calls[0][0] as { propName: string };
    expect(callArg.propName).toBe("measures_for_pibi_ordered");
    expect(callArg.propName).not.toBe("instructed_measures");
  });
});
