import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Mock the HubSpot client module before importing the helper. The helper
// calls `getHubSpotClient()` lazily inside each retry attempt, so we
// re-create the mock on every test to control behaviour per-test.
const updateMock = vi.fn();
vi.mock("./client", () => ({
  getHubSpotClient: () => ({
    crm: { deals: { basicApi: { update: updateMock } } },
  }),
}));

import { syncMeasuresFieldToHubSpot, syncSurveyRequestToHubSpot } from "./dealSync";

describe("syncMeasuresFieldToHubSpot", () => {
  beforeEach(() => {
    updateMock.mockReset();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("joins measure names with a semicolon and pushes to the named property", async () => {
    updateMock.mockResolvedValueOnce(undefined);
    const result = await syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-1",
      propName: "instructed_measures",
      measureNames: ["ASHP", "Solar PV", "Loft insulation"],
    });
    expect(result).toEqual({ ok: true });
    expect(updateMock).toHaveBeenCalledTimes(1);
    expect(updateMock).toHaveBeenCalledWith("deal-1", {
      properties: { instructed_measures: "ASHP;Solar PV;Loft insulation" },
    });
  });

  it("sends an empty string when the list is empty (clear field)", async () => {
    updateMock.mockResolvedValueOnce(undefined);
    const result = await syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-2",
      propName: "instructed_measures",
      measureNames: [],
    });
    expect(result).toEqual({ ok: true });
    expect(updateMock).toHaveBeenCalledWith("deal-2", {
      properties: { instructed_measures: "" },
    });
  });

  it("retries up to 3 times on ECONNRESET and succeeds on the final attempt", async () => {
    vi.useFakeTimers();
    const reset1 = Object.assign(new Error("reset"), { code: "ECONNRESET" });
    const reset2 = Object.assign(new Error("reset"), { code: "ECONNRESET" });
    updateMock
      .mockRejectedValueOnce(reset1)
      .mockRejectedValueOnce(reset2)
      .mockResolvedValueOnce(undefined);

    const promise = syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-3",
      propName: "instructed_measures",
      measureNames: ["EWI"],
    });
    // Advance timers past the two backoff windows (200ms then 400ms).
    await vi.advanceTimersByTimeAsync(200);
    await vi.advanceTimersByTimeAsync(400);
    const result = await promise;
    expect(result).toEqual({ ok: true });
    expect(updateMock).toHaveBeenCalledTimes(3);
  });

  it("returns failure when ECONNRESET persists past the third attempt", async () => {
    vi.useFakeTimers();
    const reset = Object.assign(new Error("network reset"), {
      code: "ECONNRESET",
    });
    updateMock
      .mockRejectedValueOnce(reset)
      .mockRejectedValueOnce(reset)
      .mockRejectedValueOnce(reset);

    const promise = syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-4",
      propName: "instructed_measures",
      measureNames: ["IWI"],
    });
    await vi.advanceTimersByTimeAsync(200);
    await vi.advanceTimersByTimeAsync(400);
    const result = await promise;
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBe("network reset");
    }
    expect(updateMock).toHaveBeenCalledTimes(3);
  });

  it("does not retry for non-ECONNRESET errors", async () => {
    const boom = new Error("HubSpot 400 — invalid property value");
    updateMock.mockRejectedValueOnce(boom);
    const result = await syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-5",
      propName: "instructed_measures",
      measureNames: ["DMevs"],
    });
    expect(result).toEqual({
      ok: false,
      error: "HubSpot 400 — invalid property value",
    });
    expect(updateMock).toHaveBeenCalledTimes(1);
  });

  it("works for any property name (so PIBI slice 4 can reuse it)", async () => {
    updateMock.mockResolvedValueOnce(undefined);
    await syncMeasuresFieldToHubSpot({
      hubspotDealId: "deal-6",
      propName: "pibi_ordered_measures",
      measureNames: ["ASHP", "EWI"],
    });
    expect(updateMock).toHaveBeenCalledWith("deal-6", {
      properties: { pibi_ordered_measures: "ASHP;EWI" },
    });
  });
});

describe("syncSurveyRequestToHubSpot", () => {
  beforeEach(() => {
    updateMock.mockReset();
  });

  it("writes survey type to osmosis_survey_required and date to osmosis_survey_date", async () => {
    updateMock.mockResolvedValueOnce(undefined);
    const requestedAt = new Date("2026-05-06T10:00:00.000Z");
    const result = await syncSurveyRequestToHubSpot({
      hubspotDealId: "deal-99",
      surveyType: "technical_building_survey",
      requestedAt,
    });
    expect(result).toEqual({ ok: true });
    expect(updateMock).toHaveBeenCalledWith("deal-99", {
      properties: {
        osmosis_survey_required: "technical_building_survey",
        osmosis_survey_date: "2026-05-06",
      },
    });
  });

  it("returns ok: false with error message on HubSpot failure", async () => {
    updateMock.mockRejectedValueOnce(new Error("HubSpot 400"));
    const result = await syncSurveyRequestToHubSpot({
      hubspotDealId: "deal-99",
      surveyType: "technical_building_survey",
      requestedAt: new Date(),
    });
    expect(result).toEqual({ ok: false, error: "HubSpot sync failed" });
  });
});
