import { describe, it, expect, vi } from "vitest";
import { bulkApprove } from "./bulkApprove";
import type { RunBulkApproveTx, SyncApprovalsForDeals } from "./bulkApprove";

const noopSync: SyncApprovalsForDeals = async () => ({ ok: true });

function makeSuccessTx(approvedByDeal: Record<string, string[]> = {}): RunBulkApproveTx {
  return async ({ changes }) => {
    const dealIds = [...new Set(changes.map((c) => c.hubspotDealId))];
    const result: Record<string, string[]> = {};
    for (const id of dealIds) {
      result[id] = approvedByDeal[id] ?? [];
    }
    return { approvedMeasuresByDeal: result };
  };
}

describe("bulkApprove", () => {
  it("returns ok:false when changes array is empty", async () => {
    const result = await bulkApprove({
      changes: [],
      userId: 1n,
      deps: { runBulkApproveTx: makeSuccessTx(), syncApprovals: noopSync },
    });
    expect(result).toEqual({ ok: false, error: "changes must not be empty" });
  });

  it("calls tx with all changes and returns ok:true on success", async () => {
    const txSpy = vi.fn(makeSuccessTx({ "deal-1": ["Loft insulation"] }));
    const result = await bulkApprove({
      changes: [{ hubspotDealId: "deal-1", measureName: "Loft insulation", approved: true }],
      userId: 1n,
      deps: { runBulkApproveTx: txSpy, syncApprovals: noopSync },
    });
    expect(result.ok).toBe(true);
    expect(txSpy).toHaveBeenCalledOnce();
    expect(txSpy.mock.calls[0][0].changes).toHaveLength(1);
  });

  it("returns ok:false when tx throws (atomic rollback)", async () => {
    const failingTx: RunBulkApproveTx = async () => {
      throw new Error("DB constraint violated");
    };
    const result = await bulkApprove({
      changes: [{ hubspotDealId: "deal-1", measureName: "CWI", approved: true }],
      userId: 1n,
      deps: { runBulkApproveTx: failingTx, syncApprovals: noopSync },
    });
    expect(result).toMatchObject({ ok: false, error: "DB constraint violated" });
  });

  it("returns ok:true with hubspotSync:'failed' when HubSpot sync fails", async () => {
    const failingSync: SyncApprovalsForDeals = async () => ({
      ok: false,
      error: "HubSpot timeout",
    });
    const result = await bulkApprove({
      changes: [{ hubspotDealId: "deal-1", measureName: "CWI", approved: true }],
      userId: 1n,
      deps: { runBulkApproveTx: makeSuccessTx(), syncApprovals: failingSync },
    });
    expect(result).toMatchObject({ ok: true, hubspotSync: "failed", hubspotError: "HubSpot timeout" });
  });

  it("calls sync once per affected deal", async () => {
    const syncSpy = vi.fn(noopSync);
    await bulkApprove({
      changes: [
        { hubspotDealId: "deal-1", measureName: "CWI", approved: true },
        { hubspotDealId: "deal-2", measureName: "ASHP", approved: true },
        { hubspotDealId: "deal-1", measureName: "EWI", approved: true },
      ],
      userId: 1n,
      deps: {
        runBulkApproveTx: makeSuccessTx({
          "deal-1": ["CWI", "EWI"],
          "deal-2": ["ASHP"],
        }),
        syncApprovals: syncSpy,
      },
    });
    expect(syncSpy).toHaveBeenCalledOnce();
    const call = syncSpy.mock.calls[0][0];
    expect(Object.keys(call.approvedMeasuresByDeal).sort()).toEqual(["deal-1", "deal-2"]);
  });
});
