import { describe, expect, it, vi } from "vitest";
import {
  applyDealPropertyUpdate,
  DEAL_PROPERTY_FIELDS,
  isDealPropertyField,
  roleAllowedForField,
} from "./dealPropertyUpdate";

describe("DEAL_PROPERTY_FIELDS registry", () => {
  it("exposes the two PIBI date fields with write-or-above permissions", () => {
    expect(isDealPropertyField("pibi_order_date")).toBe(true);
    expect(isDealPropertyField("pibi_completed_date")).toBe(true);
    expect(roleAllowedForField("pibi_order_date", "read")).toBe(false);
    expect(roleAllowedForField("pibi_order_date", "write")).toBe(true);
    expect(roleAllowedForField("pibi_order_date", "admin")).toBe(true);
    expect(roleAllowedForField("pibi_order_date", "creator")).toBe(true);
    expect(roleAllowedForField("pibi_completed_date", "read")).toBe(false);
    expect(roleAllowedForField("pibi_completed_date", "write")).toBe(true);
  });

  it("exposes the halted fields gated on the approver capability", () => {
    expect(isDealPropertyField("property_halted_date")).toBe(true);
    expect(isDealPropertyField("property_halted_reason")).toBe(true);
    // Plain roles — even the most permissive — never satisfy the approver
    // gate on their own.
    for (const role of ["read", "write", "admin", "creator"]) {
      expect(roleAllowedForField("property_halted_date", role)).toBe(false);
      expect(roleAllowedForField("property_halted_reason", role)).toBe(false);
    }
    // Capability list grants access regardless of role tier.
    expect(
      roleAllowedForField("property_halted_date", "read", ["approver"]),
    ).toBe(true);
    expect(
      roleAllowedForField("property_halted_reason", "write", ["approver"]),
    ).toBe(true);
    // Other capabilities should not unlock the field.
    expect(
      roleAllowedForField("property_halted_date", "write", ["contractor"]),
    ).toBe(false);
  });

  it("exposes the domna survey fields gated on the approver capability", () => {
    expect(isDealPropertyField("domna_survey_type")).toBe(true);
    expect(isDealPropertyField("domna_survey_date")).toBe(true);
    // Plain roles never satisfy the approver gate on their own.
    for (const role of ["read", "write", "admin", "creator"]) {
      expect(roleAllowedForField("domna_survey_type", role)).toBe(false);
      expect(roleAllowedForField("domna_survey_date", role)).toBe(false);
    }
    // Approver capability unlocks both.
    expect(
      roleAllowedForField("domna_survey_type", "read", ["approver"]),
    ).toBe(true);
    expect(
      roleAllowedForField("domna_survey_date", "read", ["approver"]),
    ).toBe(true);
    // Other capabilities do not unlock the fields.
    expect(
      roleAllowedForField("domna_survey_type", "write", ["contractor"]),
    ).toBe(false);
  });

  it("maps each registered field to the matching HubSpot property", () => {
    expect(DEAL_PROPERTY_FIELDS.pibi_order_date.hubspotProperty).toBe(
      "pibi_order_date",
    );
    expect(DEAL_PROPERTY_FIELDS.pibi_completed_date.hubspotProperty).toBe(
      "pibi_completed_date",
    );
    expect(DEAL_PROPERTY_FIELDS.property_halted_date.hubspotProperty).toBe(
      "property_halted_date",
    );
    expect(DEAL_PROPERTY_FIELDS.property_halted_reason.hubspotProperty).toBe(
      "property_halted_reason",
    );
    expect(DEAL_PROPERTY_FIELDS.domna_survey_type.hubspotProperty).toBe(
      "osmosis_survey_required",
    );
    expect(DEAL_PROPERTY_FIELDS.domna_survey_date.hubspotProperty).toBe(
      "osmosis_survey_date",
    );
  });

  it("rejects unknown fields", () => {
    expect(isDealPropertyField("not_a_field")).toBe(false);
  });
});

describe("applyDealPropertyUpdate", () => {
  it("rejects non-whitelisted fields without writing or syncing", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-1",
      fields: { unknown_field: "x" },
      role: "write",
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.unknown_field).toEqual({
      ok: false,
      error: "Field not editable",
    });
    expect(out.hubspotSync).toBe("skipped");
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("rejects PIBI fields when the user role is read", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-1",
      fields: { pibi_order_date: "2025-01-15" },
      role: "read",
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.pibi_order_date).toEqual({
      ok: false,
      error: "Insufficient permissions",
    });
    expect(out.hubspotSync).toBe("skipped");
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("rejects values that fail validation", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-1",
      fields: { pibi_order_date: "not-a-real-date" },
      role: "write",
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.pibi_order_date.ok).toBe(false);
    expect(out.hubspotSync).toBe("skipped");
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("persists valid fields to the DB and pushes them to HubSpot", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const orderIso = "2025-03-12T00:00:00.000Z";
    const completedIso = "2025-04-02T00:00:00.000Z";

    const out = await applyDealPropertyUpdate({
      dealId: "deal-42",
      fields: {
        pibi_order_date: orderIso,
        pibi_completed_date: completedIso,
      },
      role: "write",
      deps: { updateDb, pushHubspot },
    });

    expect(out.results.pibi_order_date).toEqual({ ok: true });
    expect(out.results.pibi_completed_date).toEqual({ ok: true });
    expect(out.hubspotSync).toBe("ok");

    expect(updateDb).toHaveBeenCalledTimes(1);
    const [dealIdArg, valuesArg] = updateDb.mock.calls[0];
    expect(dealIdArg).toBe("deal-42");
    expect(valuesArg.pibiOrderDate).toBeInstanceOf(Date);
    expect((valuesArg.pibiOrderDate as Date).toISOString()).toBe(orderIso);
    expect((valuesArg.pibiCompletedDate as Date).toISOString()).toBe(
      completedIso,
    );

    expect(pushHubspot).toHaveBeenCalledTimes(1);
    const pushArg = pushHubspot.mock.calls[0][0];
    expect(pushArg.hubspotDealId).toBe("deal-42");
    // HubSpot expects epoch milliseconds as strings for date properties.
    expect(pushArg.properties.pibi_order_date).toBe(
      String(new Date(orderIso).getTime()),
    );
    expect(pushArg.properties.pibi_completed_date).toBe(
      String(new Date(completedIso).getTime()),
    );
  });

  it("clears a date when null is supplied (sends empty string to HubSpot)", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const out = await applyDealPropertyUpdate({
      dealId: "deal-7",
      fields: { pibi_order_date: null },
      role: "admin",
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.pibi_order_date).toEqual({ ok: true });
    expect(out.hubspotSync).toBe("ok");
    expect(updateDb.mock.calls[0][1].pibiOrderDate).toBeNull();
    expect(pushHubspot.mock.calls[0][0].properties.pibi_order_date).toBe("");
  });

  it("rejects halted fields when the caller lacks approver capability", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-h1",
      fields: {
        property_halted_date: "2025-06-01T00:00:00.000Z",
        property_halted_reason: "Awaiting access",
      },
      // Even creator role alone is not enough — capability is orthogonal.
      role: "creator",
      capabilities: [],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.property_halted_date).toEqual({
      ok: false,
      error: "Insufficient permissions",
    });
    expect(out.results.property_halted_reason).toEqual({
      ok: false,
      error: "Insufficient permissions",
    });
    expect(out.hubspotSync).toBe("skipped");
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("persists halted date + reason for an approver and pushes them to HubSpot", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const haltedIso = "2025-06-01T00:00:00.000Z";
    const reason = "Awaiting roof access";

    const out = await applyDealPropertyUpdate({
      dealId: "deal-h2",
      fields: {
        property_halted_date: haltedIso,
        property_halted_reason: reason,
      },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });

    expect(out.results.property_halted_date).toEqual({ ok: true });
    expect(out.results.property_halted_reason).toEqual({ ok: true });
    expect(out.hubspotSync).toBe("ok");

    const dbValues = updateDb.mock.calls[0][1];
    expect(dbValues.propertyHaltedDate).toBeInstanceOf(Date);
    expect((dbValues.propertyHaltedDate as Date).toISOString()).toBe(haltedIso);
    expect(dbValues.propertyHaltedReason).toBe(reason);

    const props = pushHubspot.mock.calls[0][0].properties;
    expect(props.property_halted_date).toBe(
      String(new Date(haltedIso).getTime()),
    );
    expect(props.property_halted_reason).toBe(reason);
  });

  it("validates the halted date format", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-h3",
      fields: { property_halted_date: "definitely-not-a-date" },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.property_halted_date.ok).toBe(false);
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("collapses an empty halted reason to null on save", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const out = await applyDealPropertyUpdate({
      dealId: "deal-h4",
      fields: { property_halted_reason: "" },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.property_halted_reason).toEqual({ ok: true });
    expect(updateDb.mock.calls[0][1].propertyHaltedReason).toBeNull();
    expect(pushHubspot.mock.calls[0][0].properties.property_halted_reason).toBe(
      "",
    );
  });

  it("resume clears the halted date and leaves the reason untouched in DB + HubSpot payload", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    // The drawer's "Resume" action only sends `property_halted_date: null`.
    // The reason field is omitted entirely so the existing value is
    // preserved.
    const out = await applyDealPropertyUpdate({
      dealId: "deal-h5",
      fields: { property_halted_date: null },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.property_halted_date).toEqual({ ok: true });
    expect(out.results.property_halted_reason).toBeUndefined();
    expect(out.hubspotSync).toBe("ok");

    const dbValues = updateDb.mock.calls[0][1];
    expect(dbValues.propertyHaltedDate).toBeNull();
    // No reason key at all → reason column not touched.
    expect("propertyHaltedReason" in dbValues).toBe(false);

    const props = pushHubspot.mock.calls[0][0].properties;
    expect(props.property_halted_date).toBe("");
    // No reason key in the HubSpot payload → reason not pushed.
    expect("property_halted_reason" in props).toBe(false);
  });

  it("rejects domna fields when the caller lacks approver capability", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-d1",
      fields: {
        domna_survey_type: "Standard",
        domna_survey_date: "2025-07-15T00:00:00.000Z",
      },
      // Even creator role alone is not enough — capability is orthogonal.
      role: "creator",
      capabilities: [],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.domna_survey_type).toEqual({
      ok: false,
      error: "Insufficient permissions",
    });
    expect(out.results.domna_survey_date).toEqual({
      ok: false,
      error: "Insufficient permissions",
    });
    expect(out.hubspotSync).toBe("skipped");
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("persists domna survey type + date for an approver and pushes them to HubSpot", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const surveyType = "Detailed";
    const surveyIso = "2025-07-15T00:00:00.000Z";

    const out = await applyDealPropertyUpdate({
      dealId: "deal-d2",
      fields: {
        domna_survey_type: surveyType,
        domna_survey_date: surveyIso,
      },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });

    expect(out.results.domna_survey_type).toEqual({ ok: true });
    expect(out.results.domna_survey_date).toEqual({ ok: true });
    expect(out.hubspotSync).toBe("ok");

    const dbValues = updateDb.mock.calls[0][1];
    expect(dbValues.domnaSurveyType).toBe(surveyType);
    expect(dbValues.domnaSurveyDate).toBeInstanceOf(Date);
    expect((dbValues.domnaSurveyDate as Date).toISOString()).toBe(surveyIso);

    const props = pushHubspot.mock.calls[0][0].properties;
    expect(props.osmosis_survey_required).toBe(surveyType);
    expect(props.osmosis_survey_date).toBe(
      String(new Date(surveyIso).getTime()),
    );
  });

  it("validates the domna survey date format", async () => {
    const updateDb = vi.fn();
    const pushHubspot = vi.fn();
    const out = await applyDealPropertyUpdate({
      dealId: "deal-d3",
      fields: { domna_survey_date: "definitely-not-a-date" },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.domna_survey_date.ok).toBe(false);
    expect(updateDb).not.toHaveBeenCalled();
    expect(pushHubspot).not.toHaveBeenCalled();
  });

  it("lets domna survey type and date be settable independently", async () => {
    // Setting only the type — date column is untouched.
    const updateDbType = vi.fn().mockResolvedValue(undefined);
    const pushHubspotType = vi.fn().mockResolvedValue({ ok: true });
    const outType = await applyDealPropertyUpdate({
      dealId: "deal-d4",
      fields: { domna_survey_type: "Standard" },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb: updateDbType, pushHubspot: pushHubspotType },
    });
    expect(outType.results.domna_survey_type).toEqual({ ok: true });
    expect(outType.results.domna_survey_date).toBeUndefined();
    const typeOnlyDb = updateDbType.mock.calls[0][1];
    expect(typeOnlyDb.domnaSurveyType).toBe("Standard");
    expect("domnaSurveyDate" in typeOnlyDb).toBe(false);
    const typeOnlyProps = pushHubspotType.mock.calls[0][0].properties;
    expect(typeOnlyProps.osmosis_survey_required).toBe("Standard");
    expect("osmosis_survey_date" in typeOnlyProps).toBe(false);

    // Setting only the date — type column is untouched.
    const updateDbDate = vi.fn().mockResolvedValue(undefined);
    const pushHubspotDate = vi.fn().mockResolvedValue({ ok: true });
    const surveyIso = "2025-08-20T00:00:00.000Z";
    const outDate = await applyDealPropertyUpdate({
      dealId: "deal-d4b",
      fields: { domna_survey_date: surveyIso },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb: updateDbDate, pushHubspot: pushHubspotDate },
    });
    expect(outDate.results.domna_survey_date).toEqual({ ok: true });
    expect(outDate.results.domna_survey_type).toBeUndefined();
    const dateOnlyDb = updateDbDate.mock.calls[0][1];
    expect(dateOnlyDb.domnaSurveyDate).toBeInstanceOf(Date);
    expect("domnaSurveyType" in dateOnlyDb).toBe(false);
    const dateOnlyProps = pushHubspotDate.mock.calls[0][0].properties;
    expect(dateOnlyProps.osmosis_survey_date).toBe(
      String(new Date(surveyIso).getTime()),
    );
    expect("osmosis_survey_required" in dateOnlyProps).toBe(false);
  });

  it("clears both domna fields to null when explicitly cleared", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
    const out = await applyDealPropertyUpdate({
      dealId: "deal-d5",
      fields: {
        // empty string collapses to null (mirrors the date schema)
        domna_survey_type: "",
        domna_survey_date: null,
      },
      role: "read",
      capabilities: ["approver"],
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.domna_survey_type).toEqual({ ok: true });
    expect(out.results.domna_survey_date).toEqual({ ok: true });
    const dbValues = updateDb.mock.calls[0][1];
    expect(dbValues.domnaSurveyType).toBeNull();
    expect(dbValues.domnaSurveyDate).toBeNull();
    const props = pushHubspot.mock.calls[0][0].properties;
    expect(props.osmosis_survey_required).toBe("");
    expect(props.osmosis_survey_date).toBe("");
  });

  it("surfaces HubSpot push failures back to the caller", async () => {
    const updateDb = vi.fn().mockResolvedValue(undefined);
    const pushHubspot = vi
      .fn()
      .mockResolvedValue({ ok: false, error: "boom" });
    const out = await applyDealPropertyUpdate({
      dealId: "deal-9",
      fields: { pibi_order_date: "2025-05-01T00:00:00.000Z" },
      role: "write",
      deps: { updateDb, pushHubspot },
    });
    expect(out.results.pibi_order_date).toEqual({ ok: true });
    expect(out.hubspotSync).toBe("failed");
    expect(out.hubspotError).toBe("boom");
    // DB update still happened — UI gets the optimistic value, error is
    // surfaced separately.
    expect(updateDb).toHaveBeenCalledTimes(1);
  });
});
